码个蛋(codeegg) 第1049 次推文

作者:彭也

链接:https://zhuanlan.zhihu.com/p/193117308

前言

废话不多,有图有**

虽然老罗的锤子处于倒闭与即将倒闭的量子叠加状态,但锤子在手机行业还是带来了些许影响。拟物化的UI风格在当时众多同质化的扁平UI风格中脱颖而出。如同在一群母牛中混入了一头母犀牛——不一样的牛逼。

一日,当我仔细观察锤子的Swtich控件时,其中的交互细节使我汗颜。可以夸张地说,这个小小的控件里包含了一万个细节,如同光秃秃的脑袋里装着满满的疑惑。

内阴影、外阴影、按压效果、立体模拟等等,每个细节的完美呈现才能支撑这个控件的交互逻辑。

要开发出这个控件,难度很大。这篇文章,就来开发这个锤子的开关控件。

真·做个锤子的开关。

一、UI拆解

1.1 一万个细节分析

1.1.1 内阴影

开关背景的内阴影

状态指示器的内阴影

1.1.2 开关指示器的立体效果

注意,开关指示器带有立体感,这种立体感是通过内阴影凸显的。可以观察到,开关指示器带有从右下角往左上角方向投射的内阴影。

1.1.3 外阴影

开关指示器的外阴影,带有扩散效果

1.1.4 阴影变化

内阴影和外阴影的变化模拟出了3D按压的效果,仿佛用户真的将整个控件从屏幕外向屏幕里进行了按压。而当用户的手指离开控件时,控件内部仿佛有弹簧一般,又从屏幕内向屏幕外进行了弹出。

1.1.5 滑动回位

当用户手指滑动距离不算长时,此时控件的开关状态并不会发生改变,而是动画回归原状态的位置

1.2 形状拆解

1.1.1 开关背景

圆角矩形没跑了,需要注意的是形状的宽高,需要为阴影绘制留出距离

1.1.2 开关指示器

聪明人都知道这是个圆形[doge]

1.1.3 状态指示器

这不是圆形是啥?

二、实现方式

绘制代码onDraw中,按照形状分类拆分不同的方法。需要注意的是,先绘制状态指示器,它在最下层

@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);setLayerType(LAYER_TYPE_SOFTWARE, null);//绘制状态指示器drawFlag(canvas);//绘制背景区域drawBackgroundArea(canvas);//绘制开关指示器drawIndicator(canvas);
}

2.1 UI绘制

2.1.1 开关背景

内阴影的绘制思路我在《微质感的层级选择器,隔壁产品都馋哭了》提过,感兴趣的请翻看我的往期文章。代码如下:

void drawBackgroundArea(Canvas canvas) {//绘制边框及内阴影canvas.save();backgroundAreaPaint.setStyle(Paint.Style.STROKE);int strokeW = indicatorR / 2;backgroundAreaPaint.setStrokeWidth(strokeW);backgroundAreaPaint.setColor(Color.parseColor("#66bcbcbc"));backgroundAreaShadowSize = backgroundAreaH / 4;backgroundAreaShadowDistance = backgroundAreaH / 12;backgroundAreaPaint.setShadowLayer(backgroundAreaShadowSize + shadowOffset, 0, backgroundAreaShadowDistance, Color.GRAY);RectF strokeRectF = new RectF(-strokeW + (width - backgroundAreaW) / 2, -strokeW + (height - backgroundAreaH) / 2, strokeW + (width - backgroundAreaW) / 2 + backgroundAreaW, strokeW + (height - backgroundAreaH) / 2 + backgroundAreaH);Path strokePath = new Path();strokePath.addRoundRect(strokeRectF, (backgroundAreaH + strokeW) / 2, (backgroundAreaH + strokeW) / 2, Path.Direction.CW);RectF rectF = new RectF((width - backgroundAreaW) / 2, (height - backgroundAreaH) / 2, (width - backgroundAreaW) / 2 + backgroundAreaW, (height - backgroundAreaH) / 2 + backgroundAreaH);Path path = new Path();path.addRoundRect(rectF, backgroundAreaH / 2, backgroundAreaH / 2, Path.Direction.CW);canvas.clipPath(path);canvas.drawPath(strokePath, backgroundAreaPaint);backgroundAreaPaint.setStrokeWidth(2);backgroundAreaPaint.clearShadowLayer();canvas.drawPath(path, backgroundAreaPaint);canvas.restore();
}

2.1.2 开关指示器

其中包括了凸显立体感的内阴影以及外阴影的绘制,见代码:

