美文网首页自定义View系列自定义控件自定义view相关
android:自定义绚丽的加载进度动画

android:自定义绚丽的加载进度动画

作者: 卜可 | 来源:发表于2016-06-23 10:12 被阅读2131次

转载请注明出处

1、前言

在我们的项目中,很多场景需要使用加载进度动画,如网络请求,数据加载等。

现在市面大多数app都有拥有自己独特风格的加载动画,而不是谷歌为我们提供的菊花圈。一个绚丽美观的加载动画可以消除用户的等待焦虑。本文主要介绍利用自定义view打造一个绚丽的加载动画。

先看效果图:


2、动画分析

从效果图中,我们可以把整个加载动画拆分成以下4个功能点:

  1. 画指定数目的环绕圆环
  2. 圆环旋转动画
  3. 旋转过程圆环聚拢
  4. 旋转过程圆环收缩

3、代码实现

3.1 自定义属性

我们需要自定义俩个属性:圆点个数dot_count、圆点颜色dot_color
代码如下:

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

    <declare-styleable name="ProgressView">
        <attr name="dot_color" format="color"/>
        <attr name="dot_count" format="integer"/>
    </declare-styleable>

</resources>

3.2 获取布局文件中设置好的自定义属性

我们需要在java代码中获取在xml布局文件中设置的自定义属性:

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

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
        mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
        mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
        ta.recycle();

    }

我们的布局属性全部储存在构造器的attrs中,通过context.obtainStyledAttributes(attrs, R.styleable.ProgressView)方法即可获取到设置的自定义属性,记得获取完成后调用recycle()回收资源.

3、 初始化

在我们的自定义view ProgressView的构造器中进行初始化工作。

       mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.FILL_AND_STROKE);
        mPaint.setColor(mDotColor);

        // 屏幕适配,转化圆环半径,小点半径
        mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
        mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
        mOriginalDotRadius = mDotRadius;

初始化画笔,设置颜色,抗锯齿,通过setStyle(Style.FILL_AND_STROKE)设置画笔实心。

设置小圆离中心的距离mRingRadius,小圆半径mDotRadius,因为动画工程中小圆半径会变化,所以用一个变量mOriginalDotRadius来保存小圆半径的初始值(用来计算变化中的小圆半径)。

其中DensityUtils.dp2px()方法是根据屏幕像素密度将像dp值转化为像素。
具体代码:

public static int dp2px(Context context, float dp)
    {
        return (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 
                        dp, 
                        context.getResources().getDisplayMetrics());
    }

初始化动画

    private void initAnimatior()
    {
        mAnimator = ValueAnimator.ofInt(0, 359);
        mAnimator.setDuration(4000);
        mAnimator.setRepeatCount(-1);
        mAnimator.setRepeatMode(ValueAnimator.INFINITE);
        mAnimator.setInterpolator(new LinearInterpolator());
    }

我们用ValueAnimator来动态计算当前旋转角度mCurrentAngle,变化值从0到359更换,其他设置都很简单,比如设置动画时间,重复次数-1(表示无限循环),注意这行代码mAnimator.setInterpolator(new LinearInterpolator()),设置动画差值器为线性匀速,这个值改变后会改变动画效果

   mAnimator.addUpdateListener(new AnimatorUpdateListener()
        {
            @Override
            public void mAnimator(ValueAnimator animation)
            {
                mCurrentAngle = (int) animation.getAnimatedValue();

                invalidate();
            }
        });

给我们的mAnimator设置监听,在mAnimator()方法中将当前计算出来的值赋值给mCurrentAngle,再调用invalidate()重绘页面,此时view会执行ondraw方法,我们这个动画的原理就是,动态更改mCurrentAngle的值,不断重绘,稍后讲解怎么根据mCurrentAngle绘图。

4、 重新调整小球到中心点得距离

一个好的自定义view必须提供完美的兼容性,有时候,我们可能在布局文件了设置了view的大小,如果view的长宽小于我们代码设值得小圆点离中心点得距离mRingRadius的俩倍,小球将会绘制在视图之外,导致看不到。所以我们在onLayout方法中调整mRingRadius,这里计算宽高需要扣除内边距。

因为在view的绘制流程onLayout()中,可以获取到view的实际宽高,所以我们把调整代码放在这里,以下是具体代码:

   @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);

        // 重设圆环半径,防止超出视图大小
        int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;

        mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
        mOriginalRingRadius = mRingRadius;
    }

5、绘制小球

5.1 小球坐标分析

假设坐标轴y轴向上方向为0度,小球的当前角度angle可以计算出小球所在的坐标,计算方法如下图:

这里写图片描述

在我们这,圆点坐标是view的中心点,即宽高的一半。

    private void drawDot(Canvas canvas, double angle)
    {
        //根据当前角度获取x、y坐标点
        float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
        float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));
        
        //绘制圆
        canvas.drawCircle(x, y, mDotRadius, mPaint);
    }

5.2 小球缩小聚拢实现

将小球到中心的距离mRingRadius缩小就达到了聚拢的效果,同理缩小小球半径mDotRadius就可以改变小球大小。我们根据当前旋转角度mCurrentAngle进行变化。

