博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
做个酷炫的“锤子” 开关效果,隔壁产品都馋哭了
阅读量:2454 次
发布时间:2019-05-10

本文共 8856 字,大约阅读时间需要 29 分钟。

0概述

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

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

要开发出这个控件,难度很大。

这篇文章,就来开发这个锤子的开关控件。

真·做个锤子的开关。

1UI拆解

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中,按照形状分类拆分不同的方法。需要注意的是,先绘制状态指示器,它在最下层

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

2.1 UI绘制

2.1.1 开关背景

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

https://zhuanlan.zhihu.com/p/185805279

代码如下:

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 flag        flagPaint.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 flag        flagPaint.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;  //切换状态:ON  isChecked = true;  startTranslateAnim(true);} else if ((indicatorXOffset > 0 && indicatorXOffset < (backgroundAreaW - 2 * indicatorR) / 2) || (indicatorXOffset < 0 && indicatorXOffset <= -(backgroundAreaW - 2 * indicatorR) / 2)) {  indicatorXOffset = 0;  //切换状态:OFF  isChecked = 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() {                                 @Override                                 public 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() {                                      @Override                                      public void onAnimationUpdate(ValueAnimator valueAnimator) {    indicatorX = (int) valueAnimator.getAnimatedValue();    postInvalidate();  }});translateAnimator.start();}

后记

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

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

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

控件地址在gitee

https://gitee.com/null_077_5468/uidemos

转载地址:http://cmchb.baihongyu.com/

你可能感兴趣的文章
unity 作弊_屏幕作弊没问题,Unity打开,等等
查看>>
推动互操作性,OpenStack卡座等
查看>>
linkedin开源列表_Google的新容器项目,LinkedIn上的开源代码,Raspberry Pi B +,等等
查看>>
openstack项目_软件定义的经济,OpenStack的新孵化项目等
查看>>
git项目中的子git项目_使用子模块和子树管理Git项目
查看>>
sh脚本和bash脚本_使用此简单的Bash脚本在家打印双面文档
查看>>
raspberry pi_使用Raspberry Pi构建感知假肢
查看>>
raspberry pi_一个方便的实用程序,用于创建Raspberry Pi SD卡图像
查看>>
盲打每分钟资源10几个字_每个系统管理员应了解的10个资源
查看>>
横向扩展基础架构_您应该使用的7种基础架构性能和扩展工具
查看>>
bbc 王超_BBC Microbit入门
查看>>
sysadmin默认密码_Sysadmin感谢日的礼物想法
查看>>
如何在Kubernetes上找到您的Jenkins管理员密码
查看>>
ansible操作数据库_以数据为中心的Ansible修补系统方法
查看>>
c++编写音乐播放器_为什么此开发人员编写了快速响应的音乐播放器
查看>>
github pages_使用此HTTP hack重定向GitHub Pages网站
查看>>
python tox_使用tox自动化Python代码测试
查看>>
python cython_使用Cython为Python编写更快的C扩展
查看>>
flake8变量未使用_使用flake8确保Python代码的一致性
查看>>
ssh与gpg区别_如何使用GPG密钥启用SSH访问进行身份验证
查看>>