码出未来

用键盘敲出无限可能

ProgressBar 深入分析

ProgressBar 深入分析

1 前言

【ProgressBar】既进度条,当我们在做一些耗时操作的时候(例如下载文件),可以使用ProgressBar给用户提供一个进度提示,告诉用户当前的进度。ProgressBar提供了两种进度显示模式,分别是具有进度值的【精确模式】和不具有进度值的【模糊模式】。本文将分别从ProgressBar的属性、用法、源码分析、自定义四个方面对ProgressBar进行全面的介绍,希望对读者有一定的帮助,欢迎提出任何的疑问或者错误。我们先来看看几个示意图:

1.1 标准的ProgressBar

精确模式 模糊模式(圆形) 模糊模式(横向)

1.2 自定义的ProgressBar

奔跑的小人 旋转的齿轮 反转的齿轮
横向渐变进度条 垂直渐变进度条 横向文字进度条
横向星星进度条 垂直星星进度条

2 ProgressBar的XML属性

本节我们将会介绍ProgressBar的常用属性,通过这些预制属性,我们可以对ProgressBar的外观和行为进行个性化定制。

style

设置ProgressBar的样式,不同的样式会有不同的形状和模式:

  • Widget.ProgressBar.Horizontal

    横向进度条(精确模式或模糊模式,这取决于Android:indeterminate)。

  • Widget.ProgressBar

    中号的圆形进度条(模糊模式)。

  • Widget.ProgressBar.Small

    小号的圆形进度条(模糊模式)。

  • Widget.ProgressBar.Large

    大号的圆形进度条(模糊模式)。

  • Widget.ProgressBar.Inverse

    中号的圆形进度条(模糊模式),该样式适用于亮色背景(例如白色)。

  • Widget.ProgressBar.Small.Inverse

    小号的圆形进度条(模糊模式),该样式适用于亮色背景(例如白色)。

  • Widget.ProgressBar.Large.Inverse

    大号的圆形进度条(模糊模式),该样式适用于亮色背景(例如白色)。

android:animationResolution

当ProgressBar使用模糊模式的时候,会通过循环播放动画来提示用户任务正在进行中,通过该属性可以设置动画每一帧的间隔时间,该属性值必须是整型。

注意:实际上这个属性无论你设置任何值,都是没有效果的,查看了ProgressBar的源码后也没有发现任何地方有对该属性的使用。

android:indeterminate

设置ProgressBar是否使用模糊模式,该属性一般在进度条为横向情况(Widget.ProgressBar.Horizontal)下才设置,因为圆形进度条本身就是模糊模式。

  • true

    启用模糊模式。

  • false

    禁用模糊模式

android:indeterminateBehavior

当ProgressBar使用模糊模式的时候,会通过循环播放动画来提示用户任务正在进行中,该属性用于设置动画结束一次循环之后的行为。在ProgressBar的自定义章节中,我们会对该属性进行更详细的说明。

  • repeat

    默认值,当动画结束一次循环之后从头开始再次播放,例如圆形进度条不断旋转,如下图所示:

    Repeat

  • cycle

    当动画结束一次循环之后,反向播放动画,例如圆形进度条从0°旋转到360°之后,从360°反向旋转到0°,如此反复,如下图所示:

    Repeat

注意:该属性适用于自定义ProgressBar,并且官方的API说明文档对该属性的解释很容易误导开发者。

android:indeterminateDrawable

设置模糊状态下ProgressBar的Drawable资源,该Drawable资源是一个带动画的资源文件,例如旋转动画<rotate>。在ProgressBar的自定义章节中,我们会对该属性进行更详细的说明。

注意:该属性适用于自定义ProgressBar。

android:indeterminateDuration

设置模糊状态下ProgressBar一个周期动画的持续时间,该属性值必须是整型。在【ProgressBar的自定义】中,我们会对该属性进行更详细的说明。

注意:该属性适用于自定义ProgressBar。

android:indeterminateOnly

限制ProgressBar只能使用模糊模式。

  • true

    限制只能使用模糊模式。

  • false

    不限制只能使用模糊模式。

android:max

设置ProgressBar在精确模式下可以达到的最大值。

android:progress

设置0~MAX之间的默认进度值。

android:maxHeight

设置ProgressBar视图(Drawable)的最大高度,这是一个可选属性。

android:maxWidth

设置ProgressBar视图(Drawable)的最大宽度,这是一个可选属性。

android:minHeight

设置ProgressBar视图(Drawable)的最小高度,这是一个可选属性。

android:minWidth

设置ProgressBar视图(Drawable)的最小宽度,这是一个可选属性。

android:progressDrawable

设置在精确模式下ProgressBar第一个进度条的Drawable资源。

android:secondaryProgress

设置在精确模式下ProgressBar第二个进度条的Drawable资源。

android:mirrorForRtl

设置ProgressBar是否从右往左显示,这个主要是针对阿拉伯等一些习惯是从右往左显示的国家设置的,当你系统的语言是阿拉伯语,并且在AndroidManifest.xml的<application>中设置了android:supportsRtl="true"的时候,该属性才会有效果。

android:interpolator

设置ProgressBar默认动画的插值器,该插值器用于改变动画的变化率。当ProgressBar是模糊模式并且我们指定的Drawable资源没有动画效果时,ProgressBar会使用默认的动画效果,该动画是使用这个插值器。

3 ProgressBar的用法

本章将介绍ProgressBar两种模式(模糊模式和精确模式)的具体实现,已经常用的方法使用。

3.1 模糊进度条的使用

当我们没办法确定耗时任务的完成进度时,例如没办法确定文件下载进度,模糊模式的进度条是一个很好的方式来提示用户任务正在进行中,从而不会让用户误以为程序死掉了或者告诉用户耐心等待。模糊模式的进度条分为两种,分别是圆形模糊进度条和横向模糊进度条,模糊进度条一般都是一个动画不断地播放,如下图所示:

模糊模式(圆形) 模糊模式(横向)

两种模糊模式进度条的实现都非常简单(非自定义),只需要在布局文件中编写如下代码即可:

1. 圆形模糊进度条

<ProgressBar
    style="@android:style/Widget.ProgressBar.Large"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

2. 横向模糊进度条

<ProgressBar
    style="@android:style/Widget.ProgressBar.Horizontal"
    android:indeterminate="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

3.2 精确进度条的使用

当我们需要明确告诉用户当前的任务执行到何种程度的时候,精确进度条是最佳选择。精确进度条能够以整数值设置进度值,系统提供的默认精确进度条只有一种,既我们常见的横向进度条,如下图所示:

精确进度条

精确进度条的实现会比模糊进度条要复杂一点,具体分为以下几个步骤:

  1. 在布局文件中设置style为Widget.ProgressBar.Horizontal。

  2. 通过setMax(int max)方法设置ProgressBar的最大值,默认值为100,你也可以直接在布局文件中通过android:max=”最大值”方式来指定ProgressBar的最大值。

  3. 通过setProgress(int progress)方法设置ProgressBar当前的进度值。

  4. 如果需要的话,你可以通过getProgress()方法获取ProgressBar当前进度值。

需要注意的是ProgressBar本身并没有提供任何显示进度值的方式,所以我们需要用一个TextView来显示进度值,具体的XML代码和Java代码如下:

1. XML代码:layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/pb_loading"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="70dp"
        android:layout_marginRight="70dp" />

    <TextView
        android:id="@+id/tv_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/pb_loading"
        android:layout_below="@id/pb_loading"
        android:layout_margin="5dp"
        android:text="0%"
        android:textColor="#333333" />

</RelativeLayout>

2. Java代码:MainActivity.java

package com.hjdzone.progressbardemo;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.lang.ref.WeakReference;

public class MainActivity extends ActionBarActivity {

    private ProgressBar mPbLoading;
    private TextView mTvProgress;
    private MyHandler mHandler;

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> mWeakActivity;