为了方便计算,我们封装一个估值器方法:

    private Integer evaluate(float fraction, Integer startValue, Integer endValue)
    {
        int startInt = startValue;
        return (int) (startInt + fraction * (endValue - startInt));
    }

这个方法在安卓动画计算中很常用,实现还是很简单的,传入三个参数,含义如下:

  • fraction:估值器的值,大小从0-1变化,控制我们最终值变化的变量
  • startValue:起始值,当fraction为0时计算得出的值
  • endValue:最终值,当fraction为1时计算得出的值

下面讲解如何通过该方法mCurrentAngle计算mRingRadius

我们需要一个fraction变量来控制mRingRadius的最终值,前面说了,变量是当前旋转的角度mCurrentAngle。那么如何一个0-360的mCurrentAngle将转为为一个0-1的值呢?
俩行代码搞定:

        float fraction = 1.0f * mCurrentAngle / 180 - 1;
        fraction = Math.abs(fraction);

这样,当mCurrentAngle从0到180度变化时,fraction从1到0,随着mCurrentAngle从180变化到360时fraction继续从1变化到0,如此循环往复。得到了我们估值器的估值变量fraction
我们mRingRadius的变化是从原始值减小到一半,mDotRadius的变化是从原始值减小到4/5。
我们打印下mRingRadius变化情况。

这里写图片描述

6、完整代码

public class ProgressView extends View
{
    private int mDotCount = 5; // 圆点个数
    private int mDotColor = 0xFFFF9966;// 圆点颜色

    private Paint mPaint;

    private int mRingRadius = 50;// 圆环半径,单位dp
    private int mOriginalRingRadius;// 保存的原始圆环半径,单位dp
    private int mDotRadius = 7; // 小点半径,单位dp
    private int mOriginalDotRadius; // 保存的原始小点半径,单位dp

    private int mCurrentAngle = 0; // 当前旋转的角度

    private ValueAnimator mAnimator;// 旋转动画

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

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
        mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
        mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
        ta.recycle();

        init();
    }

    public ProgressView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public ProgressView(Context context)
    {
        this(context, null);
    }

    private void init()
    {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.FILL_AND_STROKE);
        mPaint.setColor(mDotColor);

        // 屏幕适配,转化圆环半径,小点半径
        mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
        mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
        mOriginalDotRadius = mDotRadius;

        initAnimatior();
    }

    private void initAnimatior()
    {
        mAnimator = ValueAnimator.ofInt(0, 359);
        mAnimator.setDuration(4000);
        mAnimator.setRepeatCount(-1);
        mAnimator.setRepeatMode(ValueAnimator.INFINITE);
        mAnimator.setInterpolator(new LinearInterpolator());

        mAnimator.addUpdateListener(new AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                mCurrentAngle = (int) animation.getAnimatedValue();

                invalidate();
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);

        // 重设圆环半径,防止超出视图大小
        int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;

        mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
        mOriginalRingRadius = mRingRadius;
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        // 根据小球总数平均分配整个圆,得到每个小球的间隔角度
        double cellAngle = 360 / mDotCount;

        for (int i = 0; i < mDotCount; i++)
        {
            double ange = i * cellAngle + mCurrentAngle;

            // 根据当前角度计算小球到圆心的距离
            calculateRadiusFromProgress();

            // 根据角度绘制单个小球
            drawDot(canvas, ange * 2 * Math.PI / 360);
        }
    }

    /**
     * 根据当前旋转角度计算mRingRadius、mDotRadius的值
     * mCurrentAngle:   0 - 180 - 360
     * mRingRadius:     最小 - 最大 - 最小
     * @author 漆可
     * @date 2016-6-17 下午3:04:35
     */
    private void calculateRadiusFromProgress()
    {
        float fraction = 1.0f * mCurrentAngle / 180 - 1;
        fraction = Math.abs(fraction);

        mRingRadius = evaluate(fraction, mOriginalRingRadius, mOriginalRingRadius * 2 / 4);
        mDotRadius = evaluate(fraction, mOriginalDotRadius, mOriginalDotRadius * 4 / 5);
    }

    // fraction:当前的估值器计算值,startValue:起始值,endValue:终点值
    private Integer evaluate(float fraction, Integer startValue, Integer endValue)
    {
        return (int) (startValue + fraction * (endValue - startValue));
    }

    @Override
    protected void onAttachedToWindow()
    {
        super.onAttachedToWindow();
        startAnimation();
    }

    private void drawDot(Canvas canvas, double angle)
    {
        // 根据当前角度获取x、y坐标点
        float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
        float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));

        // 绘制圆
        canvas.drawCircle(x, y, mDotRadius, mPaint);
    }

    public void startAnimation()
    {
        mAnimator.start();
    }

    public void stopAnimation()
    {
        mAnimator.end();
    }

    //销毁页面时停止动画
    @Override
    protected void onDetachedFromWindow()
    {
        super.onDetachedFromWindow();
        stopAnimation();
    }
}

最后,奉上demo下载地址:http://download.csdn.net/detail/q649381130/9552643

相关文章

网友评论

    本文标题: android:自定义绚丽的加载进度动画

    本文链接:https://www.haomeiwen.com/subject/tibndttx.html