开源组件剖析:ShimmerAndroid

1、认识ShimmerAndroid

ShimmerAndroid是Facebook贡献的一个开源组件,它可以使你的View产生类似于高光的效果,非常酷炫。 Github:ShimmerAndroid

关于它的使用,也是非常的简单,只需要将你的布局作为ShimmerFrameLayout的子view,并且对ShimmerFrameLayout进行一系列简单的配置,就可以实现相应的效果。Github上面有demo,相信大家看了以后不出5分钟就可以将其投入到生产。

  <ShimmerFrameLayout
      android:id="@+id/shimmer_view_container"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      shimmer:duration="10000">

    <LinearLayout
        android:id="@+id/content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical"
        >

      <ImageView
          android:layout_width="64dp"
          android:layout_height="64dp"
          android:src="@drawable/fb_logo"/>

      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_marginTop="20dp"
          android:gravity="center"
          android:text="Facebook’s mission is to give people the power to share and make the world more open and connected."
          style="@style/thin.white.large"/>
    </LinearLayout>
  </ShimmerFrameLayout>

这就是demo的布局了,在视图当中调用下面的方法:

mShimmerViewContainer = (ShimmerFrameLayout) findViewById(R.id.shimmer_view_container);
mShimmerViewContainer.startShimmerAnimation();

效果图如下:

其实大家不难看出,ShimmerFrameLayout当中的子view一方面按照原貌进行呈现,这个我们姑且称之为『底图』,另一方面又有高亮的效果闪过,这个我们称之为『遮罩』,这样Shimmer的效果就完全的展现在了大家的面前。

它有几个属性,其中比较重要的比如

  • BaseAlpha:子view在绘制中,底图的透明度
  • MaskShape:遮罩形状,后面我们就会知道,遮罩其实是一张绘制了Gradient的Bitmap,而这个形状其实就是Gradient的形状,分别是Linear和Radical两种
  • Angle:遮罩动画的移动方向和角度
  • Dropoff:遮罩边缘的淡出效果的跨度
  • Intensity:遮罩中间高亮区域的跨度

为了让大家直观的理解Dropoff和Intensity,我将遮罩不加混色效果直接绘制出来呈现给大家,如下图:

这样,了解了这几个重要的属性,大家就可以根据自己的喜好为自己的应用添加Shimmer效果了。说到这里,如果大家只是关心它的用法,后面的内容就可以略过了,然而我相信你们肯定不是这样的浅尝辄止:)

下面我们将从Shimmer效果的『闪动』和『遮罩』的生成来剖析一下ShimmerAndroid的原理。

2、闪动的效果如何产生?

大家都是有经验的开发者,既然我们已经知道Shimmer的效果不过是在原图上面覆盖了一层遮罩,那么不难想象,高光略过的闪动效果,无非就是一个位移动画,请看源码:

   private Animator getShimmerAnimation() {
        if (mAnimator != null) {
            return mAnimator;
        }
        int width = getWidth();
        int height = getHeight();
        switch (mMask.shape) {
            default:
            case LINEAR:
                switch (mMask.angle) {
                    default:
                    case CW_0:
                        mMaskTranslation.set(-width, 0, width, 0);
                        break;
                    case CW_90:
                        mMaskTranslation.set(0, -height, 0, height);
                        break;
                    case CW_180:
                        mMaskTranslation.set(width, 0, -width, 0);
                        break;
                    case CW_270:
                        mMaskTranslation.set(0, height, 0, -height);
                        break;
                }
        }
        mRepeatDelay = 500;
        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f + (float) mRepeatDelay / mDuration);
        mAnimator.setDuration(mDuration + mRepeatDelay);
        mAnimator.setRepeatCount(mRepeatCount);
        mAnimator.setRepeatMode(mRepeatMode);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animateValue = (float) animation.getAnimatedValue();
                float value = Math.max(0.0f, Math.min(1.0f, animateValue));
                setMaskOffsetX((int) (mMaskTranslation.fromX * (1 - value) + mMaskTranslation.toX * value));
                setMaskOffsetY((int) (mMaskTranslation.fromY * (1 - value) + mMaskTranslation.toY * value));
            }
        });
        return mAnimator;
   }   

这段代码也简单,其实就是生成了一个Animator的实例,我们看到这里面在Animator Update的时候,会去根据Animator的进度不断改变MaskOffsetX和MaskOffsetY。没错,就是一个非常简单的位移动画,至于说其他代码(比如switch里面的代码),无非是为了计算动画每一帧的这两个参数的结果所做的准备工作而已。