        public MyHandler(MainActivity activity) {
            mWeakActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {

            MainActivity activity = mWeakActivity.get();
            if (activity != null) {
                // 更新进度条
                activity.mPbLoading.setProgress(msg.arg1);
                // 更新数值显示
                activity.mTvProgress.setText(msg.arg1 + "%");
            }

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        mPbLoading = (ProgressBar) findViewById(R.id.pb_loading);
        mTvProgress = (TextView) findViewById(R.id.tv_progress);

        // 设置进度条最大值为100
        mPbLoading.setMax(100);
    }

    @Override
    protected void onStart() {
        super.onStart();

        // 启动线程模拟加载
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        for (int i = 0; i <= 100; i++) {
                            Thread.sleep(50);
                            Message message = mHandler.obtainMessage();
                            message.arg1 = i;
                            mHandler.sendMessage(message);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

    }

}

另外,精确模式的ProgressBar还提供了显示第二个进度值的功能,第二个进度值是被第一个进度值所覆盖的,也就是说当第二进度值小于第一进度值的时候,就看不到第二进度值的显示了,你可以通过setSecondaryProgress(int secondaryProgress)方法来设置第二进度值,也可以通过android:secondaryProgress属性设置,效果图如下:

第二进度值

4 ProgressBar源码分析

本章的内容将会对ProgressBar的源码进行全面的分析,了解ProgressBar的实现步骤,同时也为自定义ProgressBar打好基础,需要注意的是在分析源码时只贴出了实现ProgressBar的核心代码。首先我们来看一张ProgressBar的实现流程图,之后我们会根据流程图对ProgressBar进行一步步分析。

4.1 Drawable的Level属性介绍

首先我们来谈谈Drawable的【Level】属性,因为ProgressBar的实现都依赖于Drawable的变化,而Drawable的变化(动画效果或者进度值变化)都是依赖于Level值的变化,所以我们有必要先了解下Level这个属性的作用。下面我们以ClipDrawable为例,看看Level是如何影响Drawable的形态的。

Level的最大值固定为10000,ClipDrawable会根据Level值的不同显示不同宽度的可见区域,通过该特性我们可以根据进度比例设置Level值,从而实现进度条长度的控制。如下图所示【蓝色】区域为可见区域,【灰色】为不可见区域:

同理,其他Drawable也可以根据Level的变化实现不同的形态变化,例如RotateDrawable会根据Level值改变角度、ScaleDrawableDrawable会根据Level值改变大小等。

4.2 初始化

首先,我们来看看ProgressBar的是如何初始化各个属性值的,我们会从它的变量、常量和构造方法等方面分析ProgressBar的初始化。

4.2.1 成员变量(常量)

private static final int MAX_LEVEL = 10000

对应Drawable的level属性,该属性在不同的Drawable中有不同的效果,ProgressBar的进度的变化实际上是控制Drawable可见区域的变化,而Drawable的可以见区域是通过Level属性来改变的。

int mMinWidth

ProgressBar视图的最小宽度,对应android:minWidth属性。

int mMaxWidth

ProgressBar视图的最大宽度,对应android:maxWidth属性。

int mMinHeight

ProgressBar视图的最小高度,对应android:minHeight属性。

int mMaxHeight

ProgressBar视图的最大高度,对应android:maxHeight属性。

private int mProgress

ProgressBar当前的进度值,对应android:progress属性。

private int mSecondaryProgress

ProgressBar第二个进度值,对应android:secondaryProgress属性。

private int mMax

ProgressBar的最大进度值,对应android:max属性。

private int mBehavior

ProgressBar在模糊状态下,动画结束一次循环之后的行为,对应android:indeterminateBehavior属性。

private int mDuration

ProgressBar在模糊状态下,一周期动画的持续时间,对应android:indeterminateDuration属性。

private boolean mIndeterminate

ProgressBar是否使用模糊模式。

private boolean mOnlyIndeterminate

ProgressBar是否只能使用模糊模式。

private Transformation mTransformation

记录某一时刻动画的属性。

private AlphaAnimation mAnimation

ProgressBar默认动画,虽然是一个透明度变化的动画,但是实际上ProgressBar使用该动画的alpha值(0~1.0)来设置RotateDrawable、ScaleDrawable等具有动画效果的Drawable的Level值。

private boolean mHasAnimation

标识ProgressBar使用的是自己默认的动画效果。

private Drawable mIndeterminateDrawable

ProgressBar在模糊模式下使用的Drawable资源,对应android:indeterminateDrawable属性。

private Drawable mProgressDrawable

ProgressBar在精确模式下使用的Drawable资源,对应android:progressDrawable属性。

private Drawable mCurrentDrawable

该变量用于保存当前ProgressBar所使用的Drawable资源,ProgressBar有两种Drawable资源可以使用,它们就是上面提到的两个。

private boolean mNoInvalidate

标识ProgressBar的postInvalidate()方法是否可用。

private Interpolator mInterpolator

ProgressBar默认动画的插值器。

private RefreshProgressRunnable mRefreshProgressRunnable

异步更新ProgrssBar的时候,使用的是线程消息机制,所以会定义一个Runnable对象来封装刷新ProgressBar的业务逻辑,关于线程消息机制,可以参考Android线程消息机制。

private long mUiThreadId

记录主线程的ID,通过该ID判断更新进度条的时候是否需要异步更新。

private boolean mShouldStartAnimationDrawable

标识模糊模式的Drawable资源自带动画效果(即AnimationDrawable),可以直接循环播放动画。

private boolean mInDrawing

标识ProgressBar是否正在绘制进度条。

private boolean mRefreshIsPosted

标识异步刷新请求是否已经post到消息队列中。

boolean mMirrorForRtl = false

标识ProgressBar是否从右往左显示,这个主要是针对阿拉伯等一些习惯是从右往左显示的国家设置的,当你系统的语言是阿拉伯语,并且在AndroidManifest.xml的<application>中设置了android:supportsRtl=”true”的时候,该属性才会有效果。

private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>()

该List用于暂存异步更新进度条的数据,因为异步更新是通过线程消息机制完成的,所以我们在短时间内发送的更新数据都会暂存在该List里,直到线程消息机制开始处理更新操作。

4.2.2 构造方法

在构造方法内,都是一些ProgressBar成员变量的初始化,包括属性的默认值、Drawable资源的预处理等,该方法会涉及到其他的方法,在接下来的内容里会给出详细的介绍和分析,大家只要先了解下构造方法内都做了些什么就可以了。

public class ProgressBar extends View {

    ...

    public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        // 获取当前线程的ID,也就是主线程ID。
        mUiThreadId = Thread.currentThread().getId();

        // 设置进度条成员变量的默认值。
        initProgressBar();

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes);

        mNoInvalidate = true;

        // 获取精确模式进度条的Drawable资源,并进行特殊处理。
        final Drawable progressDrawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable);
        if (progressDrawable != null) {
            setProgressDrawableTiled(progressDrawable);
        }

        // 一些属性的初始化。
        mDuration = a.getInt(R.styleable.ProgressBar_indeterminateDuration, mDuration);
        mMinWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_minWidth, mMinWidth);
        mMaxWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_maxWidth, mMaxWidth);
        mMinHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_minHeight, mMinHeight);
        mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
        mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior);

        // 设置ProgressBar动画的插值器,默认是线性变化的插值器。
        final int resID = a.getResourceId(
                com.android.internal.R.styleable.ProgressBar_interpolator,
                android.R.anim.linear_interpolator); // default to linear interpolator
        if (resID > 0) {
            setInterpolator(context, resID);
        }

        // 设置进度条的最大值、第一进度值、第二进度值。
        setMax(a.getInt(R.styleable.ProgressBar_max, mMax));
        setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress));
        setSecondaryProgress(a.getInt(R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress));

        // 获取模糊模式进度条的Drawable资源,并进行特殊处理。
        final Drawable indeterminateDrawable = a.getDrawable(
                R.styleable.ProgressBar_indeterminateDrawable);
        if (indeterminateDrawable != null) {
            setIndeterminateDrawableTiled(indeterminateDrawable);
        }

        // 又是一些属性的初始化。
        mOnlyIndeterminate = a.getBoolean(R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate);
        mNoInvalidate = false;
        setIndeterminate(mOnlyIndeterminate || a.getBoolean(R.styleable.ProgressBar_indeterminate, mIndeterminate));
        mMirrorForRtl = a.getBoolean(R.styleable.ProgressBar_mirrorForRtl, mMirrorForRtl);

        // 接下来一大堆关于Tint属性的设置,这是一个5.0的新属性,不是我们要讨论的内容。

        a.recycle();

        // 其他无关的代码。
    }

    ...

}

4.2.3 属性的初始化赋值

public class ProgressBar extends View {

    ...

    private void initProgressBar() {
        mMax = 100;
        mProgress = 0;
        mSecondaryProgress = 0;
        mIndeterminate = false;
        mOnlyIndeterminate = false;
        mDuration = 4000;
        mBehavior = AlphaAnimation.RESTART;
        mMinWidth = 24;
        mMaxWidth = 48;
        mMinHeight = 24;
        mMaxHeight = 48;
    }

