一、前言
近段时间公司扩大人才储备,需要我去面试。大多数来面试的人当被问到Android动画的问题时候基本只会说android动画几种类型啊什么的。说用到过哪种动画,都说只用过简单的帧动画。其实这种情况让我听失望的,因为我本人还是比较喜欢动画方面的知识点的,那么针对这种情况,我再给兄弟姐妹们介绍一个新的抽象类:Drawable。
二、将要实现的效果
1.文字描述
就拿我们经常会用到的加载动画来说吧,市面上绝大多数动画都是用的帧动画,让一个点点轮番放大缩小,从而展示出较大的点点在转动的效果。随着MD概念的出现,我们经常看到的加载动画也一点点发生了改变,比如SwipeRefreshLayout中自带的下拉刷新加载框效果,低调奢华有内涵。
那我们将要实现的效果是这样的:网络访问分简单一点1-成功2-失败;两种状态,我们分别用√和X来表示。那加载的过程我们就播放一条正玄曲线,出现波浪的效果,有一种不断有海浪吹过来的感觉。当加载完毕之后,将波浪变换成对号或者是错号然后隐藏加载框。
2.Gif展示
三、实现思路
1.从源码下手,解读SwipeRefreshLayout中的加载动画
首先进入SwipeRefreshLayout源码并搜索:mCircleView。然后跟踪mCircleView,找到它的setImageDrawable方法,此时我们会看到mProgress,我们继续跟踪mProgress,发现它是一个MaterialProgressDrawable,进入源码可以看到它继承Drawable同时实现了Animatable。这就是我们要重点介绍的类了。对于Drawbale类,他是一个抽象类,我们的ImageView可以直接使用setImageDrawable来显示该Drawable。同时继承Drawable需要重写它的几个方法:
public abstract void draw(Canvas canvas);核心方法
public abstract void setAlpha(int alpha);改变整体透明度
public abstract void setColorFilter(@Nullable ColorFilter colorFilter);该方法会改变整个Drawable的颜色,我们这里暂时不做深究
public abstract void getOpacity();返回透明度的标准(MaterialProgressDrawable中返回的是PixelFormat.TRANSLUCENT,表示系统选择一个支持半透明的格式(许多α位))
那我们就针对这几个抽象类去阅读MaterialProgressDrawable的源码。
首先看draw:
@Override
public void draw(Canvas c) {
final Rect bounds = getBounds();
final int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
这个方法里面主要就是调用了mRing.draw(c,bounds);其中c提供了画布,bounds提供了画布的大小。看类的名字大概可以猜到,它画了一个环(ring)。我们继续跟踪mRing这个类:
/** * Draw the progress spinner */
public void draw(Canvas c, Rect bounds) {
final RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
arcBounds.inset(mStrokeInset, mStrokeInset);
final float startAngle = (mStartTrim + mRotation) * 360;
final float endAngle = (mEndTrim + mRotation) * 360;
float sweepAngle = endAngle - startAngle;
mPaint.setColor(mCurrentColor);
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
drawTriangle(c, startAngle, sweepAngle, bounds);
if (mAlpha < 255) {
mCirclePaint.setColor(mBackgroundColor);
mCirclePaint.setAlpha(255 - mAlpha);
c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint);
}
}
该方法分成两部分:当mAlpha<255时,也就是有透明效果的时候画圆;否则画圆弧。drawArc这个方法画了一个由startAngle和endAngle以及arcBounds控制的基于arcBounds的内接圆弧。
除此之外该类还实现了Animatable接口:
start();开始动画
<span style="font-size:18px;">@Override
public void start() {
mAnimation.reset();
mRing.storeOriginals();
// Already showing some part of the ring
if (mRing.getEndTrim() != mRing.getStartTrim()) {
mFinishing = true;
mAnimation.setDuration(ANIMATION_DURATION/2);
mParent.startAnimation(mAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mAnimation.setDuration(ANIMATION_DURATION);
mParent.startAnimation(mAnimation);
}
}</span>
stop();停止动画
<span style="font-size:18px;">@Override
public void stop() {
mParent.clearAnimation();
setRotation(0);
mRing.setShowArrow(false);
mRing.setColorIndex(0);
mRing.resetOriginals();
}</span>
isRunning();动画是否在播放中
<span style="font-size:18px;">@Override
public boolean isRunning() {
final ArrayList<Animation> animators = mAnimators;
final int N = animators.size();
for (int i = 0; i < N; i++) {
final Animation animator = animators.get(i);
if (animator.hasStarted() && !animator.hasEnded()) {
return true;
}
}
return false;
}</span>
我们在三个方法中都看到了Anim的身影,从start()中我们跟踪mAnimation,并在如下方法中找到mAnimation的初始化:
<span style="font-size:18px;">private void setupAnimators() {
final Ring ring = mRing;
final Animation animation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
if (mFinishing) {
applyFinishTranslation(interpolatedTime, ring);
} else {
// The minProgressArc is calculated from 0 to create an
// angle that matches the stroke width.
final float minProgressArc = getMinProgressArc(ring);
final float startingEndTrim = ring.getStartingEndTrim();
final float startingTrim = ring.getStartingStartTrim();
final float startingRotation = ring.getStartingRotation();
updateRingColor(interpolatedTime, ring);
// Moving the start trim only occurs in the first 50% of a
// single ring animation
if (interpolatedTime <= START_TRIM_DURATION_OFFSET) {
// scale the interpolatedTime so that the full
// transformation from 0 - 1 takes place in the
// remaining time
final float scaledTime = (interpolatedTime)
/ (1.0f - START_TRIM_DURATION_OFFSET);
final float startTrim = startingTrim
+ ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR
.getInterpolation(scaledTime));
ring.setStartTrim(startTrim);
}
// Moving the end trim starts after 50% of a single ring
// animation completes
if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) {
// scale the interpolatedTime so that the full
// transformation from 0 - 1 takes place in the
// remaining time
final float minArc = MAX_PROGRESS_ARC - minProgressArc;
float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET)
/ (1.0f - START_TRIM_DURATION_OFFSET);
final float endTrim = startingEndTrim
+ (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime));
ring.setEndTrim(endTrim);
}
final float rotation = startingRotation + (0.25f * interpolatedTime);
ring.setRotation(rotation);
float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime)
+ (FULL_ROTATION * (mRotationCount / NUM_POINTS));
setRotation(groupRotation);
}
}
};
animation.setRepeatCount(Animation.INFINITE);
animation.setRepeatMode(Animation.RESTART);
animation.setInterpolator(LINEAR_INTERPOLATOR);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mRotationCount = 0;
}
@Override
public void onAnimationEnd(Animation animation) {
// do nothing
}
@Override
public void onAnimationRepeat(Animation animation) {
ring.storeOriginals();
ring.goToNextColor();
ring.setStartTrim(ring.getEndTrim());
if (mFinishing) {
// finished closing the last ring from the swipe gesture; go
// into progress mode
mFinishing = false;
animation.setDuration(ANIMATION_DURATION);
ring.setShowArrow(false);
} else {
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
}
});
mAnimation = animation;
}</span>
从代码中看出,mAnimation是一个自定义的Animation类。具体我们要看它的applyTransformation方法,该方法内部通过在不同的动画阶段改变mRing的startAngle与endAngle以及colorFilter,来实现那种圆弧变换的效果。
源码暂时看到这里,我们总结一下它的实现原理:
继承Drawable重写draw方法,在canvas上画圆环,并开放开始角度与结束角度来控制圆环的位置,然后实现Animatable接口,通过自定义Animation的applyTransformation方法在动画执行过程中不断变换startAngle,endAngle来控制圆环,达到了不断变换的效果。
2.根据需求设计自己的加载流程
接下来我们模拟加载流程,设计一个加载动画。
1.显示加载布局:一条线从屏幕中央由上到下或者有下到上随滑动进入屏幕并在中间点向水平方向分开,当线完全变平时表示达到下拉的最大位置。
2.开始加载:完全水平的直线波纹迅速变大到某个值,然后不断的变换波形的位置制造出动态效果
3.加载完成:加载完成时波纹波纹迅速变小,变成一条直线
4.显示加载结果:根据实际加载结果显示对号或者错号,由直线进行变换得到
5.隐藏加载布局:对号/错号汇聚成一条竖线,然后向上退出屏幕
3.实现波浪
Path path = new Path();
path.moveTo(-6, rect.height() / 2);
int size = rect.width();
float y;
for (int x = 0; x < size; x++) {
y = (float) (maxH * Math.sin((x + b) * Math.PI / cycle) + rect.height() / 2);
path.lineTo(x, y);
}
canvas.drawPath(path, mPaint);
核心的应该是drawPath以及计算xy的方法了,说白了就是数学。
4.实现对号与错号
画对号
Path path = new Path();
path.moveTo(lineX, height / 2 - lineY);
path.lineTo(width / 8 * 3, (lineY + height) / 2);
path.lineTo(width - lineX, height / 2 - lineY);
canvas.drawPath(path, mPaint);
画错号
Path leftPath = new Path();
Path rightPath = new Path();
leftPath.moveTo(lineX, height / 2 - lineY);
leftPath.lineTo(width / 2 + lineX, height / 2 + lineY);
rightPath.moveTo(width - lineX, height / 2 - lineY);
rightPath.lineTo(width / 2 - lineX, height / 2 + lineY);
canvas.drawPath(leftPath, mPaint);
canvas.drawPath(rightPath, mPaint);
四、代码解析
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
/**
* 用线程与Handler去控制变换的动画
* Created by Mr-Zhang on 2016/7/27.
*/
public class SinLineDrawable extends Drawable {
private float maxH = 20;//振幅
private float theH;
private float mScale=1.0f;//缩放等级
private double cycle = 30;//周期
private double b = 0;//偏移量从0-20循环变化
private Paint mPaint;
private boolean isLoading = false;
private float lineY = 0;
private float lineX = 0;
private boolean drawSuccess = false;
private boolean drawFailed = false;
private float theX;
private float currentH;
private boolean waveDown = false;
private OnLoadFinishListener onLoadFinishListener;
private Handler mHandler = new Handler();
public SinLineDrawable() {
this.mPaint = new Paint();
initPaint();
}
public void setOnLoadFinishListener(OnLoadFinishListener onLoadFinishListener) {
this.onLoadFinishListener = onLoadFinishListener;
}
@Override
public void draw(Canvas canvas) {
Rect rect = getBounds();
int width = rect.width();
int height = rect.height();
maxH = height / 4;
theX = width / 4 / 20;
cycle = width / 4;
theH = maxH / 20;
canvas.scale(mScale,mScale,width/2,height/2);
if (drawSuccess) {
//画对号
Path path = new Path();
path.moveTo(lineX, height / 2 - lineY);
path.lineTo(width / 8 * 3, (lineY + height) / 2);
path.lineTo(width - lineX, height / 2 - lineY);
canvas.drawPath(path, mPaint);
} else if (drawFailed) {
//画错号
Path leftPath = new Path();
Path rightPath = new Path();
leftPath.moveTo(lineX, height / 2 - lineY);
leftPath.lineTo(width / 2 + lineX, height / 2 + lineY);
rightPath.moveTo(width - lineX, height / 2 - lineY);
rightPath.lineTo(width / 2 - lineX, height / 2 + lineY);
canvas.drawPath(leftPath, mPaint);
canvas.drawPath(rightPath, mPaint);
} else {
Path path = new Path();
path.moveTo(-6, rect.height() / 2);
int size = rect.width();
float y;
for (int x = 0; x < size; x++) {
if (waveDown) {
y = (float) (currentH * Math.sin((x + b) * Math.PI / cycle) + rect.height() / 2);
} else {
y = (float) (maxH * Math.sin((x + b) * Math.PI / cycle) + rect.height() / 2);
}
path.lineTo(x, y);
}
canvas.drawPath(path, mPaint);
}
}
public void setScale(float mScale) {
this.mScale = mScale;
invalidateSelf();
}
public void setMaxH(int maxH) {
this.maxH = maxH;
invalidateSelf();
}
public void setB(int b) {
this.b = b;
invalidateSelf();
}
public void setCycle(int cycle) {
this.cycle = cycle;
invalidateSelf();
}
private void initPaint() {
mPaint.setStrokeWidth(6.0f);
mPaint.setColor(Color.WHITE);
mPaint.setStrokeCap(Paint.Cap.SQUARE);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
public void setAlpha(int i) {
mPaint.setAlpha(i);
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
public void startLoading() {
this.isLoading = true;
new Thread(new LoadingRunnable()).start();
}
public void finishLoading(boolean b) {
this.isLoading = false;
lineY = 0;
lineX = 0;
if (b) {
new Thread(new SuccessRunnable()).start();
} else {
new Thread(new FailedRunnable()).start();
}
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
class LoadingRunnable implements Runnable {
@Override
public void run() {
while (isLoading) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mHandler.post(new Runnable() {
@Override
public void run() {
invalidateSelf();
}
});
b = b + cycle / 10;
}
}
}
}
class SuccessRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
try {
Thread.sleep(20);
waveDown = true;
if (i < 20) {
currentH = maxH - theH * (i + 1);
} else {
drawSuccess = true;
lineY = lineY + theH * 2;
lineX = lineX + theX;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mHandler.post(new Runnable() {
@Override
public void run() {
invalidateSelf();
}
});
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (onLoadFinishListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
onLoadFinishListener.loadFinish();
waveDown = false;
drawSuccess = false;
}
});
}
}
}
}
class FailedRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
try {
Thread.sleep(20);
waveDown = true;
if (i < 20) {
currentH = maxH - theH * (i + 1);
} else {
drawFailed = true;
lineY = lineY + theH * 2;
lineX = lineX + theX;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mHandler.post(new Runnable() {
@Override
public void run() {
invalidateSelf();
}
});
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (onLoadFinishListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
onLoadFinishListener.loadFinish();
waveDown = false;
drawFailed = false;
}
});
}
}
}
}
public interface OnLoadFinishListener {
void loadFinish();
}
}
五、使用方法
public void startLoadMore() {
mImageView.setVisibility(VISIBLE);
mImageView.bringToFront();
//开始执行加载更多动画
mSinLine.startLoading();
}
/**
* @param b true表示加载成功反之表示失败
*/
public void loadMoreFinish(boolean b) {
//加载更多完毕
mSinLine.finishLoading(b);
mSinLine.setOnLoadFinishListener(new SinLineDrawable.OnLoadFinishListener() {
@Override
public void loadFinish() {
mImageView.setVisibility(GONE);
mIsBeingLoadMore = false;
}
});
}
六、交流与总结
以上是我在SwipeRefreshLayout的源码中学习到的知识点,并自己做了一下拓展。本文中的流程设计并没有真正的完全实现。这算是对自己以后的一个要求与期待吧。然后放上资源链接https://Git.oschina.NET/mr-zhang/TestUtils.git