记得之前有人在文章下问过,华为智慧屏那种焦点框的实现。对于厂商来说,优先考虑最高效的实现方案,肯定是用c++编写,毕竟Android上层的绘制效率来说,远不及底层来的高效。
碰巧前段时间,有个朋友他们公司有类似的需求,自己独立开发的个人项目刚好用的上类似效果,就抽空用上层实现帮朋友写了一个通用组件。
![](https://img.haomeiwen.com/i4420407/763539649cb27372.png)
取名Halo,光环,已经远离游戏好多年了,算是致敬下士官长吧。题外话不说了,下面开始正文。
我们先看看华为智慧屏的效果。当焦点选中海报,按钮,选项卡的时候,这些组件外圈都有一个光晕效果在环绕旋转,说实话,在TV厂家的各种定制系统里,这焦点的动效设计真的是甩开其他家很多。
![](https://img.haomeiwen.com/i4420407/b7afe2dbc3f23c39.png)
接触过鸿蒙开发的同学,会发现即使使用原生button也会带有这种效果,可以推测应该是系统对这些基础控件都做了处理。
那我们独立的应用开发,总不可能每个控件都去定制修改一遍实现吧,就像一些无缝换肤sdk的实现,虽然是通过Factory的方式统一拦截,把原生控件替换成自定义对应的控件,但是内部依旧需要维护一系列的自定义控件,去对应适配替换原生控件。因此,这里,首先淘汰定制化Button,TextView,CardView等等基础控件的方式。说实话我也没精力和时间去帮都实现一遍。因此,确定目标方案,通过wrapper方式包裹子控件实现,自然就会考虑到轻量的ViewGroup:FrameLayout。
我们先来看个实现的效果图吧,GIF为了压缩文件大小,降帧加速了,实际上是很流畅的。支持矩形,圆角,圆形三种类型,支持光环颜色设置,环绕速度等:
![](https://img.haomeiwen.com/i4420407/fe831d22eaf5e576.gif)
![](https://img.haomeiwen.com/i4420407/d0fd9e58f7bd3775.png)
这里有一个地方其实把我卡壳了半天,注意,智慧屏上的效果是光环和内部内容区域是透明的。这样一来,也就不能简单的直接往canvas
上绘制了。
最初我想到了两种方案:
- 对
canvas
进行save
,然后按path
裁剪后再绘制光晕,恢复canvas
后在绘制内容区域。这样的确可以实现光环和内容之间的间隔透明化,但是clipPath
有一个大家都知道的致命缺点:锯齿!当然,为了验证效果,我还是实现了一遍,结果却有点意想不到。总结一下:性能比较高效,在TV上和一些低版本的手机上的确存在明显锯齿,尤其是圆形。但是在我的一加8,android 11系统下,clipPath
的圆滑程度竟然比下面的方案2还要完美。这就让我尴尬了,具体原因未知,猜测是系统层面做了优化,有知道的同学麻烦告知下。 - 使用
PorterDuffXfermode
混合模式。这十多种模式,说简单简单,说复杂也复杂,坑是挺多的,你按照说明和官方给的混合效果图自己去写,很大概率不会出现官方效果图的结果。混合模式自己去看官方demo
吧,这里就简单说下,混合模式必须是bitmap
的混合叠加,并且要注意src
和dst
先后顺序。下面会介绍通过混合模式实现间隔透明化的具体实现:
实现
- 首先我们聚焦点就是这个光环的光晕效果,它在动效执行过程是绕着内容移动的,其实仔细想想,本质上就是旋转嘛。渐变效果,并且需要在旋转过程中保持外环移动所在位置的渐变色相同,首选方案
SweepGradient
,我们先上一段创建光环的代码:
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
区域,我们的光环shader
是圆形,在旋转过程中要始终环绕在内容区域外框,那该shader
的圆形半径就是canvas
的对角线的一半。上面提到混合模式是作用于bitmap
,因此我们需要把shader
绘制到一张bitmap
上,而这张bitmap
的尺寸就如图所示:
![](https://img.haomeiwen.com/i4420407/aa726c94fc5e0074.png)
- 至此,我们完成了第一步,创建了一个光环效果。说到光环和内容区域的透明间隔,用混合模式怎么实现呢?有同学了解过
SurfaceView
的原理吧,挖孔
,这个名词应该听过。我这里采取的就是这种方式,通过一张挖孔bitmap
与光环bitmap
进行混合,达到把实体的光环图中间挖出一个透明区域,供内容绘制,haloStrokeWidth
是我们光环的宽度,左右上下各减去光环宽度,剩余的canvas
区域就是我们绘制内容的区域了:
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)
}
}
}
}
- 至此,我们就创建了
shaderBitmap
和holeBitmap
两张图片。开始混合运算,我们先通过混合把外圈的光环绘制处理好,再将剩余区域交给原生的绘制流程进行内容区域(ChildView
)的绘制。同时我们构建一个基础的ValueAnimate
进行动画运算,不断旋转重绘就能产生光环环绕移动的效果啦:
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)
}
- 核心代码就上面这些,剩下就是一些形状类型处理,资源释放,自定义属性,对外暴露设置参数方法等常规操作了。
- 最后看看使用方式:
<com.seagazer.halo.Halo
android:id="@+id/halo2"
android:layout_width="230dp"
android:layout_height="150dp"
android:layout_marginStart="30dp"
app:haloColor="#FFFF61" //光环颜色
app:haloCornerRadius="10dp" //光环圆角(设置圆角时需要设置)
app:haloInsertEdge="8dp" //光环与内容的间距(不能小于光环宽度)
app:haloShape="roundRect" //光环类型:直角,圆角,圆形
app:haloWidth="3dp">// 光环的宽度
<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
网友评论