    ...

}

该方法是用来对ProgressBar一些属性做初始化赋值的,从代码可以看出一些属性的初始值,例如ProgressBar的默认最大值是100、模糊动画默认是重复播放。在该方法内我们可以发现这么一个对应关系android:indeterminateBehavior="repeat"对应AlphaAnimation.RESTART,而android:indeterminateBehavior="cycle"则对应AlphaAnimation.REVERSE,所以该属性的效果就是动画的重播和倒播。

4.3 Drawable资源预处理

在实现ProgressBar的时候,对Drawable资源的预处理是很重要的一个环节。因为ProgressBar要保证所使用的Drawable资源的高度和宽度能够适应不同的视图大小,所以其内部会预先对图片进行预处理,本节我们将对ProgressBar对Drawable的处理过程进行分析。

4.3.1 Drawable平铺预处理

首先我们需要对图片资源进行平铺处理,因为一个图片的宽度可能达不到ProgressBar的宽度,这个时候就需要对图片进行平铺处理,单纯的拉伸图片是很难看的。

下面的代码是ProgressBar对Drawable资源进行平铺预处理的核心方法,它通过递归方式将Drawable资源转换成可以平铺的类型,需要注意的是该方法只会递归处理LayerDrawable和StateListDrawable内的BitmapDrawable资源,也就是图片资源,如果是其他的Drawable(例如ShapeDrawable)则不会做任何特殊处理。之所以会针对图片资源进行特殊处理,是因为通过图片资源指定ProgressBar的形状是不确定的,为了统一形状,所以要对其进行下特殊处理。具体步骤为首先设置图片横向平铺,纵向取边缘颜色填充,然后使用四个圆角为5°的矩形限制图像的边界。

public class ProgressBar extends View {

    ...

    private Drawable tileify(Drawable drawable, boolean clip) {
        if (drawable instanceof LayerDrawable) {
            // LayerDrawable资源,递归处理内部每一个Drawable,在递归处理之后又将
            // 每一个Drawable原封不动(id不变)地装入LayerDrawable内。
            LayerDrawable background = (LayerDrawable) drawable;
            final int N = background.getNumberOfLayers();
            Drawable[] outDrawables = new Drawable[N];
            for (int i = 0; i < N; i++) {
                int id = background.getId(i);
                outDrawables[i] = tileify(background.getDrawable(i),
                        (id == R.id.progress || id == R.id.secondaryProgress));
            }
            LayerDrawable newBg = new LayerDrawable(outDrawables);
            for (int i = 0; i < N; i++) {
                newBg.setId(i, background.getId(i));
            }
            return newBg;
        } else if (drawable instanceof StateListDrawable) {
            // StateListDrawable资源,递归处理内部每一个Drawable,在递归处理之后又将
            // 每一个Drawable原封不动(状态不变)地装入StateListDrawable内。
            StateListDrawable in = (StateListDrawable) drawable;
            StateListDrawable out = new StateListDrawable();
            int numStates = in.getStateCount();
            for (int i = 0; i < numStates; i++) {
                out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip));
            }
            return out;
        } else if (drawable instanceof BitmapDrawable) {
            // 递归处理的核心代码
            final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            final Bitmap tileBitmap = bitmapDrawable.getBitmap();

            // 此处保存了原始的Bitmap,该成员变量在ProgressBar中没有任何用处,可能是其他子类会用到。
            if (mSampleTile == null) {
                mSampleTile = tileBitmap;
            }

            // 创建具有四个5°圆角的ShapeDrawable用于限制图像的形状。
            final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape());

            // 创建图像渲染器,从这里可以看出在纵向对图片采取的填充方式是取边缘颜色,而在横向则对图片采取平铺方式。
            final BitmapShader bitmapShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
            shapeDrawable.getPaint().setShader(bitmapShader);
            shapeDrawable.setTintList(bitmapDrawable.getTint());
            shapeDrawable.setTintMode(bitmapDrawable.getTintMode());
            shapeDrawable.setColorFilter(bitmapDrawable.getColorFilter());

            // 上面的图像渲染器是将图片资源按横向平铺的方式组合成一个完整的进度条图像,接下来用ClipDrawable
            // 根据不同的进度值对完整的PorgressBar进行剪裁,例如进度值是50%,则剪裁掉一半的图像。
            return clip ? new ClipDrawable(shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL) : shapeDrawable;
        }

        // 对于其他类型的Drawable则不做任何处理直接返回。
        return drawable;
    }

    ...

}

private Drawable tileify(Drawable drawable, boolean clip)方法内调用了getDrawableShape()这方法,该方法很简单,就是创建并放回一个四个圆角为5°的矩形,这个矩形是用来限制ProgressBar图像资源的形状。

public class ProgressBar extends View {

    ...

    Shape getDrawableShape() {
        final float[] roundedCorners = new float[]{5, 5, 5, 5, 5, 5, 5, 5};
        return new RoundRectShape(roundedCorners, null, null);
    }

    ...

}

4.3.2 模糊模式的Drawable预处理

在前面ProgressBar的构造方法内部我们有看到调用setIndeterminateDrawableTiled(Drawable d),它的作用就是对模糊模式的Drawable做预处理操作,内部分成两部完成,首先是平铺和边框预处理,然后是设置用于模糊模式的Drawable资源。

public class ProgressBar extends View {

    ...

    public void setIndeterminateDrawableTiled(Drawable d) {
        if (d != null) {
            // 平铺和边框预处理。
            d = tileifyIndeterminate(d);
        }

        // 设置用于模糊模式的Drawable资源。
        setIndeterminateDrawable(d);
    }

    ...

}

接着我们来看tileifyIndeterminate(Drawable drawable)方法,它用于预处理模糊模式的Drawable资源,把AnimationDrawable内每一帧的Drawable都进行平铺和边框预处理,之所以这样做是考虑到动画里的每一帧Drawable的宽度可能达不到进度条的长度,所以做了平铺处理,其内部最后也是调用了private Drawable tileify(Drawable drawable, boolean clip)方法。

public class ProgressBar extends View {

    ...

    private Drawable tileifyIndeterminate(Drawable drawable) {
        if (drawable instanceof AnimationDrawable) {
            AnimationDrawable background = (AnimationDrawable) drawable;
            final int N = background.getNumberOfFrames();
            AnimationDrawable newBg = new AnimationDrawable();
            newBg.setOneShot(background.isOneShot());

            // 循环处理每一帧的Drawable。
            for (int i = 0; i < N; i++) {
                Drawable frame = tileify(background.getFrame(i), true);
                frame.setLevel(10000);
                newBg.addFrame(frame, background.getDuration(i));
            }
            newBg.setLevel(10000);
            drawable = newBg;
        }
        return drawable;
    }

    ...

}

最后是setIndeterminateDrawable(Drawable d)方法,它用于设置模糊模式的Drawable资源,内部做了Drawable的新旧替换操作和回调设置。

public class ProgressBar extends View {

    ...

    public void setIndeterminateDrawable(Drawable d) {
        // 判断是否需要更新Drawable资源。
        if (mIndeterminateDrawable != d) {

            // 这里做的具体操作是是移除旧Drawable的回调接口,这里涉及到Drawable和View之间
            // 的回调关系,大概情况是View实现了Drawable.Callback接口,Drawable会将View
            // 作为回调的实现保存起来(弱引用),当需要重新绘制Drawable的时候会触发该回调让
            // View重绘Drawable。
            if (mIndeterminateDrawable != null) {
                mIndeterminateDrawable.setCallback(null);
                unscheduleDrawable(mIndeterminateDrawable);
            }

            // 保存新的Drawable.
            mIndeterminateDrawable = d;

            if (d != null) {
                // 给新的Drawable设置回调实现,可以看到参数是this,也就是当前的View(或者说ProgressBar)。
                d.setCallback(this);
                d.setLayoutDirection(getLayoutDirection());

                // 如果Drawable是可以根据状态改变的,也就是StateListDrawable,
                // 则设置Drawable的最新状态,并且会触发View重新绘制视图。
                if (d.isStateful()) {
                    d.setState(getDrawableState());
                }
                applyIndeterminateTint();
            }

            // 当ProgressBar是模糊模式的时候,就将mCurrentDrawable变成模糊模式的Drawable资源,然后重绘视图。
            if (mIndeterminate) {
                mCurrentDrawable = d;
                postInvalidate();
            }
        }
    }

    ...

}

4.3.3 精确模式的Drawable预处理

public class ProgressBar extends View {