void drawIndicator(Canvas canvas) {//绘制外阴影indicatorPaint.setColor(indicatorColor);indicatorPaint.setStyle(Paint.Style.FILL);indicatorShadowSize = indicatorR / 3;indicatorShadowDistance = indicatorShadowSize / 2;indicatorPaint.setShadowLayer(indicatorShadowSize - shadowOffset, 0, indicatorShadowDistance, Color.parseColor("#ffc1c1c1"));canvas.drawCircle(indicatorX + indicatorXOffset, (height - backgroundAreaH) / 2 + indicatorR, indicatorR, indicatorPaint);//绘制内阴影canvas.save();indicatorPaint.setColor(Color.parseColor("#66bcbcbc"));int strokeW = indicatorR / 2;indicatorPaint.setStrokeWidth(strokeW);indicatorPaint.setStyle(Paint.Style.STROKE);indicatorPaint.setShadowLayer(indicatorR / 3, -indicatorR / 6, -indicatorR / 6, Color.parseColor("#fff1f1f1"));Path strokePath = new Path();strokePath.addCircle(indicatorX + indicatorXOffset, (height - backgroundAreaH) / 2 + indicatorR, indicatorR + strokeW / 2, Path.Direction.CW);Path path = new Path();path.addCircle(indicatorX + indicatorXOffset, (height - backgroundAreaH) / 2 + indicatorR, indicatorR, Path.Direction.CW);canvas.clipPath(path);canvas.drawPath(strokePath, indicatorPaint);indicatorPaint.setStrokeWidth(2);indicatorPaint.clearShadowLayer();canvas.drawPath(path, indicatorPaint);canvas.restore();
}

2.1.3 状态指示器

这一步中也包含内阴影的绘制。额外注意连续调用两次canvas.save方法,通过clipPath裁剪出背景区域形状的画布。

void drawFlag(Canvas canvas) {//首先裁剪出背景圆角矩形画布canvas.save();RectF rectF = new RectF((width - backgroundAreaW) / 2, (height - backgroundAreaH) / 2, (width - backgroundAreaW) / 2 + backgroundAreaW, (height - backgroundAreaH) / 2 + backgroundAreaH);Path bgAreaPath = new Path();bgAreaPath.addRoundRect(rectF, backgroundAreaH / 2, backgroundAreaH / 2, Path.Direction.CW);canvas.clipPath(bgAreaPath);//绘制on flagflagPaint.setStyle(Paint.Style.FILL);flagPaint.setColor(onColor);flagPaint.clearShadowLayer();canvas.drawCircle(indicatorX + indicatorXOffset - backgroundAreaW * 3 / 5, height / 2, indicatorR / 4, flagPaint);//内阴影flagPaint.setStyle(Paint.Style.STROKE);int onStrokeW = indicatorR / 4;flagPaint.setStrokeWidth(onStrokeW);flagPaint.setShadowLayer(onStrokeW, -onStrokeW, onStrokeW, onColor);Path onPath = new Path();onPath.addCircle(indicatorX + indicatorXOffset - backgroundAreaW * 3 / 5, height / 2, indicatorR / 4 + onStrokeW / 2, Path.Direction.CW);canvas.save();canvas.clipPath(onPath);canvas.drawPath(onPath, flagPaint);flagPaint.clearShadowLayer();canvas.restore();//绘制off flagflagPaint.setStyle(Paint.Style.FILL);flagPaint.setColor(offColor);canvas.drawCircle(indicatorX + indicatorXOffset + backgroundAreaW * 3 / 5, height / 2, indicatorR / 4, flagPaint);//内阴影flagPaint.setStyle(Paint.Style.STROKE);int offStrokeW = indicatorR / 4;flagPaint.setStrokeWidth(offStrokeW);flagPaint.setShadowLayer(offStrokeW, -offStrokeW, offStrokeW, offColor);Path offPath = new Path();offPath.addCircle(indicatorX + indicatorXOffset + backgroundAreaW * 3 / 5, height / 2, indicatorR / 4 + offStrokeW / 2, Path.Direction.CW);canvas.save();canvas.clipPath(offPath);canvas.drawPath(offPath, flagPaint);canvas.restore();canvas.restore();
}

2.2 交互实现

2.2.1 边界判断

当用户滑动超过边界时,强制重新赋值

indicatorXOffset = (int) (event.getX() - downX);
//边界判断
if (indicatorX + indicatorXOffset <= (width - backgroundAreaW) / 2 + indicatorR) {indicatorXOffset = (width - backgroundAreaW) / 2 + indicatorR - indicatorX;
} else if (indicatorX + indicatorXOffset >= width - (width - backgroundAreaW) / 2 - indicatorR) {indicatorXOffset = width - (width - backgroundAreaW) / 2 - indicatorR - indicatorX;
}

