天天看點

Android 自定義彈幕控件

原理概述

繼承自FrameLayout添加控件,然後開啟動畫

如果要詳細一點大體流程就是:

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

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

效果圖

Android 自定義彈幕控件

代碼

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