    ...

    public void setProgressDrawableTiled(Drawable d) {
        if (d != null) {
            // 平铺预处理。
            d = tileify(d, false);
        }

        // 设置精确模式的Drawable资源。
        setProgressDrawable(d);
    }

    ...

}

该方法用于设置可平铺的Drawable资源,该Drawable用于精确模式下绘制进度条。如果传入的Drawable资源是BitmapDrawable或者包含BitmapDrawable,该方法会复制它用于平铺显示进度条。最后调用了setProgressDrawable(Drawable d)方法,它和setIndeterminateDrawable(Drawable d)的作用基本是一样的,唯一不同的地方是它还做了刷新进度条的操作。

public class ProgressBar extends View {

    ...

    public void setProgressDrawable(Drawable d) {
        // 判断是否需要更新Drawable。
        if (mProgressDrawable != d) {

            // 这里做的具体操作是是移除旧Drawable的回调接口,这里涉及到Drawable和View之间
            // 的回调关系,大概情况是View实现了Drawable.Callback接口,Drawable会将View
            // 作为回调的实现保存起来(弱引用),当需要重新绘制Drawable的时候会触发该回调让
            // View重绘Drawable。
            if (mProgressDrawable != null) {
                mProgressDrawable.setCallback(null);
                unscheduleDrawable(mProgressDrawable);
            }

            // 保存新的Drawable。
            mProgressDrawable = d;

            if (d != null) {
                // 给新的Drawable设置回调实现,可以看到参数是this,也就是当前的View(或者说ProgressBar)。
                d.setCallback(this);
                d.setLayoutDirection(getLayoutDirection());

                // 如果Drawable是可以根据状态改变的,也就是StateListDrawable,
                // 则设置Drawable的最新状态,并且会触发View重新绘制视图。
                if (d.isStateful()) {
                    d.setState(getDrawableState());
                }

                // 确保ProgressBar有足够的高度来放下Drawable,需要注意的是在初始化
                // ProgressBar的时候,这一步操作是无效的,因为在设置完Drawable资源
                // 之后又会对mMaxHeight进行初始化,详细内容可以看看ProgressBar构
                // 造方法内的调用顺序。
                int drawableHeight = d.getMinimumHeight();
                if (mMaxHeight < drawableHeight) {
                    mMaxHeight = drawableHeight;
                    requestLayout();
                }

                applyProgressTints();
            }

            // 当ProgressBar是精确模式的时候,就将mCurrentDrawable变成模糊模式的Drawable资源,然后重绘视图。
            if (!mIndeterminate) {
                mCurrentDrawable = d;
                postInvalidate();
            }

            // 更新Drawable的绘制区域和状态。
            updateDrawableBounds(getWidth(), getHeight());
            updateDrawableState();

            // 刷新进度值。
            doRefreshProgress(R.id.progress, mProgress, false, false);
            doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false);
        }
    }

    ...

}

4.4 测量视图大小

测量视图的大小是所有View都必须做的一个步骤,它确定自己相对于父容器的大小。此外,每次Drawable有变的时候刷新Drawable的可绘制区域也是保证绘制出来的进度条的正确性的关键。onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法是确定视图大小的核心,它根据当前使用的Drawable大小和ProgressBar的大小限制值来计算出视图的期望大小,然后加入Padding的距离,最后计算出来的视图实际大小是根据视图和父容器的大小共同决定的。

提示:关于视图大小的测量,关键在于onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中的两个参数,这两个参数内部记录了当前视图和父容器的一些相对信息,实际计算的时候是根据这些信息来确定视图大小的,有兴趣的可以网上查下资料,这里就不多说了。

public class ProgressBar extends View {

    ...

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 获取当前使用的Drawable资源,可能是IndeterminateDrawable,也可能是
        // ProgressDrawable,这取决于ProgressBar的模式。
        Drawable d = mCurrentDrawable;

        // 接下来根据宽度和高度的限制,确定期望的视图大小。
        // mMinWidth ≤ dw ≤ mMaxWidth
        // mMinHeight ≤ dh ≤ mMaxHeight
        int dw = 0;
        int dh = 0;
        if (d != null) {
            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
        }

        // 更新Drwable的状态
        updateDrawableState();

        // 整个View的大小包括了Padding部分。
        dw += mPaddingLeft + mPaddingRight;
        dh += mPaddingTop + mPaddingBottom;

        // 这步才是计算View大小的关键所在,具体还要看View内部的resolveSizeAndState()方法,
        // 在最后计算View大小的时候,并不是你希望多大就是多大,这还跟父容器能给的空间大小有关系。
        setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
                resolveSizeAndState(dh, heightMeasureSpec, 0));
    }

    ...

}

onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法里调用了updateDrawableBounds(int w, int h)方法用于更新Drawable的可绘制区域,需要注意的是在onDraw的时候会移动画布把padding给去除掉,所以该方法内部假设原点(0,0)是已经扣除padding的。另外我们还可以看到MirrorForRtl属性的设置,有些国家的习惯是从右往左显示(例如阿拉伯),因此当必要的时候会设置ProgressBar从右往左显示,所以必须将left和right进行镜像变化。

public class ProgressBar extends View {

    ...

    private void updateDrawableBounds(int w, int h) {
        // 扣除padding,剩下的就是Drawable的可以绘制的区域。
        w -= mPaddingRight + mPaddingLeft;
        h -= mPaddingTop + mPaddingBottom;

        int right = w;
        int bottom = h;
        int top = 0;
        int left = 0;

        // 对Drawable资源的宽高进行缩放。
        if (mIndeterminateDrawable != null) {
            // Aspect ratio logic does not apply to AnimationDrawables
            if (mOnlyIndeterminate && !(mIndeterminateDrawable instanceof AnimationDrawable)) {
                final int intrinsicWidth = mIndeterminateDrawable.getIntrinsicWidth();
                final int intrinsicHeight = mIndeterminateDrawable.getIntrinsicHeight();
                final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
                final float boundAspect = (float) w / h;
                if (intrinsicAspect != boundAspect) {
                    if (boundAspect > intrinsicAspect) {
                        // New width is larger. Make it smaller to match height.
                        final int width = (int) (h * intrinsicAspect);
                        left = (w - width) / 2;
                        right = left + width;
                    } else {
                        // New height is larger. Make it smaller to match width.
                        final int height = (int) (w * (1 / intrinsicAspect));
                        top = (h - height) / 2;
                        bottom = top + height;
                    }
                }
            }

            // 有些国家的习惯是从右往左显示(例如阿拉伯),因此当必要的时候会设置ProgressBar从右往左
            // 显示,所以必须将left和right进行镜像变化。
            if (isLayoutRtl() && mMirrorForRtl) {
                int tempLeft = left;
                left = w - right;
                right = w - tempLeft;
            }

            // 设置模糊模式下的Drawable的绘制区域。
            mIndeterminateDrawable.setBounds(left, top, right, bottom);
        }

        // 设置精确模式下的Drawable的绘制区域。
        if (mProgressDrawable != null) {
            mProgressDrawable.setBounds(0, 0, right, bottom);
        }
    }

    ...

}

4.5 精确模式的相关设置

绘制精确模式的进度条分为如下5个步骤:

  1. 设置进度值,这是动态变更进度值的第一步。

  2. 判断是否异步更新,因为变更进度值可以是在主线程触发,也可以在其他线程中,所以需要先判断是不是异步更新。

  3. 如果是异步更新,则封装更新数据。

  4. 如果是异步更新,则向主线程消息队列中发送进度条更新的消息。

  5. 计算当前进度值和最大进度值的比例,并根据比例绘制出进度条。

4.5.1 设置进度值

精确模式进度条是根据进度值绘制的,所以第一步自然就是设置当前的进度值和最大值,之后不断更新当前进度值即可。首先我们来看如何设置进度条的最大值,内部做了一些容错处理,并且在最后刷新了进度条。

public class ProgressBar extends View {

    ...

    public synchronized void setMax(int max) {
        // 在设置进度条最大值的时候会做一些容错处理。
        if (max < 0) {
            max = 0;
        }
        if (max != mMax) {
            mMax = max;
            postInvalidate();

            // 这里可以看出当进度值大于max的时候,会直接设置进度值为max。
            if (mProgress > max) {
                mProgress = max;
            }

            // 刷新进度
            refreshProgress(R.id.progress, mProgress, false);
        }
    }

    ...

}

下面是设置第一进度条当前的进度值的方法,我们调用setProgress(int progress)的时候,最终也是调用该方法,并设置fromUser = false,在刷新进度值的时候传入了R.id.progress标识刷新的是第一进度条。

