ProgressLoadingView带进度的加载动画

本文详细介绍了如何实现一个带有进度的加载动画组件ProgressLoadingView,包括其架构设计、动画实现原理,以及如何处理加载成功和加载失败的情况。文章强调了组件的可定制性和良好的代码组织结构,适合有一定自定义View基础的Android开发者阅读。
摘要由CSDN通过智能技术生成

提笔

  难以提笔,不知如何编写才能将此控件描述清晰。思考良久,觉得与其详细扣代码细节,还不如讲解一下实现原理、架构设计的逐步形成以及整个心路历程。当然想要扣代码细节的请戳这里Gayhub仓库

简述

  这是一款带有进度的加载动画,可以定制不同的style。注:阅读本文需要有一定自定义View基础。

效果图

​   此控件目前提供了两种样式,分别于效果图中左侧和右侧,颜色和背景色可以自定义,gif图帧数不足,看起来有点卡顿,实际效果更加流畅。详细用法请看这里Gayhub仓库

架构设计

最初设想.png

​  这是最初的设想,开始认为效果比较简单,实现起来代码量应该也不会太高,全部揉到一起算了,简单粗暴。结果写着写着发现属性太多,容易产生混乱,举个简单的例子就是。在加载成功时会有个勾出现的动画,这个勾是用线条画的,也就是drawLine,那么也就会有起始坐标,终点坐标,但是勾是由两条线组成的,右边的线条也有起始坐标,终点坐标,这样算下来就有8个属性值了。这仅是一个加载成功勾的动画,还有加载过程中,加载失败对应的属性值同样繁多,这样全部放到一个类中,想想都可怕。

​  所以,必须得将它们分开。

​  怎么分呢?观看效果图,我们可以发现可以将其分成三个组成部分。一个是正在加载过程中,另外的是加载成功和加载失败,总共三部分组成。既然分开了,那找找它们之间是不是有什么共性的地方需要提取出来?经过一番比较,出现了如下的设计。

未加ResultView.png

​  我将分出来的三部分分别取名为ProgressView(负责加载过程的绘制), SuccessView(负责加载成功时的绘制), FailedView(负责加载失败时的绘制),将它们的共性提取出来并抽象到了PartView(Partview 是一个抽象类,图中用斜体字表示)中,所以这三个类都继承自PartView。ProgressLoadingView则关联ProgressView, SuccessView,FailedView。请注意这里PartView并不是继承自View,所以想要在它们各自内部进行绘制,就得在ProgressLoadingView的onDraw中,将画布canvas传给这三个类,所以我在PartView中定义了onDraw(Canvas canvas)抽象方法。在代码中则是这样的表现形式

{@link ProgressLoadingView#onDraw(Canvas canvas)}

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //Log.e(TAG, "onDraw: " + Thread.currentThread());
        if (mCurrentState == State.PROGRESS) {
            mProgressView.onDraw(canvas);
        } else if (mCurrentState == State.SUCCESS) {
            mSuccessView.onDraw(canvas);
        } else if (mCurrentState == State.FAILED) {
            mFaileddView.onDraw(canvas);
        }
    }

  因此,可以这样理解:ProgressLoadingView给ProgressView、SuccessView、FailedView提供了一个容器,一张画布,而这三个类相当于画家,各自没有任何联系,耦合度为0,只需完成各自的绘制任务即可,所以完全可以在其他地方单独使用这三个类,只需要按照ProgressLoadingView中类似的控制逻辑即可。

​  好了,架构设计的差不多了,也开始编码了。当我写完ProgressView和SuccessView部分后,将它们之间的动画合并时发现了一个问题。如果在设置加载成功时,ProgressView的动画立即结束而开启SuccessView的动画,效果比较生硬,细心的读者可以发现在效果图中,当我点击SUCCESS时,转圈的动画(也就是ProgressView中的动画)并没有立即结束,而是转到了一个特定的位置才结束的。所以这里得在ProgressView中告诉外界(在这里也就是ProgressLoadingView)它的动画什么时候结束,因为外界并不知道它什么时候结束,只能控制它开始。所以我们得加上一个接口,以便通知外界。如下,
加ProgressListener未加ResultView.png

​  ProgressLoadingView实现ProgressListener用来接收通知,ProgressView关联ProgressListener同来通知ProgressLoadingView,在代码中形式如下,

