Halo-智慧屏焦点动效实现方案

2021-03-17  本文已影响0人  seagazer

记得之前有人在文章下问过,华为智慧屏那种焦点框的实现。对于厂商来说,优先考虑最高效的实现方案,肯定是用c++编写,毕竟Android上层的绘制效率来说,远不及底层来的高效。
碰巧前段时间,有个朋友他们公司有类似的需求,自己独立开发的个人项目刚好用的上类似效果,就抽空用上层实现帮朋友写了一个通用组件。


取名Halo,光环,已经远离游戏好多年了,算是致敬下士官长吧。题外话不说了,下面开始正文。

我们先看看华为智慧屏的效果。当焦点选中海报,按钮,选项卡的时候,这些组件外圈都有一个光晕效果在环绕旋转,说实话,在TV厂家的各种定制系统里,这焦点的动效设计真的是甩开其他家很多。

接触过鸿蒙开发的同学,会发现即使使用原生button也会带有这种效果,可以推测应该是系统对这些基础控件都做了处理。
那我们独立的应用开发,总不可能每个控件都去定制修改一遍实现吧,就像一些无缝换肤sdk的实现,虽然是通过Factory的方式统一拦截,把原生控件替换成自定义对应的控件,但是内部依旧需要维护一系列的自定义控件,去对应适配替换原生控件。因此,这里,首先淘汰定制化Button,TextView,CardView等等基础控件的方式。说实话我也没精力和时间去帮都实现一遍。因此,确定目标方案,通过wrapper方式包裹子控件实现,自然就会考虑到轻量的ViewGroup:FrameLayout。

我们先来看个实现的效果图吧,GIF为了压缩文件大小,降帧加速了,实际上是很流畅的。支持矩形,圆角,圆形三种类型,支持光环颜色设置,环绕速度等:

halo.gif

这里有一个地方其实把我卡壳了半天,注意,智慧屏上的效果是光环和内部内容区域是透明的。这样一来,也就不能简单的直接往canvas上绘制了。
最初我想到了两种方案:

  1. canvas进行save,然后按path裁剪后再绘制光晕,恢复canvas后在绘制内容区域。这样的确可以实现光环和内容之间的间隔透明化,但是clipPath有一个大家都知道的致命缺点:锯齿!当然,为了验证效果,我还是实现了一遍,结果却有点意想不到。总结一下:性能比较高效,在TV上和一些低版本的手机上的确存在明显锯齿,尤其是圆形。但是在我的一加8,android 11系统下,clipPath的圆滑程度竟然比下面的方案2还要完美。这就让我尴尬了,具体原因未知,猜测是系统层面做了优化,有知道的同学麻烦告知下。
  2. 使用PorterDuffXfermode混合模式。这十多种模式,说简单简单,说复杂也复杂,坑是挺多的,你按照说明和官方给的混合效果图自己去写,很大概率不会出现官方效果图的结果。混合模式自己去看官方demo吧,这里就简单说下,混合模式必须是bitmap的混合叠加,并且要注意srcdst先后顺序。下面会介绍通过混合模式实现间隔透明化的具体实现:

实现

  1. 首先我们聚焦点就是这个光环的光晕效果,它在动效执行过程是绕着内容移动的,其实仔细想想,本质上就是旋转嘛。渐变效果,并且需要在旋转过程中保持外环移动所在位置的渐变色相同,首选方案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的尺寸就如图所示:

  1. 至此,我们完成了第一步,创建了一个光环效果。说到光环和内容区域的透明间隔,用混合模式怎么实现呢?有同学了解过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)
                }
            }
        }
    }
  1. 至此,我们就创建了shaderBitmapholeBitmap两张图片。开始混合运算,我们先通过混合把外圈的光环绘制处理好,再将剩余区域交给原生的绘制流程进行内容区域(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)
    }
  1. 核心代码就上面这些,剩下就是一些形状类型处理,资源释放,自定义属性,对外暴露设置参数方法等常规操作了。
  2. 最后看看使用方式:
    <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

上一篇 下一篇

猜你喜欢

热点阅读