public class ProgressBar extends View {

    ...

    synchronized void setProgress(int progress, boolean fromUser) {
        if (mIndeterminate) {
            return;
        }

        if (progress < 0) {
            progress = 0;
        }

        if (progress > mMax) {
            progress = mMax;
        }

        if (progress != mProgress) {
            mProgress = progress;
            // 这里传入R.id.progress标识刷新的是第一个进度条。
            refreshProgress(R.id.progress, mProgress, fromUser);
        }
    }

    ...

}

下面是设置第二进度条当前的进度值的方法,在刷新进度值的时候传入了R.id.secondaryProgress标识刷新的是第一进度条。

public class ProgressBar extends View {

    ...

    public synchronized void setSecondaryProgress(int secondaryProgress) {
        if (mIndeterminate) {
            return;
        }

        if (secondaryProgress < 0) {
            secondaryProgress = 0;
        }

        if (secondaryProgress > mMax) {
            secondaryProgress = mMax;
        }

        if (secondaryProgress != mSecondaryProgress) {
            mSecondaryProgress = secondaryProgress;
            // 这里传入R.id.secondaryProgress标识刷新的是第二个进度条。
            refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false);
        }
    }

    ...

}

4.5.2 判断是否异步更新

进度条的异步更新是十分常见的,所以ProgressBar内部对异步更新做了封装操作,每次进度值有变化的时候都会根据是否异步更新做相应的处理。前面我们已经看到了,在设置进度值的时候,最后都会调用refreshProgress(...)方法刷新进度条。在初始化进度条的时候记录了主线程的ID,然后在该方法内部根据当前线程的ID是否是主线程ID来决定是否异步更新进度条。主线程刷新进度条很简单,直接调用private synchronized void doRefreshProgress(...)方法刷新即可,而异步更新进度条则要复杂得多,首先要封装更新数据然后向主线程消息队列中发送更新消息等待进度条被更新。我们来看下面的源码:

public class ProgressBar extends View {

    ...

    private synchronized void refreshProgress(int id, float progress, boolean fromUser,
                                              boolean animate) {
        // 判断刷新进度条的请求是不是在主线程,如果是则直接刷新,否则通过线程消息机制异步更新进度条。
        if (mUiThreadId == Thread.currentThread().getId()) {
            // 主线程同步更新进度条。
            doRefreshProgress(id, progress, fromUser, true, animate);
        } else {
            // 异步更新进度条。

            // 创建Runnable封装刷新逻辑。
            if (mRefreshProgressRunnable == null) {
                mRefreshProgressRunnable = new RefreshProgressRunnable();
            }

            // 封装刷新数据。
            final RefreshData rd = RefreshData.obtain(id, progress, fromUser, animate);
            mRefreshData.add(rd);
            if (mAttached && !mRefreshIsPosted) {
                post(mRefreshProgressRunnable);
                mRefreshIsPosted = true;
            }
        }
    }

    ...

}

4.5.3 封装刷新数据

当采用异步方式更新进度条的时候,我们需要将更新的数据信息封装起来以便发送到主线程消息队列中等待更新。RefreshData类用于保存更新进度值时所需要的数据,因为异步更新进度条的时候,实际上是通过消息队列来完成的,所以需要通过该类来创建封装数据的对象,并且在队列中等待被使用。另外值得注意的是RefreshData还使用了对象池来复用已经创建的对象,避免频繁更新进度值的时候创建出大量的对象。

public class ProgressBar extends View {

    ...

    /**
     * 该类用于保存更新进度值时所需要的数据,因为我们在更新进度值的时候,实际上是通过消息队列来完成的,所以
     * 需要通过该类来创建封装数据的对象,并且在队列中等待被使用。另外该类还使用了对象池来复用已经创建的对象,
     * 避免频繁更新进度值的时候创建出大量的对象。
     */
    private static class RefreshData {
        private static final int POOL_MAX = 24;// 对象池最多保留24个对象
        private static final SynchronizedPool<RefreshData> sPool = new SynchronizedPool<RefreshData>(POOL_MAX);// 对象池

        public int id;// 更具ID判断是更新第一进度值还是第二进度值
        public float progress;// 进度值
        public boolean fromUser;
        public boolean animate;

        public static RefreshData obtain(int id, float progress, boolean fromUser,
                                         boolean animate) {
            RefreshData rd = sPool.acquire();
            if (rd == null) {
                rd = new RefreshData();
            }
            rd.id = id;
            rd.progress = progress;
            rd.fromUser = fromUser;
            rd.animate = animate;
            return rd;
        }

        public void recycle() {
            sPool.release(this);
        }
    }

    ...

}

4.5.4 发送更新消息

封装完更新数据之后,需要一个Runnable对象用于定义异步更新逻辑。该Runnable用于向消息队列中发送更新进度值的消息,也就是说我们在设置进度值的时候并不是马上就更新的,而是封装成Runnable在消息队列中等待更新进度值。

public class ProgressBar extends View {

    ...

    private class RefreshProgressRunnable implements Runnable {
        public void run() {
            synchronized (ProgressBar.this) {
                final int count = mRefreshData.size();
                for (int i = 0; i < count; i++) {
                    final RefreshData rd = mRefreshData.get(i);
                    doRefreshProgress(rd.id, rd.progress, rd.fromUser, true, rd.animate);
                    rd.recycle();
                }
                mRefreshData.clear();
                mRefreshIsPosted = false;
            }
        }
    }

    ...

}

4.5.5 更新进度条

最后一步就是更新进度条了,这一步无论是主线程同步更新还是异步更新都是最后执行的核心步骤。下面是刷新进度条的核心方法,要了解它的实现原理,我们还要先简单介绍下ClipDrawable。ClipDrawable可以根据Level值改变显示范围,例如ClipDrawable的长度为100px,Level的最大值是10000,当我们设置Level = 5000的时候,它就只会显示50%的图像内容,当设置Level = 10000的时候就完全显示。通过ClipDrawable的这个特性,我们就可以先计算出进度比例,然后设置Level值来改变进度条的长度。

public class ProgressBar extends View {

    ...

    private synchronized void doRefreshProgress(int id, float progress, boolean fromUser, boolean callBackToApp, boolean animate) {
        // 获取进度值和最大值的比例。
        float scale = getScale(progress);

        final Drawable d = mCurrentDrawable;
        if (d != null) {
            Drawable progressDrawable = null;

            if (d instanceof LayerDrawable) {
                // 如果Drawable资源是LayerDrawable,则根据id获取内部的Drawable资源,
                // 可选id有android.id.progress或android.id.secondaryProgress。
                progressDrawable = ((LayerDrawable) d).findDrawableByLayerId(id);

                if (progressDrawable != null && canResolveLayoutDirection()) {
                    progressDrawable.setLayoutDirection(getLayoutDirection());
                }
            }

            // 设置进度条变化的核心代码,通过进度值和最大值的比例来设置Drawable资源的Level。
            final int level = (int) (scale * MAX_LEVEL);
            (progressDrawable != null ? progressDrawable : d).setLevel(level);
        } else {
            invalidate();
        }

        // 回调一些方法。
        if (id == R.id.progress) {
            if (animate) {
                onAnimatePosition(scale, fromUser);
            } else if (callBackToApp) {
                onProgressRefresh(scale, fromUser);
            }
        }
    }

    ...

}

更新进度条的时候会调用getScale(flaot progress)方法计算当前进度值和最大进度值的比例,最后通过该比例设置Drawable的Level值来显示不同长度的进度条,看下下面的代码:

public class ProgressBar extends View {

    ...

    private float getScale(float progress) {
        return mMax > 0 ? progress / (float) mMax : 0;
    }

    ...

}

4.6 模糊模式的相关设置

绘制模糊模式的进度条时,主要做的就是动画播放,对于AnimationDrawable来说,动画的每一帧都是由用户决定的,所以无需做修改直接播放动画即可。对于其他Drawable类型,我们首先要知道的是所有的Drawable都有一个Level值,不同的Drawable对Level值的变化具有不同的响应,例如之前提到的ClipDrawable根据Level变化显示的区域、RotateDrawable根据Level变化角度和ScaleDrawable根据Level变化大小等,但是这些Drawable的变化是需要我们不断动态设置Level值来达到动画效果的,所以ProgressBar巧妙地利用了AlphaAnimation的alpha的变化(0~1.0)来设置Level值,从而实现了动画效果。具体的绘制步骤如下:

  1. 判断Drawable类型,进度条将模糊模式使用的Drawable分为两类,分别是AnimationDrawable和其他Drawable,根据这两种类型会做不同的动画播放处理。

  2. 如果是AnimationDrawable类型直接播放动画。

  3. 其他Drawable类型则使用默认的AlphaAnimation动画的alpha值来控制Drawable的Level值,从而实现不同的动画。

