做个酷炫的“锤子” 开关效果,隔壁产品都馋哭了

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 开关背景

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

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 开关状态回位、变化动画

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

/**

最后

小编这些年深知大多数初中级工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

链接:https://pan.baidu.com/s/1BUbENbinlO0KpI5aQDA1JA?pwd=1234
提取码:1234

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值