您的位置:首页 > 移动开发 > Android开发

Android具有粘性的小球,跌落反弹形成文字的动画效果

2016-11-30 14:06 435 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。

目录(?)[+]
AdhesiveLoadingView效果项目地址
结构分析
代码分析项目地址
圆点
LoopCircleAnimator

SmallAndDropAnimator
TextAnimator
controller
实现view

总结
项目地址

  作为一个Android开发者,看到一个好的ui无疑是赏心悦目的。对于用户来,给予一个美观的ui也是非常重要的。此篇文章分析主要学习如何自定义view,同时也逐步探求一个好的loading的设计,以及animation的一种代码的设计。

  android自定义view是一个android开发者进阶的一个重要的里程碑,其实这也是离不开Animation,Animator,canvas,path,paint等等这几个类和API,所以当遇到感觉困难的地方,android官网api一定相当有帮助。掌握了这几个类,基本上酷炫的view只要有些灵感,相信是难不倒了。

  下面这一个AdhesiveLodingView工程是由一个小球循环绕圈,与周边小球形成粘性效果,放大后过重形成水珠跌落,水珠反弹形成文字的动画效果。涉及了如下几个类:

  1.Path和Canvas、Paint

  2.ValueAnimator、AnimationSet

AdhesiveLoadingView效果(项目地址)





结构分析

这个动画包括了三个过程:

  1.小球旋转放大,其中还有震动效果

  2.小球缩小衍生水滴,迅速跌落

  3.文字弹出展现

在结构上是主要是通过controller对三个animator进行一个控制,并作为其中的信息传递媒介链接各个animator,将canvas分发给animator进行绘制。而view通过controller的初始化来达到展示动画的效果。其中,动画的效果是由AnimationSet进行顺序的控制。



代码分析(项目地址)

下面就通过代码的结构来分析一下整个的一个动画过程,其中分成三个部分,也就是三个animation的一个绘制的过程。

圆点

圆点是由抽象类Circle.Java进行衍生的,正在进行运动的是WolfCircle.java,静止不动的六个小球是RabbitCircle.java。还有之后的水滴BeadCircle.



WolfCircle具有runTo()方法,这个是更改绘制角度,实现圆点运动

RabbitCircle具有state状态,这个是用来控制当状态改变,作出不同的绘制效果。

BeadCircle具有drop方法,这个是用来控制小球下跌的动作。

1. LoopCircleAnimator

  这个动画负责圆点的旋转,利用度数来绘制六个圆点,同时通过度数来绘制运动的圆点,利用塞贝尔曲线来绘制其中粘性的效果,所以这里主要是利用度数来进行圆点之间的一个距离的判定。

  这里主要的难点是,由于位置都是根据canvas的rotate进行旋转绘制,而塞贝尔曲线绘制的是在两个圆点之间,所以在旋转的时候如果位置计算不正取就会偏移。而这里通过了调整小偏移来弥补这个问题,后期进行处理。

  绘制圆点通过Canvas的drawCircle方法进行绘制。

  关于这个塞贝尔曲线以及黏着效果的实现,可以参考一下这个博客贝塞尔曲线应用及QQ气泡拖动原理实践。主要用到的方法也就是Path的quadTo,lineTo的方法。

以下是代码解析(主要的方法):

public LoopCircleAnimator(View view) {
mView = view;

initComponent();
initAnimator();

mPath = new Path();
}

/**
* 设置六个圆点以及运动的圆点
*/
private void initComponent() {

startX = Config.START_X;
startY = Config.START_Y;
centerX = Config.CENTER_X;
centerY = Config.CENTER_Y;
bigR = Config.BIG_CIRCLE_RADIUS;

// create 6 rabbits
int r = Math.min(mView.getWidth(), mView.getHeight()) / 20;
int degree = 0;
for (int i = 0; i < Config.RABBIT_NUM; i++) {
mRabbits.add(new RabbitCircle(startX, startY, r, degree));
degree += Config.DEGREE_GAP;
}

// create wolf
if (mWolf == null) {
mWolf = new WolfCircle(startX, startY, (int)(rate * r), 0);
}

}