模糊模式所需要做的就是播放动画,该方法根据Drawbale的分类做了不同的处理,当Drawable不是AnimationDrawable的时候,就会创建AlphaAnimation和LinearInterpolator来实现Drawable的Level值根据alpha值(0~1.0)不断变化,从而实现不同动画效果。看下面的源码:

public class ProgressBar extends View {

    ...

    void startAnimation() {
        // 进度条不可见的时候就没必要启动动画。
        if (getVisibility() != VISIBLE) {
            return;
        }

        // 这里需要判断Drawable资源是否是可以播放动画(AnimationDrawable)。
        if (mIndeterminateDrawable instanceof Animatable) {
            // 如果Drawable资源是一个可以自动播放动画的资源,则设置标识为“可播放”。
            mShouldStartAnimationDrawable = true;
            // 设置ProgressBar不具有自己的动画属性。
            mHasAnimation = false;
        } else {
            // 如果Drawable资源不具有播放动画的能力,那么我们需要自己配置一些动画属性来播放动画。
            mHasAnimation = true;

            // 设置默认的插值器为线性插值器,你也自己指定插值器来播放不同加速率的动画,默认是匀速。
            if (mInterpolator == null) {
                mInterpolator = new LinearInterpolator();
            }

            // 初始化保存动画变化值对象,该对象保存了某一个时刻动画的属性值。
            if (mTransformation == null) {
                mTransformation = new Transformation();
            } else {
                mTransformation.clear();
            }

            // 创建AlphaAnimation动画,从而利用不断变化的alpha值来改变Drawable的Level值,这确实是一个很巧妙的用法。
            if (mAnimation == null) {
                mAnimation = new AlphaAnimation(0.0f, 1.0f);
            } else {
                mAnimation.reset();
            }

            // 设置动画的重复播放的行为。
            mAnimation.setRepeatMode(mBehavior);
            // 设置动画为无限循环播放。
            mAnimation.setRepeatCount(Animation.INFINITE);
            // 设置动画持续时间。
            mAnimation.setDuration(mDuration);
            // 设置插值器。
            mAnimation.setInterpolator(mInterpolator);
            // 设置动画从第一帧开始播放。
            mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);
        }
        postInvalidate();
    }

    ...

}

4.7 绘制进度条

前面所有的操作都是在绘制进度条之前的相关设置(模糊模式和精确模式),真正绘制进度条的地方当然是在onDraw(Canvas canvas)方法里,内部调用了drawTrack(Canvas canvas)核心方法来绘制进度条。看下下面的源码:

public class ProgressBar extends View {

    ...

    void drawTrack(Canvas canvas) {
        final Drawable d = mCurrentDrawable;
        if (d != null) {
            final int saveCount = canvas.save();

            // 当需要让ProgressBar从右往左显示的时候,对画布进行对称翻转并移动画布到Padding指定的位置,
            // 需要注意的是MirrorForRtl模式下Padding和正常情况是相反的。
            if (isLayoutRtl() && mMirrorForRtl) {
                canvas.translate(getWidth() - mPaddingRight, mPaddingTop);
                canvas.scale(-1.0f, 1.0f);
            } else {
                canvas.translate(mPaddingLeft, mPaddingTop);
            }

            // 对于除了AnimationDrawable之外的其他Drawable类型,我们首先要知道的是所有的Drawable都有一个Level值,
            // 不同的Drawable对Level值的变化具有不同的响应,例如之前提到的ClipDrawable根据Level变化显示的区域、
            // RotateDrawable根据Level变化角度和ScaleDrawable根据Level变化大小等,但是这些Drawable的变化是需要
            // 我们不断动态设置Level值来达到动画效果的,所以ProgressBar巧妙地利用了`AlphaAnimation`的alpha的变化
            // (0~1.0)来设置Level值,从而实现了动画效果。
            final long time = getDrawingTime();
            if (mHasAnimation) {
                mAnimation.getTransformation(time, mTransformation);
                final float scale = mTransformation.getAlpha();
                try {
                    mInDrawing = true;
                    d.setLevel((int) (scale * MAX_LEVEL));
                } finally {
                    mInDrawing = false;
                }

                // 当到达绘制下一帧的时候会触发ProgressBar的onDraw(Canvas canvas) 方法重新绘制新的一帧。
                postInvalidateOnAnimation();
            }

            // 绘制Drawable,并将画布复位。
            d.draw(canvas);
            canvas.restoreToCount(saveCount);

            // 如果ProgressBar处于模糊状态且是AnimationDrawable,则直接播放动画。
            if (mShouldStartAnimationDrawable && d instanceof Animatable) {
                ((Animatable) d).start();
                mShouldStartAnimationDrawable = false;
            }
        }
    }

    ...

}

对于模糊模式进度条,该方法的作用是不断重绘每一帧的画布实现动画效果;对于精确模式进度条,当进度值发生变化的时候,该方法的作用就是重绘进度条长度。

4.8 再谈Drawable的Level属性

由于ProgressBar大量利用了Drawable的Level属性来改变自身的形态(动画或进度条长度),所以这一节就再来分析下Level属性是如何改变ProgressBar形态的。

首先ProgressBar实现了Drawable.Callback接口,该接口的invalidateDrawable(Drawable who)方法会在Drawable的Level发生变化的时候被回调,这时候ProgressBar就调用invalidate(int l, int t, int r, int b)清除之前一帧的Drawable图像,然后重绘新一帧的Drawable,就这样不断变化Level触发回调来重绘Drawable,从而实现进度条更新或者动画效果。

**提示:**invalidate(int l, int t, int r, int b)方法会触发onDraw(Canvas canvas)方法重绘画布。

下面的代码是Drawable.Callback接口的回调方法,清除前一帧的Drawable,重绘新一帧的Drawable。

public class ProgressBar extends View {

    ...

    @Override
    public void invalidateDrawable(Drawable dr) {
        // 该操作必须在Drawable不处于绘制状态的时候,也就说当onDraw正在绘制该Drawable的时候,是不允许清除它的。
        if (!mInDrawing) {
            if (verifyDrawable(dr)) {
                // 获取Drawable所在的区域并清除。
                final Rect dirty = dr.getBounds();
                final int scrollX = mScrollX + mPaddingLeft;
                final int scrollY = mScrollY + mPaddingTop;

                invalidate(dirty.left + scrollX, dirty.top + scrollY,
                        dirty.right + scrollX, dirty.bottom + scrollY);
            } else {
                super.invalidateDrawable(dr);
            }
        }
    }

    ...

}

现在有个问题,对于精确模式进度条,是通过用户主动设置进度值触发更新的,那么对于模糊模式的进度条动画是如何循环更新每一帧从而达到动画效果的呢?其实很简单,细心的人可能在前面的drawTrack(Canvas canvas)方法分析中看到了postInvalidateOnAnimation()这个方法,这就是不断更新每一帧的关键所在,它会在系统准备绘制下一帧画面的时候触发onDraw(Canvas canvas)方法重绘画布,然后在drawTrack(Canvas canvas)方法中又再次调用postInvalidateOnAnimation()安排下一帧的绘制,从实现了不断绘制每一帧的动画效果。

5 ProgressBar自定义

经过前面对源码的分析之后,我们来谈谈如何自定义ProgressBar,根据前面的分析,我们大致可以将自定义ProgressBar分为以下几个步骤:

  1. 确定ProgressBar的模式(精确模式或模糊模式)。

  2. 定义ProgressBar的Drawable资源(核心)。

  3. 定义动画持续时间、动画重复方式以及动画的变化率(适用于模糊模式)。

5.1 可用的Drawable资源

首先我们来看下Drawable内部的两个方法,分别是public final boolean setLevel(int level)protected boolean onLevelChange(int level)两个方法,简单分析这两个方法之后我们就能很容易地找出所有可用于ProgressBar的Drawable了。我们来看下面的代码片段:

public class Drawable {

    ...

    public final boolean setLevel(int level) {
        if (mLevel != level) {
            mLevel = level;
            return onLevelChange(level);
        }
        return false;
    }

    protected boolean onLevelChange(int level) { 
        return false; 
    }

    ...

}

