Android自定義View - 元素按鈕的特效實現
Android自定義View之元素按鈕
之前在dribbble看到的三個元素的按鈕,參考了設計的創意,添加了自己定義的動畫效果來實現.先看效果

效果圖
分別是水火電三個元素的按鈕實現.其中電的實現最簡單,水的次之,火的實際還並不滿意,沒有火焰擾動的感覺,嘗試過幾次但是效果都不理想,最後只保留了自下向上的扇形遮罩.如果有好的效果再優化實現.
閃電篇
設計過程
通過閃電的位置將整體劃分成七個部分(七個部分的主要原因是最初設計了一箇中部放大的透鏡效果,但是沒能實現),從右上角進入,在中心點附近運動,直到停在中心點.閃電本身可以看做一箇中心對稱的圖形,整理就簡化成了現將canvas旋轉一定角度,然後繪製中心對稱的閃電形狀,最後在x軸上運動就可以了.

黃線是旋轉後的座標
黃線是旋轉後的座標,可以看出簡化後實現起來很簡單.
程式碼實現
- 背景部分
//繪製閃電背景 private fundrawBaseButton(canvas: Canvas , index: Float) { //設定畫筆 val paint = Paint() //新增閃電移動到指定位置時的背景顏色設定 if ((index <= 0.45F && index >= 0.35F) || (index >= 0.65F && index <= 0.75F)) { paint.color = Color.parseColor("#ACADAC") }else{ paint.color = Color.parseColor("#595A59") } paint.style = Paint.Style.FILL //繪製閃電背景 canvas.drawArc(RectF(-baseR, -baseR, baseR, baseR), 0F , 360F,true , paint) }
- 閃電部分
private fundrawLighting(canvas: Canvas , index: Float) { val baseR = baseR * coefficient var index = index var changeR = 0F //將整個閃電的運動拆成七個部分 if (index <= 0.25){ changeR= this.baseR + baseR changeR = (changeR * (1 - index / 0.25)).toFloat() }else if (index <= 0.4){ index = index - 0.25F changeR= this.baseR changeR = -(changeR * (index / (0.4F - 0.25F))) }else if (index <= 0.6F){ index = index - 0.4F changeR = this.baseR changeR = -changeR *(1 - index / 0.2F) }else if (index <= 0.7F){ index = index - 0.6F changeR = baseR changeR = changeR * index / 0.1F }else if (index <= 0.8F){ index = index - 0.7F changeR = baseR changeR = baseR - changeR * index / 0.1F }else if (index <= 0.9F){ index = index - 0.8F changeR = baseR changeR = -changeR * index / 0.1F }else if (index <= 1F){ index = index - 0.9F changeR = baseR changeR = -changeR + changeR * (index / 0.1F) } //設定畫筆 val path = Path() val paint = Paint() paint.strokeWidth = 5F paint.style = Paint.Style.FILL paint.color = viewBackgroundColor val points :MutableList<Point> = ArrayList() //設定繪製閃電的路徑點 points.add(pointFactory(60 , baseR)) points.add(pointFactory(-45 , baseR / 2F)) points.add(pointFactory(-45 - 90 , baseR / 5F)) points.add(pointFactory(-30 - 90 , baseR)) points.add(pointFactory(45 + 90 , baseR / 2F)) points.add(pointFactory(45 , baseR / 5F)) points.add(pointFactory(60 , baseR)) //設定閃電的偏移量(模擬運動情況) //原本還想實現一箇中心放大的透鏡效果,但是效果很僵硬,只能移除了 for (i in 0..points.size - 1){ points.set(i , Point(points[i].x + changeR , points[i].y)) } path.moveTo(points[0].x , points[0].y) for (index in 1..points.size - 1){ path.lineTo(points[index].x , points[index].y) } canvas.drawPath(path , paint) //閃電繪製輔助座標系 //val paint2 = Paint() //paint2.strokeWidth = 5F //paint2.color = Color.YELLOW //canvas.drawLine(1000F , 0F ,-1000F , 0F , paint2) //canvas.drawLine( 0F ,-1000F , 0F , 1000F , paint2) }
閃電的實現還是很簡單的,因為不涉及到圖形的變化,只有一個簡單的位移效果
霜(水)之哀傷篇
設計思路
水滴的實現相對對於閃電來說麻煩一些,一是水滴本身不是很好繪製,又因為水滴在下落的過程中存在變化,最後選擇通過貝塞爾曲線實現.二是水滴和背景之間的互動,在水滴未完全下落到背景中的時候,水滴背景的上部有個向下凹陷的過程,這個不是閃電背景的簡單變化可能做到的.最後也是使用貝塞爾曲線繪製的一個圓弧的區域遮蓋來實現.
整理需要變化的元素是水滴及頂部的遮蓋.都是使用貝塞爾曲線實現的.頂部的凹陷隨著水滴的下落不斷凹陷,直至水滴脫離頂部後再漸漸回落.主要是找到水滴完全脫離的時間當做頂部凹陷的關鍵點就好.水滴下落的過程中是需要變化,最開始可能稍微瘦長一些,然後相對變扁.
程式碼實現
- 水滴背景的實現
//繪製水滴背景 private fundrawBaseButton(canvas: Canvas , index: Float) { //計算水滴半進入區間(確定水滴背景上部變化範圍) val waterRand = (baseR * 1.25 * coefficient) / ((baseR * 1.25 * coefficient) + baseR) //設定畫筆 val paint = Paint() paint.color = Color.parseColor("#45AAE1") paint.style = Paint.Style.FILL //繪製水滴背景下半部分的(此部分不需要變換) canvas.drawArc(RectF(-baseR, -baseR, baseR, baseR), 180F , 180F,true , paint) //設定點list 順序儲存相關路徑及關鍵點 val points : MutableList<Point> = ArrayList() points.add(Point(-baseR , 0F)) points.add(Point(-baseR , baseR * C)) points.add(Point(-baseR * C , baseR )) var baseButtonTop : Float //根據index判斷上部的形態 if (index <= waterRand){ baseButtonTop = baseR - (baseR * coefficient * index) * 2 }else{ baseButtonTop = baseR - (baseR * coefficient) * 2 + (baseR * coefficient * index) * 2 if (baseButtonTop > baseR){ baseButtonTop = baseR } } points.add(Point(0F, baseButtonTop)) points.add(Point(baseR * C , baseR)) points.add(Point(baseR , baseR * C )) points.add(Point(baseR , 0F)) val path = Path() //畫筆移動到指定位置(不移動的話通過貝塞爾繪製的圖形會有誤差) path.moveTo(points[0].x , points[0].y) //設定貝塞爾曲線 path.cubicTo( points[1].x , points[1].y , points[2].x , points[2].y , points[3].x , points[3].y) path.cubicTo( points[4].x , points[4].y , points[5].x , points[5].y , points[6].x , points[6].y) //繪製 canvas.drawPath(path, paint) }
- 水滴的實現
private fundrawDrops(canvas: Canvas , index: Float) { //設定水滴半徑 val baseR = baseR * coefficient val index = 1 - index //根據index將畫布中心移動到對應位置 canvas.translate( 0F , (this.baseR * 1.125F + baseR)* index - this.baseR / 8) //設定畫筆 val paint = Paint() paint.style = Paint.Style.FILL paint.color = viewBackgroundColor //儲存關鍵點座標 val points : MutableList<Point> = ArrayList() points.add(Point(-baseR , 0F)) //水滴頂部變換系數 val topCoefficient = 1.5F points.add(Point(-baseR , baseR * C)) points.add(Point(-baseR * C , baseR )) points.add(Point(0F, (1.5 * baseR + baseR * topCoefficient * index).toFloat())) points.add(Point(baseR * C , baseR)) points.add(Point(baseR , baseR * C )) points.add(Point(baseR , 0F)) //水滴底部變換系數 //這兩個變換系數使得水滴在下落的過程中漸漸變扁 val bottomCoefficient = 0.3F val tempBaseR = (baseR - baseR * bottomCoefficient * index) points.add(Point(baseR , -tempBaseR * C)) points.add(Point(baseR * C , -tempBaseR )) points.add(Point(0F, -tempBaseR)) points.add(Point(-baseR * C , -tempBaseR)) points.add(Point(-baseR , -tempBaseR * C )) points.add(Point(-baseR , 0F)) //設定四個部分(90°一個部分)的貝塞爾曲線 //關於貝塞爾曲線的事情...感覺可以再做點記錄 val path = Path() path.moveTo(points[0].x , points[0].y) path.cubicTo( points[1].x , points[1].y , points[2].x , points[2].y , points[3].x , points[3].y) path.cubicTo( points[4].x , points[4].y , points[5].x , points[5].y , points[6].x , points[6].y) path.cubicTo( points[7].x , points[7].y , points[8].x , points[8].y , points[9].x , points[9].y) path.cubicTo( points[10].x , points[10].y , points[11].x , points[11].y , points[12].x , points[12].y) //繪製圖形 canvas.drawPath(path, paint) }
偷懶的原因所以直接使用背景色做的一個簡單的遮蓋,沒有使用遮罩(其實閃電的部分也是).
相對來說水滴的實現最為滿意,主要的預期效果都成功的實現出來了,整體看來效果還是可以的
火之高興篇
設計思路
雖然整體看來,應該是一個難度中等的動畫,但是在設計的過程中經歷了空手用貝塞爾畫火焰(最開始的想法本是火焰本身也是會動的),火焰擾動效果的實現(這個最為艱難,主要是不知道怎麼控制火焰擾動的效果,其次是遮罩層的使用,具體的坑會另開文字來講解),最後只能簡單做了個底部向上的遮罩層來當做火焰的擾動情況
所以其實就是繪製一個火焰的形狀,然後再用個遮罩層來遮蓋實現火焰的擾動
程式碼實現
因為背景沒有什麼特效,就不貼背景的程式碼了
- 整體火焰效果控制
因為火焰需要展示繪製完成的火焰和遮罩層中相交的部分,要使用PorterDuffXfermode相關的方法,所以在繪製中將原圖層和遮罩層分開設計
private fundrawFires(canvas: Canvas , index: Float) { //設定火焰半徑 //設定原圖層(火焰繪製) val srcB = makeSrc(2 * baseR.toInt(), 2 * baseR.toInt(), index) //設定遮罩層 val dstB = makeDst(2 * baseR.toInt(), 2 * baseR.toInt(), index) val paint = Paint() canvas.saveLayer(-baseR, -baseR, baseR , baseR, null, Canvas.ALL_SAVE_FLAG) //繪製遮罩層 canvas.drawBitmap(dstB,-baseR/2,-baseR/2, paint) //設定遮罩模式為SRC_IN顯示原圖層中原圖層與遮罩層相交部分 paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(srcB, -baseR/2, -baseR/2, paint) paint.xfermode = null }
- 繪製原圖層(火焰本身的繪製)
fun makeSrc(w: Int, h: Int , index :Float): Bitmap { val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val canvas = Canvas(bm) canvas.translate(baseR / 2F, baseR / 2F)// 將座標系移動到畫布中央 val index = index * 0.5F + 0.5F val baseR = baseR * coefficient * index //設定畫筆 val paint = Paint() paint.style = Paint.Style.FILL paint.color = viewBackgroundColor paint.strokeWidth = 10F //儲存關鍵點座標 val points : MutableList<Point> = ArrayList() //整體火焰是由六個貝塞爾曲線繪製成的 points.add(pointFactory( 190F , baseR)) points.add(pointFactory( 280F , baseR / 3F * 4)) points.add(pointFactory( 320F ,baseR / 6F)) points.add(pointFactory( 350F , baseR)) points.add(pointFactory( 10F , baseR)) points.add(pointFactory( 30F , baseR / 3F* 2)) points.add(pointFactory( 50F , baseR / 3F )) points.add(pointFactory( 60F , baseR / 6F * 3)) points.add(pointFactory( 60F , baseR / 6F * 4)) points.add(pointFactory( 50F , baseR / 6F * 5)) points.add(pointFactory( 85F , baseR / 6F * 5)) points.add(pointFactory( 120F , baseR / 6F * 5)) points.add(pointFactory( 150F , baseR )) points.add(pointFactory( 160F , baseR / 9F * 7)) points.add(pointFactory( 170F , baseR / 9F * 5)) points.add(pointFactory( 180F , baseR / 9F * 3)) points.add(pointFactory( 200F , baseR / 3F)) points.add(pointFactory( 195F , baseR / 3F * 2)) points.add(pointFactory( 190F , baseR )) val path = Path() path.moveTo(points[0].x , points[0].y) for (index in 0..((points.size - 1) / 3 - 1) ){ path.cubicTo( points[3 * index + 1].x , points[3 * index + 1].y , points[3 * index + 2].x , points[3 * index + 2].y , points[3 * index + 3].x , points[3 * index + 3].y) } //繪製圖形 canvas.drawPath(path, paint) return bm }
- 繪製遮罩層(火焰的擾動效果)
fun makeDst(w: Int, h: Int, index :Float): Bitmap { val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) val canvas = Canvas(bm) canvas.translate(baseR / 2F, 0F) val paint = Paint() paint.color = Color.YELLOW val dstLength = baseR * coefficient * index * 2 val rectf = RectF(-dstLength, -dstLength, dstLength, dstLength) //沒找到合適的擾動效果,只能簡單實現一個遮罩效果 canvas.drawArc(rectf , 0F , 360F , true, paint) return bm }
火焰來說,雖然需要的效果程式碼都實現了,但是缺少設計,整體的效果到時不盡如人意.針對效果來說還有很多的優化空間
【附錄】

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。