Android自制弹幕

好久没有写过文章,最近发现直播特别的火,很多app都集成了直播的功能,发现有些直播是带有弹幕的,效果还不错,今天心血来潮,特地写了篇制作弹幕的文章.


今天要实现的效果如下:
1.弹幕垂直方向固定

这里写图片描述

2.弹幕垂直方向随机

这里写图片描述


上面效果图中白色的背景就是弹幕本身,是一个自定义的FrameLayout,我这里是为了更好的展示弹幕的位置才设置成了白色,当然如果是叠加在VideoView上的话,就需要设置成透明色了.
制作弹幕需要考虑以下几点问题:
1.弹幕的大小可以随意调整
2.弹幕内移动的item(或者称字幕)出现的位置,水平方向是从屏幕右边移动到屏幕左边,垂直方向是不能超出弹幕本身的高度的.
3.字幕移除屏幕后,需要将对应item(字幕)从其父容器(弹幕)中移除.
4.如果字幕出现的垂直方向的高度是随机的,那么还需要避免字幕重叠的情况.

ok,下面是弹幕自定义view的代码:

/**
 * Created by dell on 2016/9/28.
 */
public class DanmuView extends FrameLayout {
    private static final String TAG = "DanmuView";
    private static final long DEFAULT_ANIM_DURATION = 6000; //默认每个动画的播放时长
    private static final long DEFAULT_QUERY_DURATION = 3000; //遍历弹幕的默认间隔
    private LinkedList<View> mViews = new LinkedList<>();//弹幕队列
    private boolean isQuerying;
    private int mWidth;//弹幕的宽度
    private int mHeight;//弹幕的高度
    private Handler mUIHandler = new Handler();
    private boolean TopDirectionFixed;//弹幕顶部的方向是否固定
    private Handler mQueryHandler;
    private int mTopGravity = Gravity.CENTER_VERTICAL;//顶部方向固定时的默认对齐方式

    public void setHeight(int height) {
        mHeight = height;
    }

    public void setWidth(int width) {
        mWidth = width;
    }

    public void setTopGravity(int gravity) {
        this.mTopGravity = gravity;
    }

    public void add(List<Danmu> danmuList) {
        for (int i = 0; i < danmuList.size(); i++) {
            Danmu danmu = danmuList.get(i);
            addDanmuToQueue(danmu);
        }
    }

    public void add(Danmu danmu) {
        addDanmuToQueue(danmu);
    }

    public DanmuView(Context context) {
        this(context, null);
    }

    public DanmuView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DanmuView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        HandlerThread thread = new HandlerThread("query");
        thread.start();
        //循环取出弹幕显示
        mQueryHandler = new Handler(thread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                final View view = mViews.poll();
                if (null != view) {
                    mUIHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            //添加弹幕
                            showDanmu(view);
                        }
                    });
                }
                sendEmptyMessageDelayed(0, DEFAULT_QUERY_DURATION);
            }
        };
    }

    /**
     * 将要展示的弹幕添加到队列中
     *
     * @param danmu
     */
    private void addDanmuToQueue(Danmu danmu) {
        if (null != danmu) {
            final View view = View.inflate(getContext(), R.layout.layout_danmu, null);
            TextView usernameTv = (TextView) view.findViewById(R.id.tv_username);
            TextView infoTv = (TextView) view.findViewById(R.id.tv_info);
            ImageView headerIv = (ImageView) view.findViewById(R.id.iv_header);
            usernameTv.setText(danmu.getUserName());//昵称
            infoTv.setText(danmu.getInfo());//信息
            Glide.with(getContext()).//头像
                    load(danmu.getHeaderUrl()).
                    transform(new CropCircleTransformation(getContext())).into(headerIv);
            view.measure(0, 0);
            //添加弹幕到队列中
            mViews.offerLast(view);
        }
    }

    /**
     * 播放弹幕
     *
     * @param topDirectionFixed 弹幕顶部的方向是否固定
     */
    public void startPlay(boolean topDirectionFixed) {
        this.TopDirectionFixed = topDirectionFixed;
        if (mWidth == 0 || mHeight == 0) {
            getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @SuppressLint("NewApi")
                @Override
                public void onGlobalLayout() {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    if (mWidth == 0) mWidth = getWidth() - getPaddingLeft() - getPaddingRight();
                    if (mHeight == 0) mHeight = getHeight() - getPaddingTop() - getPaddingBottom();
                    if (!isQuerying) {
                        mQueryHandler.sendEmptyMessage(0);
                    }
                }
            });
        } else {
            if (!isQuerying) {
                mQueryHandler.sendEmptyMessage(0);
            }
        }
    }

    /**
     * 显示弹幕,包括动画的执行
     *
     * @param view
     */
    private void showDanmu(final View view) {
        isQuerying = true;
        Log.d(TAG, "mWidth:" + mWidth + " mHeight:" + mHeight);
        final LayoutParams lp = new LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight());
        lp.leftMargin = mWidth;
        if (TopDirectionFixed) {
            lp.gravity = mTopGravity | Gravity.LEFT;
        } else {
            lp.gravity = Gravity.LEFT | Gravity.TOP;
            lp.topMargin = getRandomTopMargin(view);
        }
        view.setLayoutParams(lp);
        view.setTag(lp.topMargin);
        //设置item水平滚动的动画
        ValueAnimator animator = ValueAnimator.ofInt(mWidth, -view.getMeasuredWidth());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                lp.leftMargin = (int) animation.getAnimatedValue();
                view.setLayoutParams(lp);
            }
        });
        addView(view);//显示弹幕
        animator.setDuration(DEFAULT_ANIM_DURATION);
        animator.setInterpolator(new LinearInterpolator());
        animator.start();//开启动画
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                view.clearAnimation();
                existMarginValues.remove(view.getTag());//移除已使用过的顶部边距
                removeView(view);//移除弹幕
                animation.cancel();
            }
        });
    }

    //记录当前仍在显示状态的弹幕的垂直方向位置(避免重复)
    private Set<Integer> existMarginValues = new HashSet<>();
    private int linesCount;
    private int range = 10;

    private int getRandomTopMargin(View view) {
        //计算可用的行数
        linesCount = mHeight / view.getMeasuredHeight();
        if (linesCount <= 1) {
            linesCount = 1;
        }
        Log.d(TAG, "linesCount:" + linesCount);
        //检查重叠
        while (true) {
            int randomIndex = (int) (Math.random() * linesCount);
            int marginValue = randomIndex * (mHeight / linesCount);
            //边界检查
            if (marginValue > mHeight - view.getMeasuredHeight()) {
                marginValue = mHeight - view.getMeasuredHeight() - range;
            }
            if (marginValue == 0) {
                marginValue = range;
            }
            if (!existMarginValues.contains(marginValue)) {
                existMarginValues.add(marginValue);
                Log.d(TAG, "marginValue:" + marginValue);
                return marginValue;
            }
        }
    }
}

