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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值