2.2.2 区分滑动和点按

注意一个细节,当用户的滑动距离非常小时,算作点按,此时控件的开关状态要改变;滑动距离超过一定阈值时,算滑动操作,此时控件的开关状态不一定改变

if (Math.abs(indicatorXOffset) <= 20) {//todo:点按操作
}else{//todo:滑动操作
}

2.2.3 状态变化

定义一个字段isChecked表示开关状态,根据开关指示器位置判断状态是否应该改变

if ((indicatorXOffset > 0 && indicatorXOffset >= (backgroundAreaW - 2 * indicatorR) / 2) || (indicatorXOffset < 0 && indicatorXOffset > -(backgroundAreaW - 2 * indicatorR) / 2)) {indicatorXOffset = 0;//切换状态:ONisChecked = true;startTranslateAnim(true);
} else if ((indicatorXOffset > 0 && indicatorXOffset < (backgroundAreaW - 2 * indicatorR) / 2) || (indicatorXOffset < 0 && indicatorXOffset <= -(backgroundAreaW - 2 * indicatorR) / 2)) {indicatorXOffset = 0;//切换状态:OFFisChecked = false;startTranslateAnim(false);
}

2.3 细节实现

2.3.1 阴影变化

封装成阴影变化动画,通过Animator计算

//开始阴影变化动画
if (shadowAnimator != null) {shadowAnimator.cancel();
}
shadowAnimator = ValueAnimator.ofInt(0, indicatorR / 4);
shadowAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {shadowOffset = (int) animation.getAnimatedValue();postInvalidate();
}
});
shadowAnimator.setDuration(200L);
shadowAnimator.start();

2.3.2 开关状态回位、变化动画

封装一个平移动画方法,更具状态判断要移动的目标位置

/**
* @param isChecked true==>移动至on状态;false==>移动至off状态
*/
void startTranslateAnim(boolean isChecked) {if (translateAnimator != null) {translateAnimator.cancel();}if (isChecked == true) {translateAnimator = ValueAnimator.ofInt(indicatorX + indicatorXOffset, width - (width - backgroundAreaW) / 2 - indicatorR);} else {translateAnimator = ValueAnimator.ofInt(indicatorX + indicatorXOffset, (width - backgroundAreaW) / 2 + indicatorR);}translateAnimator.setDuration(200L);translateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {indicatorX = (int) valueAnimator.getAnimatedValue();postInvalidate();}
});
translateAnimator.start();
}

三、后记

此控件仿照锤子UI风格,部分阴影颜色、深度做了略微调整。该控件100%代码绘制,定制方便。

回顾锤子系统的整个体验,能够深刻感觉到他们团队在无数个细节中流的血汗和掉的头发,很多细节都能给用户带来惊喜。但现在锤子苟延残喘,不甚唏嘘。有时候反思,细节真的决定成败么?答案是否定的。

可以这么说,锤子因为它的无数个细节傲视整个手机市场,也因为无数个细节而迷失了发展方向。大爆炸、闪念胶囊等功能都是颇具创新的,但也无法弥补糟糕的手感、“迷人”的拍照效果、系统体验卡顿等一系列大方向体验中的短板。

一个好的产品,永远不是因为细节越多就会越好,而是在大方向正确的情况下,细节越多越具竞争力。

控件地址在gitee

加vx:pengyeah888 获取

点个赞再走,拒绝白嫖,从你我做起

只要你是程序员,在这里都能找到你想要的东西!
除此之外,还有每周送书活动!