弹幕实体类:

/**
 * Created by dell on 2016/9/28.
 */
public class Danmu {
    private String headerUrl;//头像
    private String userName;//昵称
    private String info;//信息

    public String getHeaderUrl() {
        return headerUrl;
    }

    public void setHeaderUrl(String headerUrl) {
        this.headerUrl = headerUrl;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }
}

测试类,MainActivity

public class MainActivity extends AppCompatActivity {
    DanmuView mDanmuView;
    EditText mMsgEdt;
    Button mSendBtn;
    Handler mDanmuAddHandler;
    boolean continueAdd;
    int counter;

    @Override
    protected void onResume() {
        super.onResume();
        mDanmuView.startPlay(true);//true表示弹幕的垂直方向是固定的,false则随机
        continueAdd = true;
        mDanmuAddHandler.sendEmptyMessageDelayed(0, 6000);
    }

    @Override
    protected void onPause() {
        super.onPause();
        continueAdd = false;
        mDanmuAddHandler.removeMessages(0);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
        initListener();
    }

    private void initView() {
        mDanmuView = (DanmuView) findViewById(R.id.danmuView);
        mMsgEdt = (EditText) findViewById(R.id.edt_msg);
        mSendBtn = (Button) findViewById(R.id.btn_send);
    }

    private void initData() {
        List<Danmu> danmuList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Danmu danmu = new Danmu();
            danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0725/cb00091099ffbf09f4861f2bbb5dd993.jpg");
            danmu.setUserName("Mr.chen" + i);
            danmu.setInfo("我是弹幕啊,不要问我为什么不可以那么长!!!");
            danmuList.add(danmu);
        }
        mDanmuView.add(danmuList);

        //下面是模拟每秒添加一个弹幕的过程
        HandlerThread ht = new HandlerThread("send danmu");
        ht.start();
        mDanmuAddHandler = new Handler(ht.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Danmu danmu = new Danmu();
                        danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0803/87a8b262a5edeff0e11f5f0ba24fb22f.jpg");
                        danmu.setUserName("Mr.new" + (counter++));
                        danmu.setInfo("新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!");
                        mDanmuView.add(danmu);
                    }
                });
                //继续添加
                if (continueAdd) {
                    sendEmptyMessageDelayed(0, 1000);
                }
            }
        };
    }

    private void initListener() {
        //手动添加
        mSendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = mMsgEdt.getText().toString().trim();
                if (TextUtils.isEmpty(msg)) {
                    Toast.makeText(MainActivity.this, "亲,你想发送什么啊?", Toast.LENGTH_SHORT).show();
                    return;
                }
                mMsgEdt.setText("");
                Danmu danmu = new Danmu();
                danmu.setHeaderUrl("http://img0.imgtn.bdimg.com/it/u=2198087564,4037394230&fm=11&gp=0.jpg");
                danmu.setUserName("I'am good man");
                danmu.setInfo("我是新人:" + msg);
                mDanmuView.add(danmu);
            }
        });
    }
}

源码下载

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值