本文介绍了Android的View动画中的旋转动画的应用及SquareLoading源码解析。
《冬夜读书示子聿》
古人学问无遗力,少壮工夫老始成。
纸上得来终觉浅,绝知此事要躬行。
–陆游
在前面的文章中,我们介绍了View动画、帧动画和属性动画的使用。
既然动画的基本使用我们都会了,那就从网上找一个动画来试着实现一下吧。从MaterialUp上面还真找到一个简单一点的动画:Animated Loader
乍一看还挺惊艳的,我们先来分析一下这个动画,动画中为12个方块,很简单的旋转90度的动画,我们先来实现一个方块的动画,使用GradientDrawable创建方块:1
2
3
4
5
6GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(mSquareColor);
gradientDrawable.setSize(mSquareSize, mSquareSize);
gradientDrawable.setCornerRadius(mSquareCorner);
ImageView image = new ImageView(context);
image.setImageDrawable(gradientDrawable);
旋转中心点为左下角,旋转角度为90,持续时间为300ms,设置减速插值器,设置动画结束后延时300ms执行反转动画,同样反转动画结束后延时300ms重新开始执行动画:
1 | RotateAnimation startAnim = new RotateAnimation(0, -90, 0, mSquareSize); |
这样当调用startRotateAnim方法就可以看到效果了,接下来我们在上面的基础上实现12个方块的动画,上面的图片需要添加12个,我们通过继承ViewGroup自定义View实现,这里我们设置默认X的个数为4,Y的个数为3:1
2
3
4
5
6
7
8
9
10
11
12public class SquareLoading extends ViewGroup {
private static final int DEFAULT_SQUARE_COLOR = Color.WHITE;
private static final int DEFAULT_SQUARE_SIZE = 36;
private static final int DEFAULT_SQUARE_CORNER = 8;
private static final int DEFAULT_DIVIDER_SIZE = 8;
private static final int DEFUALT_X_COUNT=4;
private static final int DEFUALT_Y_COUNT=3;
private static final int DEFAULT_FIRST_INDEX = DEFUALT_X_COUNT * (DEFUALT_Y_COUNT - 1);
private static final int DEFAULT_LAST_INDEX = DEFUALT_X_COUNT - 1;
...
}
其中DEFAULT_FIRST_INDEX和DEFAULT_LAST_INDEX分别表示第一个开始动画子View的index和最后一个开始动画子View的index。例如,当x=4,y=3时,第一个开始执行开始动画的子View的index为8,最后一个执行开始动画的子View的index为3,当然,这也对应反转动画的最后一个和第一个:
一个完整的自定义View对应的属性肯定是可以配置的,比如方块的数量、大小和颜色等:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SquareLoading);
mSquareColor = a.getColor(R.styleable.SquareLoading_squareColor, DEFAULT_SQUARE_COLOR);
mSquareSize = a.getDimensionPixelSize(R.styleable.SquareLoading_squareSize, DEFAULT_SQUARE_SIZE);
mSquareCorner = a.getDimensionPixelSize(R.styleable.SquareLoading_squareCorner, DEFAULT_SQUARE_CORNER);
mDividerSize = a.getDimensionPixelSize(R.styleable.SquareLoading_dividerSize, DEFAULT_DIVIDER_SIZE);
int xCount = a.getInteger(R.styleable.SquareLoading_xCount, DEFUALT_X_COUNT);
int yCount = a.getInteger(R.styleable.SquareLoading_yCount, DEFUALT_Y_COUNT);
if (xCount >= 2 && xCount <= 6) {
mXCount = xCount;
}
if (yCount >= 2 && yCount <= 6) {
mYCount = yCount;
}
a.recycle();
mFirstIndex = mXCount * (mYCount - 1);
mLastIndex = mXCount - 1;
}
添加图片之前有一个细节需要处理,因为我们是继承ViewGroup自定义的View,所以要防止用户添加其它子View:1
2
3if (getChildCount() > 0) {
removeAllViews();
}
接下来根据配置的方块的个数添加子View:1
2
3
4
5for (int i = 0; i < mXCount * mYCount; i++) {
ImageView image = new ImageView(context);
image.setImageDrawable(gradientDrawable);
addView(image);
}
因为每个子View都有对应的开始动画和反转动画,所以我们使用List来存储对应子View的动画,动画和只有一个方块时的区别是当第一个子View动画开始时需要延时执行下一个子View的动画,当所有子View开始动画结束后开始逆向执行反转动画,如此反复:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109private List<RotateAnimation> startAnims = new ArrayList<>();
private List<RotateAnimation> reverseAnims = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
RotateAnimation startAnim = new RotateAnimation(0, -90, 0, mSquareSize);
startAnim.setDuration(300);
startAnim.setFillAfter(true);
startAnim.setInterpolator(new DecelerateInterpolator());
final int finalI = i;
startAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
if (finalI != mLastIndex) {
int index = getNextAnimChild(true, finalI);
int delayMillis = index > mFirstIndex ? 100 : 50;
startRotateAnim(index , delayMillis);
}
}
@Override
public void onAnimationEnd(Animation animation) {
if (finalI == mLastIndex) {
startReverseAnim(mLastIndex, 300);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
startAnims.add(startAnim);
RotateAnimation reverseAnim = new RotateAnimation(-90, 0, 0, mSquareSize);
reverseAnim.setDuration(300);
reverseAnim.setFillAfter(true);
reverseAnim.setInterpolator(new DecelerateInterpolator());
final int finalI1 = i;
reverseAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
if (finalI1 != mFirstIndex) {
int index = getNextAnimChild(false, finalI1);
int delayMillis = index < mXCount ? 100 : 50;
startReverseAnim(index, delayMillis);
}
}
@Override
public void onAnimationEnd(Animation animation) {
if (finalI1 == mFirstIndex) {
startRotateAnim(mFirstIndex, 300);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
reverseAnims.add(reverseAnim);
}
private void startRotateAnim(final int index) {
if (startAnims != null && startAnims.size() > index) {
getChildAt(index).startAnimation(startAnims.get(index));
}
}
private void startReverseAnim(final int index) {
if (reverseAnims != null && reverseAnims.size() > index) {
getChildAt(index).startAnimation(reverseAnims.get(index));
}
}
private void startRotateAnim(final int index, int delayMillis) {
postDelayed(new Runnable() {
@Override
public void run() {
startRotateAnim(index);
}
}, delayMillis);
}
private void startReverseAnim(final int index, int delayMillis) {
postDelayed(new Runnable() {
@Override
public void run() {
startReverseAnim(index);
}
}, delayMillis);
}
private int getNextAnimChild(boolean isStart, int i) {
int index;
if (isStart) {
if (i < mXCount) {
i += mFirstIndex + 1;
return i;
}
index = i - mXCount;
}else {
if (i > mFirstIndex) {
i -= mFirstIndex + 1;
return i;
}
index = i + mXCount;
}
return index;
}
其中getNextAnimChild方法为获取下一个要执行动画的子View的index。
最后根据用户设置的宽高测量子View并布局:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int minWidth = mSquareSize * (mXCount+1) + (mXCount - 1) * mDividerSize;
int minHeight = mSquareSize * (mYCount+1) + (mYCount - 1) * mDividerSize;
if (widthMode == MeasureSpec.AT_MOST || (widthMode == MeasureSpec.EXACTLY && sizeWidth < minWidth)) {
sizeWidth = minWidth;
}
if (heightMode == MeasureSpec.AT_MOST || (heightMode == MeasureSpec.EXACTLY && sizeHeight < minHeight)) {
sizeHeight = minHeight;
}
if (sizeHeight > minHeight) {
mPaddingTop = (sizeHeight - minHeight) / 2;
}
if (sizeWidth > minWidth) {
mPaddingLeft = (sizeWidth - minWidth) / 2;
}
childLayout();
setMeasuredDimension(sizeWidth, sizeHeight);
}
private void childLayout() {
int l, t, r, b;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
l = (i % mXCount + 1) * mSquareSize + (i % mXCount) * mDividerSize + mPaddingLeft;
t = (i / mXCount + 1) * mSquareSize + (i / mXCount) * mDividerSize + mPaddingTop;
r = l + mSquareSize;
b = t + mSquareSize;
child.layout(l, t, r, b);
}
}
最终效果:
查看完整源码:https://github.com/yuweiguocn/SquareLoading