以前工作中的代码,整理成一个自定义layout,写在这里,供各位大佬参考。
其中涉及到的主要困难/技术点包括:
1) 属性动画控制控件的平移动画(控件的移动过程,用ObjectAnimator控制)
2) RelativeLayout.LayoutParameter控制控件的初始位置(新生成的弹幕可能是 TextView 或者 ImageView,我需要在弹幕出现之前,将他们“刚好”隐藏在布局的右侧)
3) ViewTreeObserver.OnGlobalLayoutListener监听控件树来 获取控件的真实宽高(获取控件真实宽高的方法有多种,我经过尝试,这种ViewTreeObserver的方式,对于 动态生成的弹幕控件最为准确)
下面会给出所有关键代码,一些涉及到程序思路,或者技术难点的部分代码,我加了详细注释,相信大家都能看得懂。(下面的效果图中有一个“飞机”,那是我随便下载的一个png );
先看效果:
使用方法也极为简单,只需要在layout.xml和MainActivity.java里面增加相关代码 。
请看下方代码:
MainActivity.java
1 package com.example.danmulayout;
2
3 import android.graphics.Color;
4 import android.os.Bundle;
5 import android.support.v7.app.AppCompatActivity;
6 import android.view.View;
7 import android.widget.Button;
8
9 import com.example.danmulayout.custom.DanMu;
10 import com.example.danmulayout.custom.DanMuColor;
11 import com.example.danmulayout.custom.MyDanMuLayout;
12
13 /**
14 * 本例,旨在做一个我自己的弹幕 Layout 作为开源库
15 */
16 public class MainActivity extends AppCompatActivity {
17
18 private MyDanMuLayout dl_my_danMu;
19 private Button btn_send, btn_send_plane, btn_hide, btn_show;
20
21 @Override
22 protected void onCreate(Bundle savedInstanceState) {
23 super.onCreate(savedInstanceState);
24 setContentView(R.layout.activity_main);
25 init();
26 }
27
28 private void init() {
29 dl_my_danMu = findViewById(R.id.dl_my_danmu);
30 btn_send = findViewById(R.id.btn_send);
31 btn_hide = findViewById(R.id.btn_hide);
32 btn_show = findViewById(R.id.btn_show);
33 btn_send_plane = findViewById(R.id.btn_send_plane);
34
35 btn_send.setOnClickListener(new View.OnClickListener() {
36 @Override
37 public void onClick(View v) {
38 dl_my_danMu.sendDanMu(DanMu.getRandomDanMuContentInPresets(), DanMuColor.getRandomColorInPresets(), Color.TRANSPARENT);
39 }
40 });
41 btn_send_plane.setOnClickListener(new View.OnClickListener() {
42 @Override
43 public void onClick(View v) {
44 dl_my_danMu.sendPlane();
45 }
46 });
47 btn_hide.setOnClickListener(new View.OnClickListener() {
48 @Override
49 public void onClick(View v) {
50 dl_my_danMu.hideDanMu();
51 }
52 });
53 btn_show.setOnClickListener(new View.OnClickListener() {
54 @Override
55 public void onClick(View v) {
56 dl_my_danMu.showDanMu();
57 }
58 });
59 }
60
61 @Override
62 protected void onDestroy() {
63 super.onDestroy();
64 dl_my_danMu.release();
65 }
66 }
activity_main.xml
1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:id="@+id/rl_main"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:orientation="vertical"
8 android:padding="5dp"
9 tools:context=".MainActivity">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:orientation="horizontal">
15
16 <Button
17 android:id="@+id/btn_send"
18 android:layout_width="wrap_content"
19 android:layout_height="wrap_content"
20 android:layout_margin="1dp"
21 android:text="发自动弹幕" />
22
23 <Button
24 android:id="@+id/btn_send_plane"
25 android:layout_width="wrap_content"
26 android:layout_height="wrap_content"
27 android:layout_margin="1dp"
28 android:text="送飞机" />
29
30 <Button
31 android:id="@+id/btn_hide"
32 android:layout_width="wrap_content"
33 android:layout_height="wrap_content"
34 android:layout_margin="1dp"
35 android:text="隐藏" />
36
37 <Button
38 android:id="@+id/btn_show"
39 android:layout_width="wrap_content"
40 android:layout_height="wrap_content"
41 android:layout_margin="1dp"
42 android:text="显示" />
43 </LinearLayout>
44
45
46 <com.example.danmulayout.custom.MyDanMuLayout
47 android:id="@+id/dl_my_danmu"
48 android:layout_width="match_parent"
49 android:layout_height="match_parent"
50 android:background="@android:color/darker_gray"></com.example.danmulayout.custom.MyDanMuLayout>
51
52 </LinearLayout>
两个文件的代码行数总共加起来不超过200行。
下面列出其他关键代码:
MyDanMuLayout.java
1 package com.example.danmulayout.custom;
2
3 import android.animation.Animator;
4 import android.animation.AnimatorListenerAdapter;
5 import android.animation.ObjectAnimator;
6 import android.content.Context;
7 import android.graphics.drawable.Drawable;
8 import android.os.Handler;
9 import android.util.AttributeSet;
10 import android.util.Log;
11 import android.view.View;
12 import android.view.ViewTreeObserver;
13 import android.view.animation.LinearInterpolator;
14 import android.widget.ImageView;
15 import android.widget.RelativeLayout;
16 import android.widget.TextView;
17
18 import com.example.danmulayout.R;
19
20 import java.util.LinkedList;
21 import java.util.Queue;
22 import java.util.Timer;
23 import java.util.TimerTask;
24
25 public class MyDanMuLayout extends RelativeLayout {
26
27 private Context mContext;
28
29 /*********************重写构造函数********************/
30 public MyDanMuLayout(Context context) {
31 this(context, null);
32 }
33
34 public MyDanMuLayout(Context context, AttributeSet attrs) {
35 this(context, attrs, 0);
36 }
37
38 public MyDanMuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
39 super(context, attrs, defStyleAttr);
40 mContext = context;
41 setWidthObserverOfLayout();
42 }
43
44 private int measuredWidthOfDanMuView;
45 private int layoutWidth, layoutHeight;
46 private Handler mHandler = new Handler();
47
48 /**
49 * 全局监听,为了得出自定义layout的实际宽度和高度(这个宽度会用来计算弹幕需要移动多长距离,高度会用来计算 每条弹幕离顶部的距离)
50 */
51 private void setWidthObserverOfLayout() {
52 //以本layout为根进行监听,当它的布局完成之后,执行逻辑,并且移除监听器
53 ViewTreeObserver observer = this.getViewTreeObserver();
54 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {// 当全局布局发生变化,或者子view的可见状态发生变化时,它就会被回调
55
56 @SuppressWarnings("deprecation")
57 @Override
58 public void onGlobalLayout() {//当检测到布局变化时,这个监听器就会回调
59 Log.d("WidthObserverOfLayout", "执行widthObserverOfLayout");
60 getViewTreeObserver().removeGlobalOnLayoutListener(this);//一旦被回调,立即移除监听,不然就会无限执行
61 layoutWidth = getMeasuredWidth(); //获得当前自定义layout的实际宽度
62 layoutHeight = getMeasuredHeight();//实际高度
63 startDanMuLooper();//开始队列循环
64 }
65 });
66 }
67
68
69 //在这里设计一个队列机制,目的就是 将外界传进来的弹幕信息,都封装成Danmu对象,然后队列无限循环,显示弹幕
70 private Queue<DanMu> danMuQueue = new LinkedList<>();
71 private Timer timer;
72
73 /**
74 * 开始弹幕队列循环
75 */
76 private void startDanMuLooper() {
77 if (null != timer) {
78 timer.cancel();
79 timer.purge();
80 }
81 timer = new Timer();
82 timer.schedule(new TimerTask() {
83 @Override
84 public void run() {
85 //这里有一个无限循环的timer,它会无限地将danmuQueue中的弹幕,
86 final DanMu danmu = danMuQueue.poll();
87 if (null != danmu) {
88 mHandler.post(new Runnable() {
89 @Override
90 public void run() {
91 initDanMu(mContext, danmu.content, danmu.distanceToTop, danmu.duration, danmu.textColor, danmu.bgColor, danmu.drawable);
92 }
93 });
94 }
95 }
96 }, 50, 100);//每100毫秒检测一次队列里面有没有弹幕
97 }
98
99 /**
100 * 开始对View进行动画处理
101 *
102 * @param danMuView
103 * @param duration
104 * @param distanceToTopPercent
105 */
106 private void animate(final View danMuView, final int duration, final float distanceToTopPercent) {
107 ViewTreeObserver observer = danMuView.getViewTreeObserver();//增加监听, 为了获得弹幕view的实际宽度
108 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
109 @Override
110 public void onGlobalLayout() {
111 danMuView.getViewTreeObserver().removeOnGlobalLayoutListener(this);//一旦被回调,立即移除监听,不然就会无限执行
112 measuredWidthOfDanMuView = danMuView.getMeasuredWidth();//得出测量之后的宽度值
113
114 // 借助RelativeLayout.LayoutParam 让它总是在右侧外围
115 RelativeLayout.LayoutParams layoutParams = (LayoutParams) danMuView.getLayoutParams();
116 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
117 layoutParams.topMargin = (int) (distanceToTopPercent * layoutHeight);//设定离父组件最上方的距离
118 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
119 layoutParams.rightMargin = -measuredWidthOfDanMuView;
120 danMuView.requestLayout();//强制刷新布局
121
122 //然后执行动画
123 startMovement(danMuView, duration);//拿到宽度之后才能进行动画执行,因为弹幕textview的移动距离依赖 layoutWidth
124 }
125 });
126 }
127
128 /**
129 * 弹幕初始化
130 */
131 private void initDanMu(Context ctx, String danMuContent, final float distanceToTopPercent, final int duration, int textColor, int bgColor, Drawable drawable) {
132 View danMuView;
133 if (drawable == null) {//先看看有没有图,有图的话,就创建ImageView,否则就创建TextView
134
135 TextView tv_danMuTemp = new TextView(ctx);//创建一个弹幕textView,这里的TextView是原生的,其实我可以做成自定义的TextView
136 tv_danMuTemp.setText(danMuContent);//设置文字
137 tv_danMuTemp.setTextColor(textColor);//设置文字颜色
138 tv_danMuTemp.setBackgroundColor(bgColor);//设置边框颜色
139
140 danMuView = tv_danMuTemp;
141
142 } else {
143 ImageView iv_danMuTemp = new ImageView(ctx);
144 iv_danMuTemp.setImageDrawable(drawable);
145 iv_danMuTemp.setMaxWidth(200);
146 iv_danMuTemp.setMaxHeight(200);
147
148 danMuView = iv_danMuTemp;
149 }
150
151 //下面的代码是为了将新产生的view 刚好 隐藏到本layout最右边
152 RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);//设置初始布局
153 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);//相对布局的参数:靠右
154 layoutParams.rightMargin = -layoutWidth;//并且继续往右,直到隐藏
155 addView(danMuView, layoutParams);//添加弹幕textView
156
157 animate(danMuView, duration, distanceToTopPercent);
158 }
159
160
161 /**
162 * 开始移动
163 *
164 * @param tv_danMuTemp
165 */
166 private void startMovement(final View tv_danMuTemp, int duration) {
167 float targetDistance = layoutWidth + measuredWidthOfDanMuView;// 计算总共需要移动的距离
168 ObjectAnimator animator = ObjectAnimator.ofFloat(tv_danMuTemp, "translationX", -targetDistance);//定义移动动画ObjectAnimator
169 animator.setDuration(duration * 1000);//时长
170 animator.setInterpolator(new LinearInterpolator());//匀速移动
171 animator.addListener(new AnimatorListenerAdapter() {// 动画监听器,重写适配器
172 @Override
173 public void onAnimationEnd(Animator animation) {
174 removeView(tv_danMuTemp);//一旦动画执行完毕,立即移除该view
175 Log.d("onAnimationEnd", "getChildCount:" + getChildCount());//打印移除之后还剩下多少
176 }
177 });
178 animator.start();//一切就绪,开始动画
179 }
180
181 /******************************************************* 以下是所有公开接口 ************************************************/
182
183 /**
184 * 向外开放一个接口,发送弹幕
185 *
186 * @param content 文字内容
187 * @param textColor 文字颜色
188 * @param bgColor textView背景颜色
189 */
190 public void sendDanMu(String content, int textColor, int bgColor) {
191 DanMu danmu = new DanMu();
192 danmu.content = content;
193 danmu.distanceToTop = (float) Math.random();
194 danmu.duration = 10;
195 danmu.textColor = textColor;
196 danmu.bgColor = bgColor;
197 danMuQueue.add(danmu);//将弹幕对象添加到队列中
198 }
199
200 /**
201 * 送飞机
202 */
203 public void sendPlane() {
204 DanMu danmu = new DanMu();
205 danmu.distanceToTop = (float) Math.random();
206 danmu.duration = 10;
207 danmu.drawable = mContext.getResources().getDrawable(R.drawable.plane);
208 danMuQueue.add(danmu);//将弹幕对象添加到队列中
209 }
210
211 /**
212 * 隐藏所有弹幕view
213 */
214 public void hideDanMu() {
215 for (int i = 0; i < getChildCount(); i++) {
216 getChildAt(i).setVisibility(View.INVISIBLE);
217 }
218 }
219
220 /**
221 * 显示所有弹幕view
222 */
223 public void showDanMu() {
224 for (int i = 0; i < getChildCount(); i++) {
225 getChildAt(i).setVisibility(View.VISIBLE);
226 }
227 }
228
229 /**
230 * 引用本layout的activity,在onDestroy中一定要调用资源释放接口
231 */
232 public void release() {
233 if (null != timer) {
234 timer.cancel();
235 timer.purge();
236 }
237 }
238
239 }
DanMu.java
1 package com.example.danmulayout.custom;
2
3
4 import android.graphics.drawable.Drawable;
5
6 /**
7 *
8 */
9 public class DanMu {
10 public String content;//弹幕的文字内容
11 public float distanceToTop;//距离顶端的比例(0-1)
12 public int duration;//显示时长,单位是秒
13 public int textColor;//文字颜色
14 public int bgColor;//边框颜色
15 public Drawable drawable;
16
17
18 public static String getRandomDanMuContentInPresets(){
19 String result;
20
21 int n = (int) (10 * Math.random());
22 switch (n) {
23 case 1:
24 case 2:
25 result = "6666666";
26 break;
27 case 3:
28 case 4:
29 result = "不要放弃,加油,你是最胖的!";
30 break;
31 case 5:
32 case 6:
33 result = "一条大河啊啊啊啊向西流";
34 break;
35 case 7:
36 case 8:
37 result = "冷静冷静";
38 break;
39 case 9:
40 result = "卧槽什么鬼,吓我一跳";
41 break;
42 default:
43 result = "2333333";
44 }
45 return result;
46 }
47 }
DanMuColor.java
1 package com.example.danmulayout.custom;
2
3 import android.graphics.Color;
4
5 public class DanMuColor {
6 public static final int RED = Color.RED;
7 public static final int BLUE = Color.BLUE;
8 public static final int YELLOW = Color.YELLOW;
9 public static final int GREEN = Color.GREEN;
10 public static final int ORANGE = Color.parseColor("#FFAA00");
11
12 /**
13 * 返回上面预设中的任意一个颜色
14 *
15 * @return
16 */
17 public static int getRandomColorInPresets() {
18 int result;
19
20 int n = (int) (10 * Math.random());
21 switch (n) {
22 case 1:
23 case 2:
24 result = RED;
25 break;
26 case 3:
27 case 4:
28 result = BLUE;
29 break;
30 case 5:
31 case 6:
32 result = YELLOW;
33 break;
34 case 7:
35 case 8:
36 result = GREEN;
37 break;
38 case 9:
39 result = ORANGE;
40 break;
41 default:
42 result = BLUE;
43 }
44 return result;
45 }
46
47 }
本文只为 弹幕layout的实现提供一种思路。在上面代码的基础上,我们可以拓展,优化,或者做成独立第三方“弹幕控件库”。
大佬们有啥看法的,欢迎留言交流。