/**
* 设置animator的参数
*/
private void initAnimator() {

this.setIntValues(0, 360);
this.setDuration(DURATION);
this.setInterpolator(new AccelerateDecelerateInterpolator());
this.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int degree = (int) animation.getAnimatedValue();
startActivities(degree);

mView.invalidate();
}
});
}

/**
* 开始运动,目的是旋转一圈,所以是从0-360度
* @param degree
*/
private void startActivities(int degree) {
mWolf.runTo(degree);
// 运动小球增大
mWolf.bigger(degree / Config.DEGREE_GAP * 2);
// 这里有一个细节动作,当运动小球靠近时,静止的圆点会进行一个震动.震动是通过设置其状态来完成的
for (RabbitCircle rabbit : mRabbits) {
if (mAliveRabbits < 6 && rabbit.getState() == RabbitCircle.DIED
&& rabbit.getDegree() < degree) {
rabbit.setState(RabbitCircle.ALIVE);
mAliveRabbits++;
}
if (mWolf.getDegree() - rabbit.getDegree() > 0 && mWolf.getDegree() - rabbit.getDegree() <= 40) {
float deg = (mWolf.getDegree() - rabbit.getDegree()) / 2f;
mPathDegree = (int) (deg + rabbit.getDegree());
int distance = (int) (Math.sin(Math.PI * deg / 180) * bigR);
updatePath(distance);
}

if (rabbit.getDegree() - mWolf.getDegree() > 0 && rabbit.getDegree() - mWolf.getDegree() < 60) {
rabbit.setState(RabbitCircle.DANGER);
} else if (rabbit.getState() == RabbitCircle.DANGER) {
rabbit.setState(RabbitCircle.ALIVE);
}
}
}

/**
* 黏着效果实现,这里的黏着效果是由4个点完成的,外加两个控制点.
*
* @param distance
*/
private void updatePath(int distance){
// TODO 塞贝尔曲线还有一些问题,由于是通过旋转角度实现两个圆点之间的链接,所以会有偏差,现在暂且通过微调解决
mPath.reset();
int x1 = startX - distance;
int y1 = startY - mRabbits.get(0).getRadius() + 2;

int x2 = startX - distance;
int y2 = startY + mRabbits.get(0).getRadius() + 1;

int x3 = startX + distance;
int y3 = startY + mWolf.getRadius() + 1;

int x4 = startX + distance;
int y4 = startY - mWolf.getRadius() + 2;

int controlX1T4 = startX;
int controlY1T4 = y1 + distance;
int controlX2T3 = startX;
int controlY2T3 = y2 - distance;

mPath.moveTo(x1, y1);
mPath.lineTo(x2, y2);
mPath.quadTo(controlX2T3, controlY2T3, x3, y3);
mPath.lineTo(x4, y4);
mPath.quadTo(controlX1T4, controlY1T4, x1, y1);
mPath.close();
}

/**
* 绘制圆点
* @param canvas
* @param paint
*/
public void draw(Canvas canvas, Paint paint) {

for (Circle rabbit : mRabbits) {
rabbit.draw(canvas, paint, centerX, centerY);
}

mWolf.draw(canvas, paint, centerX, centerY);

if (mPathDegree > 0) {
drawPath(canvas, paint);
}

}

/**
* 绘制黏着部分
* @param canvas
* @param paint
*/
public void drawPath(Canvas canvas, Paint paint) {
paint.setColor(Color.BLACK);
canvas.save();
canvas.rotate(mPathDegree, centerX, centerY);
canvas.drawPath(mPath, paint);
canvas.restore();
mPathDegree = -1;
}
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149


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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

2. SmallAndDropAnimator

  这个动画主要将运动的小球缩小,形成水滴下落的效果,这里的难点主要是一个水滴下落后的一个压缩的过程,让用户看起来的感觉是这个水滴下落后确实压扁了。这里就是通过ValueAnimator的一个参数来进行一个压扁的一个控制。还是从代码上看一下。主要看一下这个动画的设置。