{@link ProgressView#getAnimation()}

mProgressAnimation = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                super.applyTransformation(interpolatedTime, t);
                //Log.e(TAG, "applyTransformation: isCancel = " + mIsCancel);
                if (mIsCancel) {
                    if (mStartAngle == mDefaultStartAngle) {
                        //cancel();
                        //通知ProgressLoadingView动画结束
                        if (mListener != null) {
                            mListener.finished();
                        }
                    }
                }
                mStartAngle = (-1 * 360f * interpolatedTime + mDefaultStartAngle) % 360;
                mLength = (float) Math.abs(Math.sin(Math.PI * interpolatedTime * mCycleCount))
                        * mMaxLength;
                //Log.e(TAG, "applyTransformation: mLength = " + mLength + " interlodatedTime = " + interpolatedTime);
                mView.invalidate();
            }
        };

{@link ProgressLoadingView}
public class ProgressLoadingView extends View implements ProgressListener
mProgressView.setProgressListener(this);
在finished方法中做判断执行加载成功或者加载失败的动画

@Override
    public void finished() {
        Log.e(TAG, "finished: " + Thread.currentThread());
        if (mResult == State.SUCCESS) {
            mCurrentState = State.SUCCESS;
            this.startAnimation(mSuccessView.getAnimation());
        } else if (mResult == State.FAILED) {
            mCurrentState = State.FAILED;
            this.startAnimation(mFaileddView.getAnimation());
        }
    }

  现在开始写FailedView加载失败的内容了,读者肯定会觉得既然单独拿出来那就是遇到问题了,确实没错。。问题来了,与其说是问题,不如说是之前想得不周到。参考效果图中,勾和叉的画法,都是使用的两条线条,那么它们之间就自然而然也存在一些共性的地方,比如,这两个线条都分别有起末位置的坐标,还有它们的颜色等,所以这里也得给他们抽取出来。如下图,
加入ResuleView.png

​  这里抽象了一个ResultView类,它里面包含了SuccessView和FailedView中的一些共性属性,并实现了一些基本方法,在图中所示的几个方法其实都是已经实现的方法,由于使用工具的原因,竟然不能选取部分字体变为斜体,一个字体改变,这个类所有的字体都会改变(这一点processOn这个在线工具让我内心毫无波动,甚至还有点想笑…还是推荐大家使用VP吧,毕竟专业,不过VP的水印也是让人抓狂),至于这几个方法的作用将会在下面讲解动画原理中介绍。抽象完成之后,就只需要完成FailedView的动画和它的绘制过程就基本完成了。

动画实现原理

​  动画实现原理我并不打算全部讲到,有几方面原因,一是全部讲解篇幅会大幅加深,降低读者兴趣;二是加载成功和加载失败两个动画有很多的相似点;三是画图是在是太花费时间了(马上就得考试了,留给我的复习时间已经寥寥无几了,还得去准备实习的相关工作,说到这里得吐槽一下,学校的破规矩真是太多了)。如果有兴趣的读者可以去看看源码,我觉得看代码爽哆啦。

​  虽然ProgressView中的动画细节不会讲,但是这里还得提一下,两种style的区别,只需要画圆弧时将mPaint.setStyle()设置为FILL就从第一种style变为了第二种,也就是效果图中左侧变为了右侧(这还是我无意中发现的,汗。。。关键是效果还比第一种好。。。)。

加载成功

先看一下当加载成功时的动画实现方式,如下图所示

ProgressLoadingView-Success.png

​  绿色部分表示勾的部分,他分为两部分,左侧部分HL, 右侧部分LJ,这两部分为别在两条直线y1,y2上,根据android坐标轴的方向,这里也是右向X为正,下向Y为正。算得这两条直线的函数为y1 = mOffsetRatio * mRadius + x, y2 = mOffsetRatio * mRadius - x,其中,mOffsetRatio 表示L点到原点的距离所占据整个圆半径的比例,实际含义可以理解为X轴到L点的偏移量。知道了两条直线的函数,就可以根据在其上点的横坐标算出纵坐标了。注意一点,图中的D点和E点,是两条直线和圆相交的两点,算出其横纵坐标将对动画的完成起到重大的作用,根据高中解析几何的知识,算出两点的坐标比较容易,这里就不展开。在代码中的表现形式就是这样,
{@link ResultView}

protected float calculateLeftY(float leftX) {
        return leftX + mOffsetYRatio * mRadius;
    }
protected float calculateRightY(float rightX) {
        return -1 * rightX + mOffsetYRatio * mRadius;
    }
//计算D点横坐标
protected void calculateLeftIntersectingPoint() {
        mLeftIntersectingX = (float) ((-1 * mOffsetYRatio * mRadius
                - Math.sqrt((2 - mOffsetYRatio * mOffsetYRatio) * mRadius * mRadius)) / 2);
    }
