一日,当我仔细观察锤子的Swtich控件时,其中的交互细节使我汗颜。这个小小的控件里好像包含了一万个细节,如同光秃秃的脑袋里装着满满的疑惑。
内阴影、外阴影、按压效果、立体模拟等等,每个细节的完美呈现才能支撑这个控件的交互逻辑。
要开发出这个控件,难度很大。
这篇文章,就来开发这个锤子的开关控件。
真·做个锤子的开关。
_1_UI拆解
1.1.1 内阴影
开关背景的内阴影
状态指示器的内阴影
1.1.2 开关指示器的立体效果
注意,开关指示器带有立体感,这种立体感是通过内阴影凸显的。可以观察到,开关指示器带有从右下角往左上角方向投射的内阴影。
1.1.3 外阴影
开关指示器的外阴影,带有扩散效果
1.1.4 阴影变化
内阴影和外阴影的变化模拟出了3D按压的效果,仿佛用户真的将整个控件从屏幕外向屏幕里进行了按压。而当用户的手指离开控件时,控件内部仿佛有弹簧一般,又从屏幕内向屏幕外进行了弹出。
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDU
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
5NjQxNC04MjdkMzJlMDI4OGNiZDdl?x-oss-process=image/format,png)
1.1.5 滑动回位
当用户手指滑动距离不算长时,此时控件的开关状态并不会发生改变,而是动画回归原状态的位置.
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.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 边界判断
当用户滑动超过边界时,强制重新赋值