前言
在App中常见的解锁动画有很多种,侧滑解锁也是较为常见的一种解锁交互行为,例如我们常见的侧滑验证登陆,一个比较长的横条,里面嵌套着一个滑块,手指从左至右拖动完成验证。于是决定自己造一个,先来看下最终效果:
实现
思路
从动画的组成来看,可以分为几部分,分别是背景色条、滑块样式、文本样式、滑块滑出之后左侧的背景样式、背景和滑块背景自然不用说,直接通过绘制矩形即可,然后右边的文字扫光效果可以通过属性动画结合渐变器去实现,滑块上的箭头透明度可以通过属性动画实现,然后通过判断触摸事件的区域来控制滑块滑动时的逻辑。
1.绘制背景矩形
2.绘制滑块及滑块上的箭头
3.绘制提示文案
4.在手指触摸事件中判断是否点击了滑块,并处理滑动逻辑
5.箭头动画及文字扫光
1.绘制背景矩形
背景条可以直接取整个View的背景宽高作为自己的宽高,并且设置圆角参数,通过drawRoundRect
绘制圆角矩形条:
class ProfileSlideView : View {
private lateinit var mBgPaint: Paint
private lateinit var mBgRectF: RectF
/**
* 侧滑条圆角度数
*/
private var mCorner: Float = 0f
//...此处省略构造方法及初始化代码
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val viewWidth = right - left
val viewHeight = bottom - top
mBgRectF.left = 0f
mBgRectF.top = 0f
mBgRectF.right = viewWidth * 1.0f
mBgRectF.bottom = viewHeight * 1.0f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//绘制背景
drawBackground(canvas)
}
private fun drawBackground(canvas: Canvas) {
canvas.drawRoundRect(mBgRectF, mCorner, mCorner, mBgPaint)
}
}
效果如下:
2.绘制滑块及滑块上的箭头
滑块的其实本质也是一个圆角矩形,且度数与背景条是一致的,只是做了个颜色上的区分:
private fun drawSlider(canvas: Canvas) {
mSliderPaint.style = Paint.Style.FILL
mSliderPaint.color = mSliderColor
canvas.drawRoundRect(mSliderRectF, mCorner, mCorner, mSliderPaint)
}
这里有个细节,需要给滑块与背景条之间,有一个Padding值,使得滑块有一种内嵌在里面的感觉,可以直接在设置滑块的RectF的时候计算好这个间距:
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val viewWidth = right - left
val viewHeight = bottom - top
mBgRectF.left = 0f
mBgRectF.top = 0f
mBgRectF.right = viewWidth * 1.0f
mBgRectF.bottom = viewHeight * 1.0f
//mPadding为滑块与背景条之间的边距
mSliderRectF.left = mPadding
mSliderRectF.top = mPadding
mSliderRectF.right = mSliderWidth
mSliderRectF.bottom = viewHeight * 1.0f - mPadding
}
这里用来绘制滑块的RectF
也是等会儿做滑动逻辑的关键之处。现在效果如下:
接下来绘制滑块上的三个箭头,一个箭头由3点2线组成,且是有一定规律的, 比如中间的这个箭头,箭头中心点的y坐标肯定是滑块高度的一半,箭头上顶点定为滑块高度的1/3,下顶点定为滑块高度的2/3,也就是最终整个箭头的高度占据滑块高度的1/3,这是目前调整的一个比较合适的比例。然后它们的横坐标也是根据滑块的宽度以及箭头的宽度进行计算,如下图:
由于三个箭头是水平并排的,所以其他两个箭头纵坐标也跟这个箭头是一致的,只是横坐标有一定的偏移量,最终代码如下:
mSliderPaint.style = Paint.Style.STROKE
mSliderPaint.strokeWidth = 2f
//箭头宽度为滑块的1/16
val arrowWidth = mSliderWidth / 16f
//遍历下标1到3,依次绘制3个箭头
for (index in 1..3) {
mSlideArrowPath.reset()
//这里使得3个箭头的横坐标依次在滑块的1/3,1/2,2/3处
val arrowPos = (index + 1) / 6f
mSlideArrowPath.moveTo(mSliderRectF.left + mSliderWidth * arrowPos - arrowWidth / 2, mSliderRectF.bottom / 3f)
mSlideArrowPath.lineTo(mSliderRectF.left + mSliderWidth * arrowPos + arrowWidth, mSliderRectF.bottom / 2f)
mSlideArrowPath.lineTo(mSliderRectF.left + mSliderWidth * arrowPos - arrowWidth / 2, mSliderRectF.bottom * 2 / 3f)
canvas.drawPath(mSlideArrowPath, mSliderPaint)
}
最终绘制出来的效果如下:
3.绘制提示文案
刚才已经绘制好了背景和滑块,接下来绘制文字,由于滑块已经占据了一部分位置,如果让文案居中的话,会被遮挡住,所以将文案的位置定在除滑块之外的剩余区域的中间。绘制文字就直接采用 Canvas 的 drawText() 就可以了:
private fun drawTipText(canvas: Canvas) {
val textWidth = mTextPaint.measureText("滑动通过验证")
val fontMetrics = mTextPaint.fontMetrics
val baseLine: Float = mBgRectF.height() * 0.5f - (fontMetrics.ascent + fontMetrics.descent) / 2
canvas.drawText(mTipText, ((mBgRectF.width() - mSliderWidth - textWidth) / 2f) + mSliderWidth, baseLine, mTextPaint)
}
注意由于要让文本真正意义上的居中,所以需要根据文本的长度和文字的基准线进行计算,(fontMetrics.ascent + fontMetrics.descent) / 2
可以得到文字在竖直方向的中点。
效果如下:
4.在手指触摸事件中判断是否点击了滑块,并处理滑动逻辑
上面已经完成了基本的绘制部分,完成了架子,还需要注入灵魂——滑动解锁,涉及到滑动相关,肯定是需要用到View的触摸事件,我们可以重写onTouchEvent方法,在触发MotionEvent.ACTION_DOWN
事件的时候,判断触摸的点的坐标是否落在滑块里面(这个时候就用到了前文提到的滑块的RectF了):
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (!checkTouchSlide(x, y)) {
return false
}
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP -> {
}
}
return true
}
private fun checkTouchSlide(x: Float, y: Float): Boolean {
return mSliderRectF.contains(x, y)
}
如果不落在滑块的RectF里面的话,ACTION_DOWN
事件就返回false,不消费此次触摸事件。
接着是按住滑块时的处理,假如ACTION_DOWN
事件满足条件了,则需要开始处理ACTION_MOVE
事件,滑块的移动应该是与手指左右拖动的距离同步,那我们就用一个变量来记录上一次事件触摸点的横坐标,然后与当前触摸的横坐标做差值,即可得到偏移量,然后再用这个偏移量去改变滑块的RectF的left和right,就能实现拖动的效果了:
MotionEvent.ACTION_MOVE -> {
if (mLastTouchX > 0f) {
when {
//判断是否滑到了整个View的最右边
mSliderRectF.left + x - mLastTouchX + mSliderWidth > mBgRectF.width() - mPadding -> {
mSliderRectF.left = mBgRectF.width() - mPadding - mSliderWidth
mSliderRectF.right = mSliderRectF.left + mSliderWidth
}
////判断是否滑到了整个View的最左边
mSliderRectF.left + x - mLastTouchX < mPadding -> {
mSliderRectF.left = mPadding
mSliderRectF.right = mSliderRectF.left + mSliderWidth
}
else -> {
mSliderRectF.left += x - mLastTouchX
mSliderRectF.right = mSliderRectF.left + mSliderWidth
}
}
invalidate()
}
mLastTouchX = x
}
这里有个要注意的点,滑动的时候需要处理临界值,如果滑块已经滑到了最左边或最右边,要限制不能让它超过边界,另外记得每个ACTION_MOVE
事件处理完之后都需要调用invalidate()
通知View刷新绘制。
此时的效果如下:
虽然已经可以拖动了,但发现有什么不对劲的地方,滑到一半,松开手,滑块就停在了那个位置,这不符合我们想要的交互,正常的交互应该是松开手指的时候,如果滑块已经滑过横条的一半距离,就自动滑到右端,反之自动滑到左端,自动滑动的话就需要结合属性动画来实现了:
val mAutoSlideAnimator = ValueAnimator()
mAutoSlideAnimator.duration = 500L
mAutoSlideAnimator.addUpdateListener {
mSliderRectF.left = it.animatedValue as Float
mSliderRectF.right = mSliderRectF.left + mSliderWidth
mProgressRectF.right = mSliderRectF.right
invalidate()
}
根据动画的进度值,实时更新滑块RectF的left和right,然后接着便是在MotionEvent.ACTION_UP
事件中进行判断并启动动画:
MotionEvent.ACTION_UP -> {
if (mSliderRectF.centerX() > mBgRectF.centerX()) {
mAutoSlideAnimator.setFloatValues(mSliderRectF.left, mBgRectF.right - mPadding - mSliderWidth)
mAutoSlideAnimator.start()
} else {
mAutoSlideAnimator.setFloatValues(mSliderRectF.left, mPadding)
mAutoSlideAnimator.start()
}
}
将mSliderRectF
的enterX
与mBgRectF
的centerX
进行比较,并以滑块当前的位置作为动画的起始点,View的左右边缘作为动画的终点,启动动画:
5.箭头动画及文字扫光
上面已经实现了大体的功能,但还不足矣,我们可以再给它增添一些效果,滑块的三个箭头可以做一个透明度的逐个变化,文字可以加一个扫光的效果。
箭头动画
箭头的透明度可以用一个动画器配合十六进制色值的前两位透明度进行调整:
val mArrowAlphaAnimator = ValueAnimator()
mArrowAlphaAnimator.setFloatValues(1F, 3F)
mArrowAlphaAnimator.repeatCount = -1
mArrowAlphaAnimator.duration = 800
mArrowAlphaAnimator.addUpdateListener {
mAlphaProgress = it.animatedValue as Float
invalidate()
}
//遍历下标1到3,依次绘制3个箭头
for (index in 1..3) {
val alphaValue = if (index + mAlphaProgress >= 4) {
index + mAlphaProgress - 3
} else {
(index + mAlphaProgress).coerceAtMost(3F)
}
mSlideArrowPath.reset()
val colorBuilder = StringBuilder()
colorBuilder.clear()
colorBuilder.append("#")
colorBuilder.append(Integer.toHexString((85 * alphaValue).toInt()))
colorBuilder.append("FFFFFF")
mSliderPaint.color = Color.parseColor(colorBuilder.toString())
//这里使得3个箭头的横坐标依次在滑块的1/3,1/2,2/3处
val arrowPos = (index + 1) / 6f
mSlideArrowPath.moveTo(mSliderRectF.left + mSliderWidth * arrowPos - arrowWidth / 2, mSliderRectF.bottom / 3f)
mSlideArrowPath.lineTo(mSliderRectF.left + mSliderWidth * arrowPos + arrowWidth, mSliderRectF.bottom / 2f)
mSlideArrowPath.lineTo(mSliderRectF.left + mSliderWidth * arrowPos - arrowWidth / 2, mSliderRectF.bottom * 2 / 3f)
canvas.drawPath(mSlideArrowPath, mSliderPaint)
}
mAlphaProgress
从1到3一直循环变化,然后由于颜色透明度的范围是0~255,所以分为三等分即是85一份,通过Integer.toHexString
即可快速转换为十六进制,拼接上#号和后面的色值,即可得到最终的一个颜色,将其赋给绘制箭头的画笔即可。当然由于是onDraw
,所以最好用StringBuilder
去拼接。
文字扫光效果
我们可以先为文字的画笔设置一个线性渐变的Shader
,颜色设置为灰-白-灰的过渡:
val mLinearGradient = LinearGradient(0F, 0F, viewWidth * 1.0F, 0F, intArrayOf(Color.GRAY, Color.WHITE, Color.GRAY), null, Shader.TileMode.CLAMP)
mTextPaint.shader = mLinearGradient
加好了“光”,还需要让“光”左右动起来,可以用一个Matrix
矩阵,通过对矩阵进行平移,然后设置给mLinearGradient
:
val mTextAnimator = ValueAnimator()
mTextAnimator.setFloatValues(0F, 1F)
mTextAnimator.repeatCount = -1
mTextAnimator.duration = 2000
mTextAnimator.addUpdateListener {
mGradientProgress = it.animatedValue as Float
}
//...
//绘制文字时
mGradientMatrix.setTranslate(mGradientProgress * mBgRectF.width(), 0F)
mLinearGradient.setLocalMatrix(mGradientMatrix)
最终效果:
结语
总体效果大概如此,当然由于场景的不同,会有一些调整的地方,比如滑块自动归位的一个判断点,不一定是在View的中间,另外滑动成功,甚至还可以在滑块上加一个类似打勾的效果,让整个动画交互体验更棒,后面会再进一步优化,由于篇幅有限,一些非关键细节就不再详细阐述,完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
欢迎关注 Android小Y 的,更多Android精选自定义View
『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』自定义带入场动画的弧形百分比进度条
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
简 书:Android小Y
在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~