/**
* 初始化动画的配置
*/
public void initAnim() {
this.setDuration(DURATION);
// flatten distance 水滴放大的距离
final int flattenDis = mixDis / 4;
final int preFlattenDis = mixDis - flattenDis;
this.setInterpolator(new AccelerateInterpolator(1.5f));
// 由于要形成一个下落压缩的效果,所以在VALUE的设置上通过参数高->低->高->恢复这样的效果来实现
this.setIntValues(Config.START_Y, Config.START_Y + mixDis, mDis - preFlattenDis, mDis + flattenDis);
this.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int curY = (int) animation.getAnimatedValue();
if (curY < mDis  - preFlattenDis) {
if (curY <= Config.START_Y + mixDis) {
// 缩小
mCircle.bigger(mixDis - curY + Config.START_Y);
// 放大
mBead.bigger(curY - Config.START_Y);
}
// 下落
mBead.drop(curY);
} else if (curY < mDis){
// 压缩
mBead.flatten(mDis + flattenDis - curY);
// 下落
mBead.drop(curY);
} else {
// 压缩
mBead.flatten(mDis + flattenDis - curY);
}
mView.invalidate();
}
});
this.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBead.reset(Config.START_X, Config.START_Y);
}
});

}
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


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

3. TextAnimator

  这个Animator主要是实现了字体弹出,向外移动的效果,这里的一个弹出的效果也是通过ValueAnimator的参数去设置的。

  绘制文字及他们向外移动的效果,主要是通过paint.measureText进行文字宽度的一个测量,从而可以进行移动。

  而向外围移动的这个是比较坑爹的,原因是canvas.drawText本身是根据左下点为起点进行绘制,将其移动到中心点进行绘制的时候字体原本的距离就会产生变化,也就是缩小了。这样就使得字体变得难看。

  解决方法:通过设置绘制文字的一个align来进行文字的绘制,同时测量文字的宽度,进行相应的移动。达到一个向外围移动的效果。

下面看一下关键的代码块:

/**
* 初始化参数
*/
private void initConfig() {
initWord();
curIndex = 0;
mTextSize = mView.getWidth() / (STR.length() - 2);
mBaseLine = Config.BASELINE;
mScaleSize = 30;
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
texts = new Text[word.length];
// 初始化各个字母运动的方向
boolean toRight = false;
for (int i = 0; i < word.length; i++) {
// 向左运动
if (!toRight) {
texts[i] = new Text(word[i], 0, Config.CENTER_X, Text.DIRECTION_LEFT);
toRight = true;
} else {
if (i + 1 == word.length) {
// 居中不动
texts[i] = new Text(word[i], 0, Config.CENTER_X, Text.DIRECTION_CENTER);
} else {
// 向右运动
texts[i] = new Text(word[i], 0, Config.CENTER_X, Text.DIRECTION_RIGHT);
}
toRight = false;
}

}
}

/**
* 初始化word的顺序,例如loading->lgonaid,方便进行动画
*/
protected void initWord() {
word = new String[STR.length()];
int a = 0;
int b = word.length - 1;
int i = 0;
while (a <= b) {
if (a == b) {
word[i] = String.valueOf(STR.charAt(a));
} else {
word[i] = String.valueOf(STR.charAt(a));
word[i + 1] = String.valueOf(STR.charAt(b));
}
a++;
b--;
i += 2;
}
}