做个锤子的开关,隔壁产品都馋哭了相关推荐

  1. 花里胡哨的3D翻页卡片,隔壁产品都馋哭了

    阅读完本文约需7分钟. 废不说,看图,有图有** 带有立体纵深的卡片翻页效果,稍加组合和颜色变化就可以搭配出多种不同的风格,如: 比赛比分牌 卡片翻页时钟 一.设计思路 如何使得数字的变化更为灵动?3 ...

  2. 便利贴撕页效果,隔壁产品都馋哭了

    阅读完本文约需 10 分钟. 废不说,有图 逼真模拟便利贴撕页效果,在一些需要分步操作的场景非常适合. 用户的每一步操作都非常清晰,撕页的效果可以提醒用户上一步操作已经圆满完成了. 比如,现在用户在填 ...

  3. android 控件随手指移动_液体流动控件,隔壁产品都馋哭了

    作者:彭也 链接: https://www.jianshu.com/p/4f0844c72e8a 模拟液体流动的展开特效,适合一些需要侧边展开进行辅助说明的页面,如用户在填写某个表单,需要操作很多步骤 ...

  4. android 根据bounds坐标进行点击操作_炫酷的Android时钟UI控件,隔壁产品都馋哭了...

    废话不多说,先上效果效果酷炫,动画丰富,效果爆炸boom-设计思路看腻了市面上各种丑陋难看的时钟控件,是时候整点新活!将现实生活中的摆钟圆形表盘设计.电子手表的数显表盘设计抽象出来,提取出" ...

  5. 程序员为这支笔掰头10个月,隔壁小学生都馋哭了

    鱼羊 萧箫 发自 凹非寺 量子位 报道 | 公众号 QbitAI 见证一款互联网风格的新智能硬件诞生,是一种怎样的体验? 产品经理和软硬件工程师们的面对面battle,总是其中见(xi)怪(wen)不 ...

  6. 用了5年的旧笔记本不要丢,1/4新机价格升级机器学习战斗本,隔壁研究员都馋哭了...

    大数据文摘出品 来源:medium 编译:zeroinfinity.Andy 高性能的硬件配置在机器学习研究中是枪炮一般的存在.面对大规模数据和多层结构算法,普通的计算机根本无法胜任. 但是,高配置的 ...

  7. 电脑主机,晚上就煎肉,把隔壁宿舍都馋哭了!

    点击上方"大鱼机器人",选择"置顶/星标公众号" 福利干货,第一时间送达! 本文转自网络,如有侵权请联系我们删除 ------------------分割线-- ...

  8. 全国最好吃的大学食堂来啦!隔壁小孩都馋哭了!

    考研择校千千万 有人看学术实力.有人看院校排名 有人看导师水准.有人看未来发展 但俗话说的好 民以食为天 不吃饱怎么搞学习? 全国最好吃的大学食堂 今天给大家盘一下 凭食堂打下优秀口碑 干饭人看完别流 ...

  9. 擎创动态 | 1024 这么过,隔壁公司都馋哭了

    左手咖啡杯,右手耍魔方 身穿潮流衣,眼戴金丝镜 发型酷帅炫,多才又多艺 百变外观,精致容颜 可盐可甜,又御又萌 技术高超,能力卓越 没错,描述的这种靓仔靓女 就是擎创的中流砥柱--程序员 1024,是 ...

最新文章

  1. 浅谈UWB(超宽带)室内定位技术(转载)
  2. 解决Minimum supported Gradle version is 3.3. Current version is 2.14.1问题
  3. 团体——L1-005 考试座位号 (15 分)
  4. 数组遍历——Vue.js
  5. torch 双线性上采样
  6. 动手---sbt(2)
  7. 2015年4月8日主从不同步故障解决(字符集导致)
  8. ajax 乱码问题 以及Response.charset=GB2312
  9. 内存(Display)、显示器(Monitor)和计算机(Computer)均属于一种产品(Product),其中计算机需要显示器和内存。请用Python语言简要实现这些类及它们之间的关系。
  10. form表单获取input对象浏览器区别
  11. 北大教授李忠:谁说学数学只是为了升学?数学可以让你受益终生!
  12. 4.19计算机网络笔记
  13. Windows Phone 7 优秀开源项目概览 来源:http://www.cnblogs.com/porscheyin/archive/2010/12/15/1906476.html...
  14. ZooKeeper 到底解决了什么问题?
  15. java图的邻接表实现两种方式及实例应用分析
  16. 联系人存储ContactsProvider表分析
  17. 外虚内实是什么意思_俗语“五虚令人贫,五实人富贵”是什么意思?有道理吗?...
  18. 数据湖:网易严选的数据湖实践
  19. 小孩学计算机的电视剧,小时候在教育频道看的一个电视剧,好像是叮当演的,讲一堆小孩学唱戏之类的...
  20. WebSocket连接wss链接

热门文章

  1. 【新手基础教程】音频处理之关键词识别
  2. vant 索引城市不对_vue,vant,使用过程中 Swipe 轮播自定义大小遇到的坑
  3. Tom and Jerry
  4. 今日学到:Day 2
  5. 笔记本电脑的电源显示没有了
  6. 主流DDR内存芯片与编号识别
  7. 【C语言 strlen函数的实现】
  8. 初识scrapy框架,美空图片爬虫实战
  9. 《Python编程快速上手——让繁琐工作自动化》笔记:3.11 实践项目 Collatz 序列(考拉咨猜想)
  10. MySQL函数find_in_set介绍