既然知道了,动画本质就是对MaskOffsetX和MaskOffsetY的操作,下一步就要知道怎么应用这两个数值了。

   private void drawMasked(Canvas renderCanvas) {
        Bitmap maskBitmap = getMaskBitmap();
        if (maskBitmap == null) {
            return;
        }
        //注意,源码当中下面的drawColor和clipRect的顺序是反过来的
        renderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
        renderCanvas.clipRect(
                mMaskOffsetX,
                mMaskOffsetY,
                mMaskOffsetX + maskBitmap.getWidth(),
                mMaskOffsetY + maskBitmap.getHeight());

        super.dispatchDraw(renderCanvas);

        renderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint);
    }

在这里我们就可以看到mMaskOffsetX和mMaskOffsetY的用处了,他们决定了遮罩绘制的位置,这样也就好解释我们看到的高光掠过底图的闪动效果了。

代码当中,我把源文件的drawColor和clipRect的顺序倒置了。按照原来的顺序,如果底图是静止的,或者BaseAlpha几乎接近于1.0时,问题并不是很明显,而一但让底图动起来,或者把BaseAlpha设置为0,那么高光掠过时,会留下残影,有兴趣的读者不妨一试。原因也很容易看出,drawColor是为了清屏,但清屏的区域是上一次绘制的区域,上一次绘制的区域跟这一次自然是不同的。因此按照我给出的代码顺序更为妥当。

3、遮罩如何生成?

有经验的开发者肯定能想到,遮罩其实就是一些颜色的变化图,通过遮罩与底图的色值混合运算,我们就可以看到比较酷炫的效果。

其实在为了讲清楚Dropoff和Intensity时,我们就讲遮罩直接绘制了出来,大家应该也不陌生了,为了生成这张遮罩,我们就要了解下面的一个概念:shader

此shader并非OpenGL的shader,我们都知道,Android底层的图形引擎主要是Skia,想必大家也能猜到所谓shader不过是对Skia引擎的SkShader的一个封装而已,而这个shader其实是决定我们的paint在绘制到canvas的那一刻,所绘制的色值的对象。

在SkShader当中,有个叫shadeSpan的函数,还有一个shadeProc函数指针,这二者一起决定了paint的绘制内容。

关于Shader的一些细节,大家可以参考Skia引擎的源码,我后续也会整理一篇文章来介绍这一部分知识。

Shader和PorterDuff的关系和区别:前者决定了绘制的SRC是什么,而后者决定了SRC和DST如何混色,进而产生出最终的结果。

Shimmer效果有两种,分别是线性、圆形,对应的Shader分别是LinearGradient和RadicalGradient。

gradient = new LinearGradient(
                x1, y1,
                x2, y2,
                mMask.getGradientColors(),
                mMask.getGradientPositions(),
                Shader.TileMode.CLAMP);
int x = width / 2;
int y = height / 2;
gradient = new RadialGradient(
       x,
       y,
       (float) (Math.max(width, height) / Math.sqrt(2)),
       mMask.getGradientColors(),
       mMask.getGradientPositions(),
       Shader.TileMode.CLAMP);

一旦生成遮罩之后,就在绘制过程中用PorterDuff进行混色,产生高亮的效果:

   private void drawMasked(Canvas renderCanvas) {
        Bitmap maskBitmap = getMaskBitmap();
        if (maskBitmap == null) {
            return;
        }
        renderCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
        renderCanvas.clipRect(
                mMaskOffsetX,
                mMaskOffsetY,
                mMaskOffsetX + maskBitmap.getWidth(),
                mMaskOffsetY + maskBitmap.getHeight());

        super.dispatchDraw(renderCanvas);
        renderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint);
    }

这段代码我们已经看到过了,这次我们主要关注mMaskPaint。在绘制遮罩时,遮罩就是PorterDuff应用中涉及的的SRC,而底图就是DST。二者混色的模式是什么呢?

mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));

而DST_IN又是什么意思呢?

        /** [Sa * Da, Sa * Dc] */
        DST_IN      (6),

这种模式下,混色的结果中,alpha为DST和SRC的alpha的乘积(如 Ra = Sa * Da = 0.5 * 0.6 = 0.3),color按RGB通道计算的结果为DST的color乘以SRC的alpha。换句话说,SRC只是输入了一个alpha而已,绘制的具体色值,取决于DST。

4、结语

第一眼看到ShimmerAndroid的时候,觉得效果确实不错,细细读下来,代码只有千行。真是大道至简。

本文作者:bennyhuo

转载请注明出处:开源组件剖析:ShimmerAndroid

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值