protected void initAnim() {
// 通过设置参数来实现字体的大小变化,从而实现弹出放大的效果.
this.setIntValues(0, mTextSize, mTextSize + mScaleSize, 0, mTextSize + mScaleSize / 2, mTextSize);
this.setDuration(DURATION);
this.setInterpolator(new AccelerateInterpolator());
this.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
curTextSize = (int) animation.getAnimatedValue();
if (curIndex > 0) {
texts[curIndex - 1].setSize(curTextSize);
}
mView.invalidate();
}
});
this.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// 定格最后字母的大小
if (curIndex > 0 && curIndex <= word.length) {
texts[curIndex - 1].setSize(mTextSize);
}
int tmpIndex = curIndex;
while (tmpIndex - 3 >= 0) {
texts[tmpIndex - 3].setExtraX(mPaint.measureText(texts[curIndex - 1].content));
tmpIndex -= 2;
}
curIndex++;
if (curIndex > word.length) {
curIndex = 1;
resetText();
}
}
});
}

public void draw(Canvas canvas, Paint paint) {
if (curIndex < 1 || curIndex > word.length) {
return;
}
for (int i = 0; i < curIndex; i++) {
paint.setTextSize(texts[i].size);
if (i == curIndex - 1) {
paint.setTextAlign(Paint.Align.CENTER);
// 绘制中间的字母
canvas.drawText(texts[i].content, texts[i].x, mBaseLine, paint);
} else {
// 由于文字绘制的原点影响了文字的间距,因为文字的宽度都是通过align.right进行一个间距的计算的,所以
// 当以中心为绘制原点的时候,相同的间距会变成原来的一半,这样就会导致间距缩小,尤其是小字体像i等,所以通过
// 设置不同的绘制原点,加上不同的位移来解决这个问题.
paint.setTextAlign(Paint.Align.LEFT);
if (texts[i].direction == Text.DIRECTION_RIGHT) {
canvas.drawText(texts[i].content,
texts[i].x + paint.measureText(word[curIndex - 1]) / 2 + texts[i].extraX, mBaseLine, paint);
} else if (texts[i].direction == Text.DIRECTION_LEFT) {
canvas.drawText(texts[i].content,
texts[i].x - paint.measureText(word[i]) - paint.measureText(word[curIndex - 1]) / 2 + texts[i].extraX, mBaseLine, paint);
}
}
}
}

/**
* 重置文字的距离
*/
private void resetText() {
if (texts != null) {
for (Text text : texts) {
text.extraX = 0;
}
}
}

/**
* 字母状态
*/
private class Text{
public final static int DIRECTION_RIGHT = 1;
public final static int DIRECTION_LEFT = 2;
public final static int DIRECTION_CENTER = 3;

public String content;
public int size;
public float x;
public int direction;
public float extraX;

public Text(String content, int size , float x, int direction) {
this.content = content;
this.size = size;
this.x = x;
this.direction = direction;
this.extraX = 0;
}

public void setSize(int size) {
this.size = size;
}

public void setExtraX(float extra) {
if (direction == DIRECTION_LEFT) {
this.extraX -= extra;
} else if (direction == DIRECTION_RIGHT) {
this.extraX += extra;
}
}
}
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161


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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

4. controller

最后就是用一个animationset来将各个animator顺序调用了,同时将canvas分发出去。上Controller的关键代码

public Controller(View view) {

initConfig(view);

mAnimSet = new AnimatorSet();
mLoopCircleAnim = new LoopCircleAnimator(view);
mSapAnim = new SmallAndDropAnimator(view, mLoopCircleAnim.getWolf());
mTextAnim = new TextAnimator(view);
// 顺序播放
mAnimSet.playSequentially(mLoopCircleAnim, mSapAnim, mTextAnim);
mAnimSet.start();
mAnimSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimSet.start();
}
});
}
public void draw(Canvas canvas, Paint paint) {
mLoopCircleAnim.draw(canvas, paint);
mSapAnim.draw(canvas, paint);
mTextAnim.draw(canvas, paint);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

5. 实现view

最后通过一个实现view来装载controller。这个可以看源码部分。这个项目就到此为止了

总结

其实,自定义view离不开canvas,path,paint,value animator,animation等等,只要将这些api熟练运用,加上由好的代码规范和逻辑,自定义view湿湿水啊!慢慢积累慢慢进步!

(项目地址)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