//计算E点横坐标
protected void calculateRightIntersectingPoint() {
        mRightIntersectingX = (float) ((mOffsetYRatio * mRadius
                + Math.sqrt((2 - mOffsetYRatio * mOffsetYRatio) * mRadius * mRadius)) / 2);
    }

  勾的动画我将其分作了三个阶段, 第一阶段是只有左侧部分mLeftEndX从D点拉伸到L点,mLeftStartX一直在D点不变;第二阶段是左侧mLeftStartX从D点移到L点,mLeftEndX在L点不变,同时mRightStartX也在L点不动,mRightEndX从L点移动到E点,这样的效果就是左侧慢慢消失,右侧慢慢伸长;第三阶段是左侧mLeftSatrtX慢慢恢复到图中的H点,mLeftEndX不变,右侧mRightEndX慢慢恢复到J点,mRightStartX不变。请注意上述所说的X的移动,实际上就可以看作点的移动,因为Y可以根据函数计算出来。这三个阶段所对应的时间片段为0~0.2, 0.2~0.8,0.8~1,在代码中的表现形式

mSuccessAnimation = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                super.applyTransformation(interpolatedTime, t);
                //Log.e(TAG, "applyTransformation: ");
                if (0.0 <= interpolatedTime && 0.2 >= interpolatedTime) {
                    //mLeftRatio表示x的值,所占mLeftIntersectingX的比例
                    mLeftRatio = 1.0f - interpolatedTime * 5;
                    mLeftStartX = mLeftIntersectingX;
                    mLeftStartY = calculateLeftY(mLeftStartX);
                    mLeftEndX = mLeftIntersectingX * mLeftRatio;
                    mLeftEndY = calculateLeftY(mLeftEndX);
                    mView.invalidate();
                } else if (0.2 < interpolatedTime && 0.8 >= interpolatedTime) {
                    mLeftRatio = 1.0f - (interpolatedTime - 0.2f) * 5 / 3;
                    mLeftStartX = mLeftIntersectingX * mLeftRatio;
                    mLeftStartY = calculateLeftY(mLeftStartX);
                    mLeftEndX = mCenterX;
                    mLeftEndY = mCenterY;

                    mIsArriveCenter = true;
                    mRightRatio = (interpolatedTime - 0.2f) * 5 / 3;
                    mRightEndX = mRightIntersectingX * mRightRatio;
                    mRightEndY = calculateRightY(mRightEndX);
                    mView.invalidate();
                } else {
                    mLeftRatio = (interpolatedTime - 0.8f) * 5 / 2;
                    mLeftStartX = mLeftIntersectingX * mLeftRatio;
                    mLeftStartY = calculateLeftY(mLeftStartX);
                    mLeftEndX = mCenterX;
                    mLeftEndY = mCenterY;

                    mRightRatio = 1 - (interpolatedTime - 0.8f) * 5 / 4;
                    mRightEndX = mRightIntersectingX * mRightRatio;
                    mRightEndY = calculateRightY(mRightEndX);
                    mView.invalidate();
                }
            }
        };

  几何的东西,虽然看着简单,但是通过文字描述清楚难度比较大,希望读者能对照效果图分析,或者下载源码加长动画的执行时间,重新编译,就能很好的看出动画过程。

加载失败

  加载失败和加载成功的动画存在太多的相似点,首先都是两条线段逐渐变化,其次,两条线段之间的角度都是一样的。所以我们可以用类似的方式来实现,在计算坐标的时候,只需要将两条直线的函数改为y1 = x, y2 = -x即可,也就是将mOffsetRatio改为0,让两条直线的交点在原点即可,因为要使两边对称嘛。动画过程同理都是不断的改变两条线段的起末坐标,这里贴下代码即可。

mFailedAnimation = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                super.applyTransformation(interpolatedTime, t);

                mLeftEndX = mLeftIntersectingX * 0.5f - interpolatedTime * mLeftIntersectingX;
                mLeftEndY = calculateLeftY(mLeftEndX);

                mRightEndX = mRightIntersectingX * 0.5f - interpolatedTime * mRightIntersectingX;
                mRightEndY = calculateRightY(mRightEndX);
                mView.invalidate();
            }
        };

  到这里,本文就已经结束了,贴上Gayhub的地址,Gayhub仓库,习惯阅读源码的读者,还是推荐读一下源码吧,另外控件的使用方法在其中也有详细介绍。

​   感谢读者的阅读~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值