前言
在App中常見(jiàn)的解鎖動(dòng)畫(huà)有很多種,側(cè)滑解鎖也是較為常見(jiàn)的一種解鎖交互行為,例如我們常見(jiàn)的側(cè)滑驗(yàn)證登陸猩系,一個(gè)比較長(zhǎng)的橫條,里面嵌套著一個(gè)滑塊中燥,手指從左至右拖動(dòng)完成驗(yàn)證寇甸。于是決定自己造一個(gè),先來(lái)看下最終效果:
?
實(shí)現(xiàn)
思路
從動(dòng)畫(huà)的組成來(lái)看疗涉,可以分為幾部分拿霉,分別是背景色條、滑塊樣式咱扣、文本樣式绽淘、滑塊滑出之后左側(cè)的背景樣式、背景和滑塊背景自然不用說(shuō)闹伪,直接通過(guò)繪制矩形即可收恢,然后右邊的文字掃光效果可以通過(guò)屬性動(dòng)畫(huà)結(jié)合漸變器去實(shí)現(xiàn),滑塊上的箭頭透明度可以通過(guò)屬性動(dòng)畫(huà)實(shí)現(xiàn)祭往,然后通過(guò)判斷觸摸事件的區(qū)域來(lái)控制滑塊滑動(dòng)時(shí)的邏輯伦意。
1.繪制背景矩形
2.繪制滑塊及滑塊上的箭頭
3.繪制提示文案
4.在手指觸摸事件中判斷是否點(diǎn)擊了滑塊,并處理滑動(dòng)邏輯
5.箭頭動(dòng)畫(huà)及文字掃光
?
1.繪制背景矩形
背景條可以直接取整個(gè)View的背景寬高作為自己的寬高硼补,并且設(shè)置圓角參數(shù)驮肉,通過(guò)drawRoundRect
繪制圓角矩形條:
class ProfileSlideView : View {
private lateinit var mBgPaint: Paint
private lateinit var mBgRectF: RectF
/**
* 側(cè)滑條圓角度數(shù)
*/
private var mCorner: Float = 0f
//...此處省略構(gòu)造方法及初始化代碼
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.繪制滑塊及滑塊上的箭頭
滑塊的其實(shí)本質(zhì)也是一個(gè)圓角矩形,且度數(shù)與背景條是一致的已骇,只是做了個(gè)顏色上的區(qū)分:
private fun drawSlider(canvas: Canvas) {
mSliderPaint.style = Paint.Style.FILL
mSliderPaint.color = mSliderColor
canvas.drawRoundRect(mSliderRectF, mCorner, mCorner, mSliderPaint)
}
這里有個(gè)細(xì)節(jié)离钝,需要給滑塊與背景條之間,有一個(gè)Padding值褪储,使得滑塊有一種內(nèi)嵌在里面的感覺(jué)卵渴,可以直接在設(shè)置滑塊的RectF的時(shí)候計(jì)算好這個(gè)間距:
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
}
這里用來(lái)繪制滑塊的RectF
也是等會(huì)兒做滑動(dòng)邏輯的關(guān)鍵之處。現(xiàn)在效果如下:
接下來(lái)繪制滑塊上的三個(gè)箭頭鲤竹,一個(gè)箭頭由3點(diǎn)2線組成浪读,且是有一定規(guī)律的,比如中間的這個(gè)箭頭辛藻,箭頭中心點(diǎn)的y坐標(biāo)肯定是滑塊高度的一半碘橘,箭頭上頂點(diǎn)定為滑塊高度的1/3,下頂點(diǎn)定為滑塊高度的2/3吱肌,也就是最終整個(gè)箭頭的高度占據(jù)滑塊高度的1/3痘拆,這是目前調(diào)整的一個(gè)比較合適的比例。然后它們的橫坐標(biāo)也是根據(jù)滑塊的寬度以及箭頭的寬度進(jìn)行計(jì)算氮墨,如下圖:
由于三個(gè)箭頭是水平并排的纺蛆,所以其他兩個(gè)箭頭縱坐標(biāo)也跟這個(gè)箭頭是一致的吐葵,只是橫坐標(biāo)有一定的偏移量,最終代碼如下:
mSliderPaint.style = Paint.Style.STROKE
mSliderPaint.strokeWidth = 2f
//箭頭寬度為滑塊的1/16
val arrowWidth = mSliderWidth / 16f
//遍歷下標(biāo)1到3桥氏,依次繪制3個(gè)箭頭
for (index in 1..3) {
mSlideArrowPath.reset()
//這里使得3個(gè)箭頭的橫坐標(biāo)依次在滑塊的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)
}
?
最終繪制出來(lái)的效果如下:
?
3.繪制提示文案
剛才已經(jīng)繪制好了背景和滑塊折联,接下來(lái)繪制文字,由于滑塊已經(jīng)占據(jù)了一部分位置识颊,如果讓文案居中的話(huà)诚镰,會(huì)被遮擋住,所以將文案的位置定在除滑塊之外的剩余區(qū)域的中間祥款。繪制文字就直接采用 Canvas 的 drawText() 就可以了:
private fun drawTipText(canvas: Canvas) {
val textWidth = mTextPaint.measureText("滑動(dòng)通過(guò)驗(yàn)證")
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)
}
注意由于要讓文本真正意義上的居中清笨,所以需要根據(jù)文本的長(zhǎng)度和文字的基準(zhǔn)線進(jìn)行計(jì)算,(fontMetrics.ascent + fontMetrics.descent) / 2
可以得到文字在豎直方向的中點(diǎn)刃跛。
效果如下:
?
4.在手指觸摸事件中判斷是否點(diǎn)擊了滑塊抠艾,并處理滑動(dòng)邏輯
上面已經(jīng)完成了基本的繪制部分,完成了架子桨昙,還需要注入靈魂——滑動(dòng)解鎖检号,涉及到滑動(dòng)相關(guān),肯定是需要用到View的觸摸事件蛙酪,我們可以重寫(xiě)onTouchEvent方法齐苛,在觸發(fā)MotionEvent.ACTION_DOWN
事件的時(shí)候,判斷觸摸的點(diǎn)的坐標(biāo)是否落在滑塊里面(這個(gè)時(shí)候就用到了前文提到的滑塊的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里面的話(huà)桂塞,ACTION_DOWN
事件就返回false凹蜂,不消費(fèi)此次觸摸事件。
接著是按住滑塊時(shí)的處理阁危,假如ACTION_DOWN
事件滿(mǎn)足條件了玛痊,則需要開(kāi)始處理ACTION_MOVE
事件,滑塊的移動(dòng)應(yīng)該是與手指左右拖動(dòng)的距離同步狂打,那我們就用一個(gè)變量來(lái)記錄上一次事件觸摸點(diǎn)的橫坐標(biāo)擂煞,然后與當(dāng)前觸摸的橫坐標(biāo)做差值,即可得到偏移量趴乡,然后再用這個(gè)偏移量去改變滑塊的RectF的left和right对省,就能實(shí)現(xiàn)拖動(dòng)的效果了:
MotionEvent.ACTION_MOVE -> {
if (mLastTouchX > 0f) {
when {
//判斷是否滑到了整個(gè)View的最右邊
mSliderRectF.left + x - mLastTouchX + mSliderWidth > mBgRectF.width() - mPadding -> {
mSliderRectF.left = mBgRectF.width() - mPadding - mSliderWidth
mSliderRectF.right = mSliderRectF.left + mSliderWidth
}
////判斷是否滑到了整個(gè)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
}
這里有個(gè)要注意的點(diǎn),滑動(dòng)的時(shí)候需要處理臨界值浙宜,如果滑塊已經(jīng)滑到了最左邊或最右邊官辽,要限制不能讓它超過(guò)邊界,另外記得每個(gè)ACTION_MOVE
事件處理完之后都需要調(diào)用invalidate()
通知View刷新繪制粟瞬。
此時(shí)的效果如下:
雖然已經(jīng)可以拖動(dòng)了,但發(fā)現(xiàn)有什么不對(duì)勁的地方萤捆,滑到一半裙品,松開(kāi)手俗批,滑塊就停在了那個(gè)位置,這不符合我們想要的交互市怎,正常的交互應(yīng)該是松開(kāi)手指的時(shí)候岁忘,如果滑塊已經(jīng)滑過(guò)橫條的一半距離,就自動(dòng)滑到右端区匠,反之自動(dòng)滑到左端干像,自動(dòng)滑動(dòng)的話(huà)就需要結(jié)合屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)了:
val mAutoSlideAnimator = ValueAnimator()
mAutoSlideAnimator.duration = 500L
mAutoSlideAnimator.addUpdateListener {
mSliderRectF.left = it.animatedValue as Float
mSliderRectF.right = mSliderRectF.left + mSliderWidth
mProgressRectF.right = mSliderRectF.right
invalidate()
}
根據(jù)動(dòng)畫(huà)的進(jìn)度值,實(shí)時(shí)更新滑塊RectF的left和right驰弄,然后接著便是在MotionEvent.ACTION_UP
事件中進(jìn)行判斷并啟動(dòng)動(dòng)畫(huà):
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
進(jìn)行比較麻汰,并以滑塊當(dāng)前的位置作為動(dòng)畫(huà)的起始點(diǎn),View的左右邊緣作為動(dòng)畫(huà)的終點(diǎn)戚篙,啟動(dòng)動(dòng)畫(huà):
?
5.箭頭動(dòng)畫(huà)及文字掃光
上面已經(jīng)實(shí)現(xiàn)了大體的功能五鲫,但還不足矣,我們可以再給它增添一些效果岔擂,滑塊的三個(gè)箭頭可以做一個(gè)透明度的逐個(gè)變化位喂,文字可以加一個(gè)掃光的效果。
箭頭動(dòng)畫(huà)
箭頭的透明度可以用一個(gè)動(dòng)畫(huà)器配合十六進(jìn)制色值的前兩位透明度進(jìn)行調(diào)整:
val mArrowAlphaAnimator = ValueAnimator()
mArrowAlphaAnimator.setFloatValues(1F, 3F)
mArrowAlphaAnimator.repeatCount = -1
mArrowAlphaAnimator.duration = 800
mArrowAlphaAnimator.addUpdateListener {
mAlphaProgress = it.animatedValue as Float
invalidate()
}
//遍歷下標(biāo)1到3乱灵,依次繪制3個(gè)箭頭
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個(gè)箭頭的橫坐標(biāo)依次在滑塊的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一直循環(huán)變化塑崖,然后由于顏色透明度的范圍是0~255,所以分為三等分即是85一份痛倚,通過(guò)Integer.toHexString
即可快速轉(zhuǎn)換為十六進(jìn)制弃舒,拼接上#號(hào)和后面的色值,即可得到最終的一個(gè)顏色状原,將其賦給繪制箭頭的畫(huà)筆即可聋呢。當(dāng)然由于是onDraw
,所以最好用StringBuilder
去拼接颠区。
文字掃光效果
我們可以先為文字的畫(huà)筆設(shè)置一個(gè)線性漸變的Shader
削锰,顏色設(shè)置為灰-白-灰的過(guò)渡:
val mLinearGradient = LinearGradient(0F, 0F, viewWidth * 1.0F, 0F, intArrayOf(Color.GRAY, Color.WHITE, Color.GRAY), null, Shader.TileMode.CLAMP)
mTextPaint.shader = mLinearGradient
加好了“光”,還需要讓“光”左右動(dòng)起來(lái)毕莱,可以用一個(gè)Matrix
矩陣器贩,通過(guò)對(duì)矩陣進(jìn)行平移,然后設(shè)置給mLinearGradient
:
val mTextAnimator = ValueAnimator()
mTextAnimator.setFloatValues(0F, 1F)
mTextAnimator.repeatCount = -1
mTextAnimator.duration = 2000
mTextAnimator.addUpdateListener {
mGradientProgress = it.animatedValue as Float
}
//...
//繪制文字時(shí)
mGradientMatrix.setTranslate(mGradientProgress * mBgRectF.width(), 0F)
mLinearGradient.setLocalMatrix(mGradientMatrix)
最終效果:
?
結(jié)語(yǔ)
總體效果大概如此朋截,當(dāng)然由于場(chǎng)景的不同蛹稍,會(huì)有一些調(diào)整的地方,比如滑塊自動(dòng)歸位的一個(gè)判斷點(diǎn)部服,不一定是在View的中間唆姐,另外滑動(dòng)成功,甚至還可以在滑塊上加一個(gè)類(lèi)似打勾的效果廓八,讓整個(gè)動(dòng)畫(huà)交互體驗(yàn)更棒奉芦,后面會(huì)再進(jìn)一步優(yōu)化赵抢,由于篇幅有限,一些非關(guān)鍵細(xì)節(jié)就不再詳細(xì)闡述声功,完整代碼已上傳到 一個(gè)集合酷炫效果的自定義組件庫(kù)烦却,歡迎Issue。
?
歡迎關(guān)注 Android小Y 的簡(jiǎn)書(shū)先巴,更多Android精選自定義View
『Android自定義View實(shí)戰(zhàn)』實(shí)現(xiàn)一個(gè)小清新的彈出式圓環(huán)菜單
『Android自定義View實(shí)戰(zhàn)』玩轉(zhuǎn)PathMeasure之自定義支付結(jié)果動(dòng)畫(huà)
『Android自定義View實(shí)戰(zhàn)』自定義弧形旋轉(zhuǎn)菜單欄——衛(wèi)星菜單
『Android自定義View實(shí)戰(zhàn)』自定義帶入場(chǎng)動(dòng)畫(huà)的弧形百分比進(jìn)度條
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
簡(jiǎn) 書(shū):Android小Y
在 GitHub 上建了一個(gè)集合炫酷自定義View的項(xiàng)目其爵,里面有很多實(shí)用的自定義View源碼及demo,會(huì)長(zhǎng)期維護(hù)伸蚯,歡迎Star~ 如有不足之處或建議還望指正摩渺,相互學(xué)習(xí),相互進(jìn)步朝卒,如果覺(jué)得不錯(cuò)動(dòng)動(dòng)小手點(diǎn)個(gè)喜歡证逻, 謝謝~