1. 程式人生 > >Android 自定義彈幕控制元件

Android 自定義彈幕控制元件

原理概述

繼承自FrameLayout新增控制元件,然後開啟動畫
如果要詳細一點大體流程就是:

  1. 初始化一個彈幕View
  2. 確認彈幕View位置
  3. 新增到父佈局
  4. 開啟動畫/定時任務
  5. 動畫結束/定時任務開始執行,移除彈幕View

滾動彈幕需要動畫效果,頂部和底部的彈幕不需要動畫效果只要開啟定時任務時間到了移除就可以了

效果圖

程式碼


import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.
annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.
ViewGroup; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 彈幕控制元件 * * @author wkk */ public class BulletScreenView extends
FrameLayout { private int lv = 0;//滾動彈幕共有幾行可用 private int maxLv = 0;//最多可以有幾行 private int height;//每一行的高度 private Paint paint = new Paint(); @SuppressLint("UseSparseArrays") private Map<Integer, Temporary> map = new HashMap<>();//每一行最後的動畫 private List<Temporary> list = new ArrayList<>();//存有當前螢幕上的所有動畫 @SuppressLint("UseSparseArrays") private Map<Integer, CountDown> tbMap = new HashMap<>();//key 行數 private List<CountDown> countDownList = new ArrayList<>();//快取所有倒計時 private int textSize = 14; private boolean stop = false;//暫停功能 public BulletScreenView(Context context) { this(context, null); } public BulletScreenView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //設定文字大小 paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, getContext().getResources().getDisplayMetrics())); } @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); height = (int) (paint.measureText("我") + 10);//測量一行的高度 lv = getHeight() / height;//最多可以存在多少行 maxLv = lv; lv = maxLv / 2;//限制滾動彈幕位置 } //新增一條滾動彈幕 public void add(String string) { if (stop) { return; } //建立控制元件 final TextView textView = new TextView(getContext()); textView.setText(string); textView.setTextSize(textSize); textView.setTextColor(Color.WHITE); addView(textView); //找到合適插入到行數 float minPosition = Integer.MAX_VALUE;//最小的位置 int minLv = 0;//最小位置的行數 for (int i = 0; i < lv; i++) { Temporary temporary = map.get(i);//獲取到該行最後一個動畫 if (temporary == null) { minLv = i; break; } float p = (float) map.get(i).animation.getAnimatedValue() + map.get(i).viewLength;//獲取位置 if (minPosition > p) { minPosition = p; minLv = i; } } //設定行數 LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); if (layoutParams == null) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } layoutParams.topMargin = height * minLv; textView.setLayoutParams(layoutParams); //設定動畫 final ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView, "translationX", getWidth(), -paint.measureText(string)); objectAnimator.setDuration(7000);//設定動畫時間 objectAnimator.setInterpolator(new LinearInterpolator());//設定差值器 //將彈幕相關資料快取起來 final Temporary temporary = new Temporary(objectAnimator); temporary.time = 0; temporary.viewLength = paint.measureText(string); list.add(temporary); map.put(minLv, temporary); //動畫結束監聽 objectAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (!stop) { removeView(textView);//移除控制元件 list.remove(temporary);//移除快取 } } }); objectAnimator.start();//開啟動畫 } //新增一條彈幕 public void add(String str, Type type) { if (stop) { return; } if (type == Type.ROLL) { add(str); return; } int minLv = 0; View view = null; switch (type) { case TOP: { final TextView textView = new TextView(getContext()); textView.setText(str); textView.setTextSize(textSize); textView.setTextColor(Color.GREEN); //確定位置 long minTime = Integer.MAX_VALUE; for (int i = 0; i < lv; i++) { CountDown countDown = tbMap.get(i); if (countDown == null) { minLv = i; break; } if (countDown.over) { minLv = i; break; } //剩餘時間最小的 long st = countDown.getSurplusTime(); if (minTime > st) { minTime = st; minLv = i; } } LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); if (layoutParams == null) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; layoutParams.topMargin = height * minLv; textView.setLayoutParams(layoutParams); addView(textView); view = textView; } break; case BOTTOM: { final TextView textView = new TextView(getContext()); textView.setText(str); textView.setTextSize(textSize); textView.setTextColor(Color.RED); long minTime = Integer.MAX_VALUE; for (int i = maxLv - 1; i >= 0; i--) { CountDown countDown = tbMap.get(i); if (countDown == null) { minLv = i; break; } if (countDown.over) { minLv = i; break; } //剩餘時間最小的 long st = countDown.getSurplusTime(); if (minTime > st) { minTime = st; minLv = i; } } LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); if (layoutParams == null) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; layoutParams.bottomMargin = height * (maxLv - minLv); textView.setLayoutParams(layoutParams); addView(textView); view = textView; } break; } CountDown countDown = new CountDown(view); tbMap.put(minLv, countDown); countDownList.add(countDown); } //停止動畫 public void stop() { if (stop) { return; } stop = true; for (int i = 0; i < list.size(); i++) { Temporary temporary = list.get(i); temporary.time = temporary.animation.getCurrentPlayTime(); temporary.animation.cancel();//會呼叫結束介面 } for (CountDown countDown : countDownList) { countDown.stop(); } } //重新開始 public void restart() { if (!stop) { return; } stop = false; for (Temporary temporary : list) { temporary.animation.start(); temporary.animation.setCurrentPlayTime(temporary.time); } for (CountDown countDown : countDownList) { countDown.restart(); } } //清除全部 public void clear() { map.clear(); tbMap.clear(); list.clear(); countDownList.clear(); removeAllViews(); } private static class Temporary {//方便快取動畫 long time; float viewLength; ObjectAnimator animation; Temporary(ObjectAnimator animation) { this.animation = animation; } } public enum Type {//彈幕型別 TOP,//頂部彈幕 BOTTOM,//底部彈幕 ROLL//滾動彈幕 } private class CountDown {//為了方便暫停,所以寫了這個類用於頂部和底部的彈幕暫停恢復 long startTime; private long surplusTime = 0;//暫停過後的剩餘時間 long sustain = 1000 * 3;//持續時間 boolean over = false;//任務是否執行完成 Runnable runnable; CountDown(final View view) { startTime = System.currentTimeMillis(); runnable = new Runnable() { @Override public void run() { countDownList.remove(CountDown.this); removeView(view); over = true; } }; postDelayed(runnable, 3000);//直接開始 } //暫停當前倒計時任務 void stop() { if (over) { return; } surplusTime = sustain - (System.currentTimeMillis() - startTime);//剩餘時間=需要顯示的時間 - (當前時間 - 開始時間) sustain = surplusTime; removeCallbacks(runnable);//暫停移除任務 } //恢復倒計時任務 void restart() { if (over) { return; } startTime = System.currentTimeMillis();//重置開始時間 postDelayed(runnable, surplusTime); } //獲取剩餘時間 long getSurplusTime() { surplusTime = sustain - (System.currentTimeMillis() - startTime); sustain = surplusTime; return surplusTime; } } }

結語

整體流程程式碼不算複雜,由add方法新增彈幕為切入點,原理跟著流程走看註釋就能明白.

需要注意的是此處使用的是屬性動畫,為什麼要選擇屬性動畫呢?

  1. 使彈幕可點選
  2. 使彈幕可暫停,可重新啟動

使用屬性動畫,如果後期需要增加點贊之類的功能方便擴充套件,我在這裡只是簡單的使用實現一下.

關於彈幕暫停,恢復,為了配合視訊的暫停回覆以及Activity的生命週期,儲存下彈幕的資訊,然後恢復.

關於插入到哪一行,肯定是儘量防止彈幕的覆蓋,所以優先插入沒有彈幕的,和該行最後一條彈幕的執行時間最長的,距離結束最短的

關於其他屬性的封裝,例如滾動速度,行數,字型大小之類的都可以進行封裝,這裡我寫的只是demo就不做更多事情了

我一開始寫這個控制元件的時候就是用補間動畫實現的,後來為了彈幕的暫停恢復功能就改用了屬性動畫實現.

還有就是在這裡特意提一下這個方法:


    //獲取translateAnimation執行時的座標
    private float getPosition(TranslateAnimation translateAnimation) {
        translateAnimation.getTransformation(AnimationUtils.currentAnimationTimeMillis(), transformation);
        Matrix matrix = transformation.getMatrix();
        float[] matrixValues = new float[9];
        matrix.getValues(matrixValues);
        return matrixValues[2];
    }

是我在用補間動畫實現彈幕,計算滾動彈幕位置的時候用到的,補間動畫不像屬性動畫提供的了可以直接獲取值的方法,要獲取座標就要獲取Matrix的中的值.這個獲取值的方法不是我想出來的,參考:
https://www.cnblogs.com/hithlb/p/3554919.html