Androd自定义控件(三)飞翔的小火箭

Android自定义控件之组合控件。

在前面的自定义控件概述中已经跟大家分享了Android开发当中自定义控件的种类。今天跟大家分享一个非主流的组合控件。
我们在开发当中,难免需要在不同的场合中重复使用一些控件的组合。而Java的最高目标呢,是消灭所有重复代码。这个时候怎么办呢?办法之一就是创建一个囊括逻辑和布局的视图,以便可以重复使用而不用在不同的场合中写重复的代码。代码复用的同时我们还把逻辑包装到了控件内部,做到更好的解耦。比如我们App页面中的顶栏等等。
今天呢,跟大家分享一个我前一阵子在项目中遇到的实例。先看下效果图:

需求:

  1. 该控件可以左右滑动。
  2. 底部积分是一个等差数列,可以自己定义。积分初始为半透明,小红旗下方显示设定的最大值。小火箭会飞到当前用户对应的积分位置,用户得到的积分在小火箭动画之后会显示为白色,同时当前积分位置出现一条标识线。
  3. 动画开始的时候小火箭会从0开始移动,直到当前积分位置,在移动过程中小火箭会有一个喷射火焰的效果。
  4. 背景会随着火箭的移动而移动,当动画结束的时候,保证小火箭在屏幕中心。

实现方式:

自己写一个类继承HorizontalScrollView,HorizontalScrollView会帮我们处理左右滑动的事件,否则还要重写ontouchEvent自己处理滑动。然后加载一个布局文件,给小火箭加一个帧动画和位移属性动画,实现小火箭的移动和喷火动画。同时自定义一个动画,来处理控件本身的滑动。

需要的技能点:

1.Android的view动画和属性动画,以及简单的自定义动画。
2.view的绘制流程,详情参照Androd自定义控件(一)概述
3.Activity中view的加载机制。
4.Android中dp,px等单位的概念。
5.用代码创建控件。
6.LayoutParams的使用方法。

具体实现:

初始化,在这里我们让一个参数的构造方法调用两个参数的构造方法,两个参数的构造方法调用三个参数的构造方法,把初始化的方法放到三个参数的构造方法当中。在初始化方法中加载布局文件。

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
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.point_view, this);
initView();
this.context = context;
bottomLeftMargin = UIUtil.dip2px(context, 65);
}
private void initView() {
//顶部内容区域
content = (FrameLayout) findViewById(R.id.point_content);
rocket = (ImageView) findViewById(R.id.point_rocket);
//底部标注
one = (TextView) findViewById(R.id.point_one);
two = (TextView) findViewById(R.id.point_two);
three = (TextView) findViewById(R.id.point_three);
four = (TextView) findViewById(R.id.point_four);
five = (TextView) findViewById(R.id.point_five);
six = (TextView) findViewById(R.id.point_six);
seven = (TextView) findViewById(R.id.point_seven);
pointMax = (TextView) findViewById(R.id.point_max);
mark = (LinearLayout) findViewById(R.id.point_mark);
bottom = (FrameLayout) findViewById(R.id.point_bottom);
}

布局文件

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@mipmap/point_view_bg"
android:orientation="vertical">
<!-- 内容区域 -->
<FrameLayout
android:id="@+id/point_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/point_rocket"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="85dp"
android:src="@mipmap/rocket_four" />
</FrameLayout>
<!-- 底部标注 -->
<FrameLayout
android:id="@+id/point_bottom"
android:layout_width="match_parent"
android:layout_height="57dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="7dp"
android:text="积分"
android:textColor="#fff"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/point_mark"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="0"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="300"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="600"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_four"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="900"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_five"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="1200"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_six"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="1500"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="1px"
android:layout_height="5dp"
android:background="@color/light_red" />
<TextView
android:id="@+id/point_seven"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="1800"
android:textColor="@color/zhuce"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/point_max"
android:layout_width="58dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="7dp"
android:gravity="center"
android:textColor="@color/zhuce"
android:textSize="12sp" />
<View
android:layout_width="33dp"
android:layout_height="match_parent" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</HorizontalScrollView>

然后在onlayout方法中拿到我们需要的底部标注的长度,用来计算小火箭和view动画的位移。

1
2
3
4
5
6
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
markLength = mark.getMeasuredWidth();
// L.e(TAG, "markLength---" + markLength);
}

设置显示标注线

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
/**
* 设置当前分数
*
* @param point
*/
public void setCurrentPoint(int point) {
int location;
if (point < MAX_POINT) {
location = (int) ((point / MAX_POINT) * markLength + bottomLeftMargin);//算出当前分数显示位置的偏移量
} else {
location = markLength + bottomLeftMargin + UIUtil.dip2px(context, 12);
}
//标注当前位置,
ImageView line = new ImageView(context);
line.setImageDrawable(getResources().getDrawable(R.color.point_line));
bottom.addView(line);
LayoutParams linePa = (LayoutParams) line.getLayoutParams();
linePa.leftMargin = location;
linePa.width = UIUtil.dip2px(context, 1);
linePa.height = UIUtil.dip2px(context, 58);
line.setLayoutParams(linePa);
// L.e(TAG, "location---" + location + ";bottomLeftMargin---" + bottomLeftMargin);
}

火箭的动画

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
//火箭平移动画
ObjectAnimator rocketAni = ObjectAnimator.ofFloat(rocket, "translationX", rocketX);
DecelerateInterpolator interpolator = new DecelerateInterpolator();
rocketAni.setInterpolator(interpolator);
rocketAni.setDuration(DEFAULT_DURATION);
rocketAni.start();
//火箭切换动画
rocket.setImageResource(R.drawable.rocket_frame);
final AnimationDrawable animationDrawable = (AnimationDrawable) rocket.getDrawable();
animationDrawable.start();
rocketAni.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//停止帧动画
animationDrawable.stop();
rocket.setImageResource(R.mipmap.rocket_three);
//设置当前积分标注线
setCurrentPoint(point);
//设置已经到达积分为白色
setMarkColor(point, 300);
}
});

因为scroller自带的滚动插值器与火箭动画插值器不同步,所以使用自定义动画实现控件的平滑滚动

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
/**
* 自定义动画,控制scrollview滚动
*/
public class ViewAnimation extends Animation {
private int viewX;
public ViewAnimation(int viewX) {
this.viewX = viewX;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
smoothScrollTo((int) (viewX * interpolatedTime), 0);
}
}
//view滚动动画
/**
* scroller自带的滚动插值器与火箭动画插值器不同步,所以使用自定义动画实现平滑滚动
*/
ViewAnimation viewAnimation = new ViewAnimation(finalViewX);
viewAnimation.setDuration(DEFAULT_DURATION);
viewAnimation.setInterpolator(interpolator);
this.setAnimation(viewAnimation);
viewAnimation.start();

调用

1
2
3
4
5
6
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mPoinView.setMaxPoint(2500);
mPoinView.startAni(600);
}

这里我们在onWindowFocusChanged回调中调用,保证在控件加载完成之后再设置参数。

到这里这个控件就基本完成了。其实还有很多可以优化的地方,比如把一些属性抽离出来,写成自定义属性,还有下标根据传入数组动态生成等等,有兴趣的朋友可以交流一下。源码地址。

任磊_Coder wechat
关注博主是一种态度,评论博主是一种欣赏。
坚持原创技术分享,您的支持将鼓励我继续创作!