『Android自定义View实战』自定义炫酷侧滑解锁效果

前言

在App中常见的解锁动画有很多种,侧滑解锁也是较为常见的一种解锁交互行为,例如我们常见的侧滑验证登陆,一个比较长的横条,里面嵌套着一个滑块,手指从左至右拖动完成验证。于是决定自己造一个,先来看下最终效果:


侧滑解锁最终效果.gif

 

实现

思路

从动画的组成来看,可以分为几部分,分别是背景色条、滑块样式、文本样式、滑块滑出之后左侧的背景样式、背景和滑块背景自然不用说,直接通过绘制矩形即可,然后右边的文字扫光效果可以通过属性动画结合渐变器去实现,滑块上的箭头透明度可以通过属性动画实现,然后通过判断触摸事件的区域来控制滑块滑动时的逻辑。

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)
    }
}

效果如下:


背景色条.png

 

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也是等会儿做滑动逻辑的关键之处。现在效果如下:

滑块.png

接下来绘制滑块上的三个箭头,一个箭头由3点2线组成,且是有一定规律的,比如中间的这个箭头,箭头中心点的y坐标肯定是滑块高度的一半,箭头上顶点定为滑块高度的1/3,下顶点定为滑块高度的2/3,也就是最终整个箭头的高度占据滑块高度的1/3,这是目前调整的一个比较合适的比例。然后它们的横坐标也是根据滑块的宽度以及箭头的宽度进行计算,如下图:

滑块箭头坐标示意图.png

由于三个箭头是水平并排的,所以其他两个箭头纵坐标也跟这个箭头是一致的,只是横坐标有一定的偏移量,最终代码如下:

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)
}

 
最终绘制出来的效果如下:


滑块箭头.png

 

3.绘制提示文案

刚才已经绘制好了背景和滑块,接下来绘制文字,由于滑块已经占据了一部分位置,如果让文案居中的话,会被遮挡住,所以将文案的位置定在除滑块之外的剩余区域的中间。绘制文字就直接采用 CanvasdrawText() 就可以了:

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可以得到文字在竖直方向的中点。
效果如下:

滑动提示文案.png

 

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刷新绘制。

此时的效果如下:


滑块拖动.gif

虽然已经可以拖动了,但发现有什么不对劲的地方,滑到一半,松开手,滑块就停在了那个位置,这不符合我们想要的交互,正常的交互应该是松开手指的时候,如果滑块已经滑过横条的一半距离,就自动滑到右端,反之自动滑到左端,自动滑动的话就需要结合属性动画来实现了:

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()
    }
}

mSliderRectFenterXmBgRectFcenterX进行比较,并以滑块当前的位置作为动画的起始点,View的左右边缘作为动画的终点,启动动画:

滑块拖动自动归位.gif

 

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)

最终效果:


箭头动画文字扫光.gif

 

结语

总体效果大概如此,当然由于场景的不同,会有一些调整的地方,比如滑块自动归位的一个判断点,不一定是在View的中间,另外滑动成功,甚至还可以在滑块上加一个类似打勾的效果,让整个动画交互体验更棒,后面会再进一步优化,由于篇幅有限,一些非关键细节就不再详细阐述,完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
 

欢迎关注 Android小Y 的简书,更多Android精选自定义View

『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』自定义带入场动画的弧形百分比进度条

GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
简 书Android小Y
GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335