• android-自定义弹幕layout的实现


    以前工作中的代码,整理成一个自定义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的实现提供一种思路。在上面代码的基础上,我们可以拓展,优化,或者做成独立第三方“弹幕控件库”。

    大佬们有啥看法的,欢迎留言交流。

     

  • 相关阅读:
    CentOS7与CentOS8一些区别
    windows下bat脚本记录
    windows server AD增加自定义属性
    vsphere6.7为虚拟机添加硬盘报“目标数据存储 不在存储容器中。”错误
    linux 常用的命令
    CentOS7开机无法启动,报 Failed to load SELinux policy. Freezing错误
    SpringCloudAlibaba笔记06
    SpringCloudAlibaba笔记05
    接触CrackMe 第一个
    HOOK钩子
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/8998449.html
Copyright © 2020-2023  润新知