記得之前有人在文章下問過,華為智慧屏那種焦點框的實現(xiàn)扮宠。對于廠商來說西乖,優(yōu)先考慮最高效的實現(xiàn)方案,肯定是用c++編寫坛增,畢竟Android上層的繪制效率來說获雕,遠不及底層來的高效。
碰巧前段時間收捣,有個朋友他們公司有類似的需求届案,自己也不是特別忙,就抽空用上層實現(xiàn)幫朋友寫了一個通用組件罢艾。
取名Halo楣颠,光環(huán),已經(jīng)遠離游戲好多年了咐蚯,算是致敬下士官長吧童漩。題外話不說了,下面開始正文春锋。
我們先看看華為智慧屏的效果矫膨。當焦點選中海報,按鈕看疙,選項卡的時候,這些組件外圈都有一個光暈效果在環(huán)繞旋轉(zhuǎn)直奋,說實話能庆,在TV廠家的各種定制系統(tǒng)里,這焦點的動效設(shè)計真的是遙遙領(lǐng)先哈哈脚线。
看智慧屏的效果搁胆,會發(fā)現(xiàn)大部分原生可聚焦的組件都會自帶有這種效果,個人推測應該是系統(tǒng)統(tǒng)一客制化了這些基礎(chǔ)控件邮绿。
那我們獨立的應用開發(fā)渠旁,總不可能每個控件都去定制一遍實現(xiàn)吧,就像一些無縫換膚sdk的實現(xiàn)船逮,雖然是通過攔截的方式統(tǒng)一把原生控件替換成自定義對應的控件顾腊,但是內(nèi)部依舊需要維護一系列的自定義控件,去對應適配替換原生控件挖胃。因此杂靶,這里梆惯,首先淘汰定制化Button,TextView吗垮,CardView等等基礎(chǔ)控件的方式垛吗。說實話個人開發(fā)者很少會有精力和時間去將其都重新實現(xiàn)一遍。因此烁登,確定目標方案怯屉,通過wrapper方式包裹子控件實現(xiàn),自然就會考慮到輕量的ViewGroup:FrameLayout饵沧。
我們先來看個實現(xiàn)的效果圖吧锨络,GIF為了壓縮文件大小,降幀加速了捷泞,實際上是很流暢的足删。支持矩形,圓角锁右,圓形三種類型失受,支持光環(huán)顏色設(shè)置,環(huán)繞速度等:
這里的設(shè)計有一個地方其實把我卡殼了半天咏瑟,注意拂到,智慧屏上的效果是光環(huán)和內(nèi)部內(nèi)容區(qū)域是透明的。這樣一來码泞,也就不能簡單的直接往canvas
上繪制了兄旬。
最初我想到了兩種方案:
- 對
canvas
進行save
,然后按path
裁剪后再繪制光暈余寥,恢復canvas
后在繪制內(nèi)容區(qū)域领铐。這樣的確可以實現(xiàn)光環(huán)和內(nèi)容之間的間隔透明化,但是clipPath
有一個大家都知道的致命缺點:鋸齒宋舷!當然绪撵,為了驗證效果,我還是實現(xiàn)了一遍祝蝠,結(jié)果卻有點意想不到音诈。總結(jié)一下:性能比較高效绎狭,在TV上和一些低版本的手機上的確存在明顯鋸齒细溅,尤其是圓形。但是在我的一加8儡嘶,android 11系統(tǒng)下喇聊,clipPath
的圓滑程度竟然比下面的方案2還要完美。這就讓我尷尬了蹦狂,具體原因未知承疲,猜測是系統(tǒng)層面做了優(yōu)化邻耕,有知道的同學麻煩告知下。 - 使用
PorterDuffXfermode
混合模式燕鸽。這十多種模式兄世,說簡單簡單,說復雜也復雜啊研,坑是挺多的御滩,你按照說明和官方給的混合效果圖自己去寫,很大概率不會出現(xiàn)官方效果圖的結(jié)果党远∠鹘猓混合模式自己去看官方demo
吧,這里就簡單說下沟娱,混合模式必須是bitmap
的混合疊加氛驮,并且要注意src
和dst
先后順序。下面會介紹通過混合模式實現(xiàn)間隔透明化的具體實現(xiàn):
實現(xiàn)
- 首先我們聚焦點就是這個光環(huán)的光暈效果济似,它在動效執(zhí)行過程是繞著內(nèi)容移動的矫废,其實仔細想想,本質(zhì)上就是旋轉(zhuǎn)嘛砰蠢。漸變效果蓖扑,并且需要在旋轉(zhuǎn)過程中保持外環(huán)移動所在位置的漸變色相同,首選方案
SweepGradient
台舱,我們先上一段創(chuàng)建光環(huán)的代碼:
private fun createHalo() {
if (width > 0 && height > 0) {
val shaderBound = sqrt((width * width + height * height).toDouble()).toInt()
shaderBitmap = Bitmap.createBitmap(shaderBound, shaderBound, Bitmap.Config.ARGB_8888)
val shaderCanvas = Canvas(shaderBitmap)
val shaderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
// 0.625 0.75 0.875
// +++++++++++++++++
// white 0.5+---------------+0 white
// +++++++++++++++++
// 0.375 0.25 0.125
val shader = SweepGradient(shaderBound / 2f, shaderBound / 2f,
intArrayOf(haloColor, Color.TRANSPARENT, Color.TRANSPARENT, haloColor, Color.TRANSPARENT, Color.TRANSPARENT, haloColor),
floatArrayOf(0f, 0.125f, 0.375f, 0.5f, 0.625f, 0.875f, 1f)
)
this.shader = shader
}
shaderCanvas.drawCircle(shaderBound / 2f, shaderBound / 2f, shaderBound.toFloat(), shaderPaint)
shaderLeft = -(shaderBound - width) / 2f
shaderTop = -(shaderBound - height) / 2f
}
}
我們先分析下下圖律杠,白色是我們的canvas
區(qū)域,我們的光環(huán)shader
是圓形竞惋,在旋轉(zhuǎn)過程中要始終環(huán)繞在內(nèi)容區(qū)域外框柜去,那該shader
的圓形半徑就是canvas
的對角線的一半。上面提到混合模式是作用于bitmap
拆宛,因此我們需要把shader
繪制到一張bitmap
上嗓奢,而這張bitmap
的尺寸就如圖所示:
- 至此,我們完成了第一步胰挑,創(chuàng)建了一個光環(huán)效果蔓罚。說到光環(huán)和內(nèi)容區(qū)域的透明間隔椿肩,用混合模式怎么實現(xiàn)呢瞻颂?有同學了解過
SurfaceView
的原理吧,挖孔
郑象,這個名詞應該聽過贡这。我這里采取的就是這種方式,通過一張挖孔bitmap
與光環(huán)bitmap
進行混合厂榛,達到把實體的光環(huán)圖中間挖出一個透明區(qū)域盖矫,供內(nèi)容繪制丽惭,haloStrokeWidth
是我們光環(huán)的寬度,左右上下各減去光環(huán)寬度辈双,剩余的canvas
區(qū)域就是我們繪制內(nèi)容的區(qū)域了:
private fun createHole() {
if (width > 0 && height > 0) {
val holeWidth = width - haloStrokeWidth * 2
val holeHeight = height - haloStrokeWidth * 2
holeBitmap = Bitmap.createBitmap(holeWidth.toInt(), holeHeight.toInt(), Bitmap.Config.ARGB_8888)
val holeCanvas = Canvas(holeBitmap)
val holePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
style = Paint.Style.FILL
}
when (shapeType) {
SHAPE_RECT -> {
holeCanvas.drawRect(0f, 0f, holeWidth, holeHeight, holePaint)
}
SHAPE_ROUND_RECT -> {
holeCanvas.drawRoundRect(0f, 0f, holeWidth, holeHeight, cornerRadius.toFloat(), cornerRadius.toFloat(), holePaint)
}
SHAPE_CIRCLE -> {
holeCanvas.drawCircle(holeWidth / 2f, holeHeight / 2f, holeWidth / 2f, holePaint)
}
}
}
}
- 至此责掏,我們就創(chuàng)建了
shaderBitmap
和holeBitmap
兩張圖片。開始混合運算湃望,我們先通過混合把外圈的光環(huán)繪制處理好换衬,再將剩余區(qū)域交給原生的繪制流程進行內(nèi)容區(qū)域(ChildView
)的繪制。同時我們構(gòu)建一個基礎(chǔ)的ValueAnimate
進行動畫運算证芭,不斷旋轉(zhuǎn)重繪就能產(chǎn)生光環(huán)環(huán)繞移動的效果啦:
private val holePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)
}
override fun dispatchDraw(canvas: Canvas?) {
if (isFocused && canvas != null) {
canvas.drawBitmap(holeBitmap, haloStrokeWidth, haloStrokeWidth, null)
canvas.let {
canvas.save()
canvas.rotate(degrees, centerX, centerY)
canvas.drawBitmap(shaderBitmap, shaderLeft, shaderTop, holePaint)
canvas.restore()
}
}
super.dispatchDraw(canvas)
}
- 核心代碼就上面這些瞳浦,剩下就是一些形狀類型處理,資源釋放废士,自定義屬性叫潦,對外暴露設(shè)置參數(shù)方法等常規(guī)操作了。
- 最后看看使用方式:
<com.seagazer.halo.Halo
android:id="@+id/halo2"
android:layout_width="230dp"
android:layout_height="150dp"
android:layout_marginStart="30dp"
app:haloColor="#FFFF61" //光環(huán)顏色
app:haloCornerRadius="10dp" //光環(huán)圓角(設(shè)置圓角時需要設(shè)置)
app:haloInsertEdge="8dp" //光環(huán)與內(nèi)容的間距(不能小于光環(huán)寬度)
app:haloShape="roundRect" //光環(huán)類型:直角官硝,圓角矗蕊,圓形
app:haloWidth="3dp">// 光環(huán)的寬度
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="@color/halo_card"
app:cardCornerRadius="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Round Rect"
android:textColor="@color/white"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
</com.seagazer.halo.Halo>
時間不早了,年紀大了泛源,得早點休息拔妥,也就不多寫了,完整代碼和demo
大家自己去看吧达箍,喜歡的話點個贊支持下吧没龙。
項目地址: https://github.com/seagazer/halo