从代码中我们可以看出,但我们调用setLevel(int level)方法设置Level值时,方法内部会回调onLevelChange(int level)方法,而该方法默认返回false,它是预留给Drawable的子类来实现的,也就说当一个Drawable的子类希望自己能够根据Level值的变化做出响应时就必须重写该方法,再加上ProgressBar是通过改变Drawable的Level属性来改变自身形态的,我们就很容易得出这样一个结论:【所有重写了protected boolean onLevelChange(int level)方法的Drawable子类都可以作为ProgressBar的Drawable资源。】

5.2 自定义精确进度条

接下来我们将介绍如何自定义精确模式的进度条,首先我们会先分析下系统自带进度条是如何定义,然后依葫芦画瓢自己定义不同样式的进度条。

5.2.1 系统自带进度条

<style name="Widget.ProgressBar.Horizontal">
    <item name="indeterminateOnly">false</item>
    <item name="progressDrawable">@drawable/progress_horizontal</item>
    <item name="indeterminateDrawable">@drawable/progress_indeterminate_horizontal</item>
    <item name="minHeight">20dip</item>
    <item name="maxHeight">20dip</item>
    <item name="mirrorForRtl">true</item>
</style>

这段样式是系统自带的横向进度条的定义内容,我们可以看出它支持横向的精确模式进度条和模糊模式进度条,ProgressBar根据当前模式选择使用不同的Drawable资源:

  • 精确模式:@drawable/progress_horizontal

  • 模糊模式:@drawable/progress_indeterminate_horizontal

从样式可以看出,要自定义精确模式的进度条,关键就在于@drawable/progress_horizontal的定义,所以接下来我们来看看这个文件里面都写了什么。

<?xml version="1.0" encoding="utf-8"?>

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 定义ProgressBar的背景 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                    android:startColor="#ff9d9e9d"
                    android:centerColor="#ff5a5d5a"
                    android:centerY="0.75"
                    android:endColor="#ff747674"
                    android:angle="270"/>
        </shape>
    </item>

    <!-- 定义ProgressBar第二进度条的资源 -->
    <item android:id="@android:id/secondaryProgress">
        <!-- 将ShapeDrawable转换成ClipDrawable -->
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                        android:startColor="#80ffd300"
                        android:centerColor="#80ffb600"
                        android:centerY="0.75"
                        android:endColor="#a0ffcb00"
                        android:angle="270"/>
            </shape>
        </clip>
    </item>

    <!-- 定义ProgressBar第一进度条的资源 -->
    <item android:id="@android:id/progress">
        <!-- 将ShapeDrawable转换成ClipDrawable -->
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                        android:startColor="#ffffd300"
                        android:centerColor="#ffffb600"
                        android:centerY="0.75"
                        android:endColor="#ffffcb00"
                        android:angle="270"/>
            </shape>
        </clip>
    </item>

</layer-list>

从样式的定义中可以看出,系统自带的精确模式进度条其实就是定义简单的ShapeDrawable,然后转换成ClipDrawable,而ClipDrawable默认是横向剪裁的。这里之所以要自己用<clip>将ShapeDrawable转换成ClipDrawable,是因为ProgressBar只会对BitmapDrawable进行平铺预处理,并转换成ClipDrawable,而ShapeDrawable则不会做任何操作。

5.2.2 横向渐变进度条

这是一个横向渐变的进度条,随着进度值的增加,颜色也会越变越深。这里只给出了主要文件的代码定义,其他的用法和前面介绍的进度条用法一样。

**1. 自定义Drawable资源:**res/drawable/progress_horizontal_simple_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                android:angle="270"
                android:centerColor="#ff5a5d5a"
                android:centerY="0.75"
                android:endColor="#ff747674"
                android:startColor="#ff9d9e9d" />
        </shape>
    </item>

    <item android:id="@android:id/secondaryProgress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:angle="0"
                    android:endColor="#a0ffcb00"
                    android:startColor="#80ffd300" />
            </shape>
        </clip>
    </item>

    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:angle="0"
                    android:endColor="#ffff4b00"
                    android:startColor="#ffffd300" />
            </shape>
        </clip>
    </item>

</layer-list>

**2. 使用自定义Drawable资源:**res/layout/activity_main.xml

<ProgressBar
    style="@android:style/Widget.ProgressBar.Horizontal"
    android:id="@+id/pb_horizontal_gradient_simple_shape"
    android:layout_width="275dp"
    android:layout_height="20dp"
    android:layout_margin="20dp"
    android:indeterminateOnly="false"
    android:max="100"
    android:progress="70"
    android:progressDrawable="@drawable/progress_horizontal_simple_drawable" />

5.2.3 垂直渐变进度条

这是一个垂直渐变的进度条,随着进度值的增加,颜色也会越变越深,和上面横向渐变进度条的实现基本相同,唯一的区别就是指定了ClipDrawable的剪裁方向和ShapeDrawable的颜色渐变方向。

**1. 自定义Drawable资源:**res/drawable/progress_vertical_gradient_simple_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                android:angle="270"
                android:centerColor="#ff5a5d5a"
                android:centerY="0.75"
                android:endColor="#ff747674"
                android:startColor="#ff9d9e9d" />
        </shape>
    </item>

    <item android:id="@android:id/secondaryProgress">
        <clip
            android:clipOrientation="vertical"
            android:gravity="bottom">
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:angle="90"
                    android:endColor="#a0ffcb00"
                    android:startColor="#80ffd300" />
            </shape>
        </clip>
    </item>

    <item android:id="@android:id/progress">
        <!-- 定义ClipDrawable的剪裁方向为垂直 -->
        <clip
            android:clipOrientation="vertical"
            android:gravity="bottom">
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:angle="90"
                    android:endColor="#ffff4b00"
                    android:startColor="#ffffd300" />
            </shape>
        </clip>
    </item>

</layer-list>

**2. 使用Drawable资源:**res/layout/activity_main.xml

<ProgressBar
    android:id="@+id/pb_vertical_simple_shape"
    android:layout_width="20dp"
    android:layout_height="150dp"
    android:layout_margin="20dp"
    android:indeterminateOnly="false"
    android:max="100"
    android:progress="70"
    android:progressDrawable="@drawable/progress_vertical_gradient_simple_drawable" />

5.2.4 横向文字进度条

这是一个以填充Loading文字的方式实现的横向进度条,其实现原理也很简单,将ProgressBar的背景设置成未填充的Loading文字,将第一进度条的Drawable设置成已填充的Loading文字,然后利用ClipDrawable来根据进度值剪裁第一进度条。另外,因为我们使用的图片资源,也就是BitmapDrawable,所以ProgressBar会对它进行预处理,我们只需简单指定图片资源即可。

  • 未填充的Loading文字图片

  • 已填充的Loading文字图片

**1. 自定义Drawable资源:**res/drawable/progress_loading_text.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@android:id/background"
        android:drawable="@drawable/loading_text_bg" />

    <item
        android:id="@android:id/progress"
        android:drawable="@drawable/loading_text_progress" />

</layer-list>

**2. 使用Drawable资源:**res/layout/activity_main.xml

<ProgressBar
    android:id="@+id/pb_loading_text"
    android:layout_width="275dp"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:indeterminateOnly="false"
    android:max="100"
    android:progress="70"
    android:progressDrawable="@drawable/progress_loading_text" />
 ```

### 5.2.5 横向星星进度条

 <center><img src="Picture/ProgressBar13.gif" /></center>

 这个进度条是利用ProgressBar对BitmapDrawable的横向平铺处理实现了由多个星星组成的进度条。

* 星星图标素材

  <img src="Picture/ProgressBar17.png" width="50" />

**1. 自定义Drawable资源:**src/drawable/progress_horizontal_stars.xml

```xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#ffa8a8a8" />
        </shape>
    </item>

    <item
        android:id="@android:id/progress"
        android:drawable="@drawable/ic_star" />

</layer-list>




<div class="se-preview-section-delimiter"></div>

**2. 使用Drawable资源:**src/layout/activity_main.xml

<ProgressBar
    android:id="@+id/pb_horizontal_stars"
    android:layout_width="275dp"
    android:layout_height="33dp"
    android:layout_margin="20dp"
    android:indeterminateOnly="false"
    android:max="100"
    android:progress="70"
    android:progressDrawable="@drawable/progress_horizontal_stars" />




<div class="se-preview-section-delimiter"></div>

5.2.6 垂直星星进度条

