Android 自定义圆形旋转进度条,仿微博头像加载效果

5,083 阅读5分钟

微博 App 的用户头像有一个圆形旋转进度条的加载效果,看上去效果非常不错,如图所示:

据说 Instagram 也采用了这种效果。最近抽空研究了一下,最后实现的效果是这样:

基本上能模拟出个大概,代码量不大,来讲讲实现思路吧。

模拟一种动画效果,首先需要仔细分析其运作过程,找到其中的物理规律,从而确定实现方案。像这种运动速度较快的动画,一般不是很容易看清。可以先通过录屏软件,录取动画运作的过程,然后借助一些辅助工具放慢放大,比如 PS,反复重复播放几遍,基本上就能看出动画的运作规律了。

回到这里的加载效果,拆分开来,可以理解为画笔上的两层绘制和时间上的两段过程。时间上,很明显可以看出分为前 360 度和后 360 度,主要在画笔上:

1,单一完整的圆弧绘制。前 360 度,从 360 度的圆弧到 0 度圆弧的递减过程;后 360 度,从 0 度圆弧到 360 度圆弧的递增过程。

2,重复片段的圆弧绘制。前 360 度,从零开始,逐渐递增,直到多段填满圆周;后 360 度,反过来,逐渐递减,直到数量为零。

其他的就是细节的处理,后面具体实现时再提及,我们先来看看如何实现这两个核心流程的绘制。

由于这两个流程使用的画笔属性相同,所以使用一个 Paint 对象即可,这段代码没什么好讲的,就是一些初始化工作:

mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mArcWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);

注意,与微博原图效果不同的是,我的效果图中使用到了渐变色圆环,这样效果更好看一些。使用 setShader() 方法可以给画笔设置渐变色,实现方式是:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    ......

    Shader shader = new LinearGradient(0f, 0f, mWidth, mHeight,
            mStartColor, mEndColor, Shader.TileMode.CLAMP);
    mPaint.setShader(shader);
}

Shader 子类有很多,这里使用的是线性渐变类 LinearGradient,由于需要使用 View 宽高,所以放在了 onSizeChanged() 函数里面。

核心计算和绘制工作都在 onDraw() 方法里面,看下这里的代码:

@Override
protected void onDraw(Canvas canvas) {
    if (maxAngle <= 360) {
        float angle = 0;
        canvas.rotate(mRatio * maxAngle / 360, mWidth / 2, mHeight / 2);
        canvas.drawArc(mRectF, maxAngle, 360 - maxAngle, false, mPaint);
        while (angle <= maxAngle) {
            float length = mArcRadian * angle / maxAngle;
            canvas.drawArc(mRectF, 0, length, false, mPaint);
            canvas.rotate(mArcSpacing, mWidth / 2, mHeight / 2);
            angle += mArcSpacing;
        }
    } else {
        float angle = 0;
        canvas.rotate(mRatio + mRatio * (maxAngle - 360) / 360, mWidth / 2, mHeight / 2);
        canvas.drawArc(mRectF, 0, maxAngle - 360, false, mPaint);
        canvas.rotate(maxAngle - 360, mWidth / 2, mHeight / 2);
        while (angle <= 720 - maxAngle) {
            float length = mArcRadian * angle / (720 - maxAngle);
            canvas.drawArc(mRectF, 0, length, false, mPaint);
            canvas.rotate(mArcSpacing, mWidth / 2, mHeight / 2);
            angle += mArcSpacing;
        }
    }

    if (maxAngle <= 720) {
        maxAngle += mArcSpacing;
        postInvalidateDelayed(30);
    }
}

前面提到,动画在时间上分前后 360 度的两段过程,所以这里定义了一个 maxAngle 变量来定义时间的变化。可以看到,前后 360 度的绘制代码看上去差不多,但还是有很大区别的。需要理解的地方有:

1,多个小段圆弧可以利用画布旋转的方式轻松实现,也就是 canvas.rotate() 方法,上面代码中的 while 循环部分。这里有个细节处理,就是每段圆弧的弧度有个递增变化。

2,整个 View 给人的感觉有一种旋转的效果,为了实现这个效果,在每次绘制前,都增加了旋转的步骤,也就是这行代码:

canvas.rotate(mRatio * maxAngle / 360, mWidth / 2, mHeight / 2);

其中 mRatio 表示整个动画结束时,View 相比初始状态时整体旋转的角度。 这里我设置的默认值是 60,值越大,动画执行时呈现出越快的旋转效果。大家可以修改源码,自己尝试一下。

3,还有一点就是,maxAngle 变量每次增量值的设置,一定要设置为相邻两段片段圆弧的间距弧度。这样做的目的是,保证每次绘制,片段圆弧都能有一个完整的递增或递减。否则,动画执行时,看上会发生视觉上的抖动效果。

基本上就是这样,代码量虽然不多,但是各种细节的处理还是耗费了很多时间调整。本来想使用一些图例展示一下每行代码的作用,但是太费时间了。如果感兴趣的,完全可以自己下载一下源码,修改试试看。

经过简单地封装,提取出一部分属性:

<declare-styleable name="CircleLoadingView">
    <attr name="circleStartColor" format="color|reference"/>
    <attr name="circleEndColor" format="color|reference"/>
    <attr name="circleArcWidth" format="integer|reference"/>
    <attr name="circleArcRadian" format="integer|reference"/>
    <attr name="circleArcSpacing" format="integer|reference"/>
</declare-styleable>

注意:中间的头像部分不是这里自定义 View 的内容,使用时可以自由填充内容。比如,看下我这里的使用方式,还是比较自由的:

<RelativeLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_centerInParent="true">

    <com.yifeng.view.view.CircleLoadingView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:circleStartColor="#ffff00"
        app:circleEndColor="#ff0000"
        app:circleArcRadian="5"
        app:circleArcWidth="10"
        app:circleArcSpacing="10"/>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_avatar_default"/>

</RelativeLayout>

源代码还是老样子,统一在 GitHub 上的自定义 View 集锦库里,地址如下。:

github.com/Mike-bel/an…

关于我:亦枫,博客地址:yifeng.studio/,新浪微博:IT亦枫

微信扫描二维码,欢迎关注我的个人公众号:安卓笔记侠

不仅分享我的原创技术文章,还有程序员的职场遐想