这个进度条是在垂直方向上显示由N个星星组成的进度条,它利用<bitmap>标签封装星星图标,设置成垂直平铺模式,然后使用<clip>标签将Drawable资源转换成可以垂直剪裁的图像。我们并没有利用ProgressBar自带的预处理功能,因为ProgressBar的预处理是进行横向平铺,这并不符合我们垂直平铺的要求,所以我们需要自己定义Drawable资源的处理方式。

**1. 自定义Drawable资源:**scr/drawable/progress_vertical_stars.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#ffa8a8a8" />
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <clip
            android:clipOrientation="vertical"
            android:gravity="bottom">

            <bitmap
                android:tileMode="repeat"
                android:src="@drawable/ic_star" />

        </clip>
    </item>

</layer-list>




<div class="se-preview-section-delimiter"></div>

**2. 使用Drawable资源:**src/layout/activity_main.xml

<ProgressBar
    android:id="@+id/pb_vertical_stars"
    android:layout_width="33dp"
    android:layout_height="150dp"
    android:layout_margin="20dp"
    android:indeterminateOnly="false"
    android:max="100"
    android:progress="70"
    android:progressDrawable="@drawable/progress_vertical_stars" />
 ```





<div class="se-preview-section-delimiter"></div>

### 5.2.7 小结

自定义精确模式的进度条时,我们需要使用`<layer-list>`定义三个Drawable资源,并且设置每个Drawable资源的id用于指定ProgressBar的背景、第一进度条和第二进度条:

* `android:id="@android:id/background"`指定ProgressBar的背景资源。

* `android:id="@android:id/progress"`指定ProgressBar第一进度条的Drawable资源。

* `android:id="@android:id/secondaryProgress"`指定ProgressBar第二进度条的Drawable资源。

另外根据ProgressBar对Drawable资源的预处理原则(只处理BitmapDrawable),我们需要对不同的Drawable资源做不同的处理,例如是否剪裁、是否平铺等。





<div class="se-preview-section-delimiter"></div>

## 5.3 自定义模糊进度条

前面分析过,任何能对Level属性变化做出响应的Drawable资源都可以用于模糊进度条,并且我们知道ProgressBar对模糊进度条动画的处理分为两种:

1. AnimationDrawable资源,ProgressBar会直接播放动画,不做任何处理。

2. 其他Drawable资源,ProgressBar会利用AlphaAnimation不断变化的alpha值来改变Drawable的Level属性来播放不同动画。

所以自定义模糊进度条我们可以分为两种,一种是使用AnimationDrawable实现逐帧动画的进度条,另一种是利用Level属性变化来播放不同动画的进度条。





<div class="se-preview-section-delimiter"></div>

### 5.3.1 奔跑的小人

<center><img src="Picture/ProgressBar6.gif" /></center>

该模糊进度条的实现是利用AnimationDrawable来实现的,动画的每一帧都对应一张图片,并且我们需要指定每一帧的持续时间,利用AnimationDrawable我们可以实现一些很复杂的动画效果。

* AnimationDrawable每一帧的图片

  <img src="Picture/ProgressBar18.png" width="100" />
  <img src="Picture/ProgressBar19.png" width="100" />
  <img src="Picture/ProgressBar20.png" width="100" />
  <img src="Picture/ProgressBar21.png" width="100" />
  <img src="Picture/ProgressBar22.png" width="100" />
  <img src="Picture/ProgressBar23.png" width="100" />

**1. 自定义Drawable资源:**src/drawable/progress_indeterminate_running.xml





<div class="se-preview-section-delimiter"></div>

```xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/progress_indeterminate_running1" android:duration="80" />
    <item android:drawable="@drawable/progress_indeterminate_running2" android:duration="80" />
    <item android:drawable="@drawable/progress_indeterminate_running3" android:duration="80" />
    <item android:drawable="@drawable/progress_indeterminate_running4" android:duration="80" />
    <item android:drawable="@drawable/progress_indeterminate_running5" android:duration="80" />
    <item android:drawable="@drawable/progress_indeterminate_running6" android:duration="80" />
</animation-list>

**2. 使用Drawable资源:**src/layout/activity_main.xml

<ProgressBar
    android:layout_width="311dp"
    android:layout_height="262dp"
    android:layout_margin="20dp"
    android:indeterminate="true"
    android:indeterminateDrawable="@drawable/progress_indeterminate_running" />

5.3.2 旋转的齿轮

这个进度条估计是最常见的了,一个图片在不断的旋转,其实现原理就是利用RotateDrawable会根据Level属性的变化不断旋转。

  • 齿轮图片

**1. 定义Drawable资源:**src/drawable/progress_indeterminate_rotate_gear.xml

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:pivotX="50%"
    android:pivotY="50%"
    android:drawable="@drawable/progress_indeterminate_gear1"/>

**2. 使用Drawable资源:**src/layout/activity_main.xml

<ProgressBar
    android:layout_margin="20dp"
    android:indeterminateDrawable="@drawable/progress_indeterminate_rotate_gear"
    android:indeterminate="true"
    android:indeterminateDuration="3000"
    android:indeterminateBehavior="repeat"
    android:layout_width="200dp"
    android:layout_height="200dp" />

5.3.3 两个反转的齿轮

这个进度条的实现,是利用LayerDrawable会将Level值传递给内部的每一个Drawable资源,然后我们在内部定义了两个旋转方向相反的RotateDrawable。

  • 两个齿轮图片


**1. 定义Drawable资源:**scr/drawable/progress_indeterminate_rotate_double_gears.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
            android:drawable="@drawable/progress_indeterminate_gear2"
            android:pivotX="50%"
            android:pivotY="50%"
            android:fromDegrees="0"
            android:toDegrees="360" />
    </item>

    <item
        android:top="80dp"
        android:bottom="80dp"
        android:left="80dp"
        android:right="80dp">

        <rotate
            android:drawable="@drawable/progress_indeterminate_gear3"
            android:pivotX="50%"
            android:pivotY="50%"
            android:fromDegrees="0"
            android:toDegrees="-360" />
    </item>
</layer-list>

**2. 使用Drawable资源:**src/layout/activity_main.xml

<ProgressBar
    android:layout_margin="20dp"
    android:indeterminateDrawable="@drawable/progress_indeterminate_roate_double_gears"
    android:indeterminate="true"
    android:indeterminateDuration="3000"
    android:indeterminateBehavior="repeat"
    android:layout_width="256dp"
    android:layout_height="256dp" />

5.3.4 小结

定义模糊模式进度条的步骤如下:

  1. 任何对Level属性做出响应的Drawable资源都可以实现不同的动画效果。

  2. 根据动画的复杂程度选择使用AnimationDrawable还是其他能对Level属性变化做出响应的Drawable,使用AnimationDrawable可以实现很复杂的动画效果,但是却需要为每一帧动画指定一张图片,这需要占用更多的空间,而使用其他Drawable则简单的几张图片就可以实现不同的动画效果,但是无法应对过于复杂的动画。

  3. 可以考虑使用LayerDrawable来组合多个Drawable,实现组合动画。

  4. 对Level属性的变化能做出响应的Drawable有ScaleDrawable、ClipDrawable、RotateDrawable、LayerDrawable等,可以翻阅API文档查看Drawable的所有子类。

6 总结

至此,对于ProgressBar的深入分析已经结束,最后总结出几个ProgressBar值得学习的地方:

  1. AlphaAnimation的巧妙用法

    对于动画的实现,ProgressBar巧妙地利用了AlphaAnimation不断变化的Alpha值来改变Drawable的Level属性,从而实现不同Drawable具有不同动画效果的功能,这一点应该说是ProgressBar的精华所在,动画不仅仅可以用来播放,也可以用来动态改变属性,这让我看到了PropertyAnimation的影子。我们可以利用AlphaAnimation的Alpha属性来模拟属性动画中的ValueAnimator,从而在3.0以下实现类似属性动画的效果,这个技巧在实现兼容低版本的View动画的时候很有用。

  2. 面向Drawable,而不是Canvas

    从Progressbar的实现方式上,我们可以发现它一直是利用Drawable来实现视图的,而不是使用Canvas来绘制图形,这样让ProgressBar的具有十分强的通用性和可扩展性,我们在自定义View的时候也应该多考虑使用Drawable资源,而不是一味地自己绘图。

  3. SynchronizedPool对象池的使用

    ProgressBar在频繁更新进度值的时候使用了对象池来回收资源,这样避免创建过多的对象,最大限度上减少了资源的消耗。

阅读更多
版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/hjpdyxhjd/article/details/50365723
个人分类: Android
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