Android 教你亲手打造酷炫的弹幕效果

公司的新产品上线需要添加的弹幕功能,于是花了一天时间写了一个Demo。

效果实现如下:

一开始的思路是:

1、首先实现一个自定义的Layout,在其中获得需要展示的弹幕数组,每个弹幕数组的项包括弹幕文本以及图片Url地址。

2、在Layout内部使用Handler或者计时线程循环发送弹幕。

3、弹幕实现采用自定义弹幕View,配合动画实现滚屏呈现。

总结之后发现主要的难点还是在弹幕的出现位置选择以及弹幕如何确保及时销毁上(我会说一开始调试的时候出现满屏弹幕的华丽场景么。。),以及如何实现组件的复用,并尽可能提高性能。还要注意一些需要实现的功能点:通过Url获得图片(可通过图片缓存加载框架实现并替代)、防止弹幕堆叠(这个算法实现还是比较容易的)。

之后发现再写个自定义的弹幕view(左边一个圆形头像右边文字,外框椭圆背景透明)有点麻烦,于是采用了ListView里item复用的思想,使用了一个Item布局轻松实现辣~

好在最近一直在写公司新项目的界面,各种技巧运用得比较熟练,弹幕Demo的编写全程没有碰到什么压力,倒是最后忘记加网络权限导致调试了半天。。。(哭泣)。

实现步骤:

1、实现主布局:

弹幕区域的位置是可以自己调整的,理论上来说可以安置在屏幕任一位置上。

barrageview_test.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <com.whale.nangua.toquan.view.BarrageView
        android:id="@+id/barrageview"
        android:background="@drawable/testbackground"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_width="match_parent"
        android:layout_height="200dp"/>
</RelativeLayout>

2、实现弹幕item布局:

使用了自定义的圆圈类实现了圆形头像的效果,网上一搜一大堆或者评论留言这里就不填源码占篇幅了。

barrageview_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:background="@drawable/barrage_shape"
              android:layout_width="wrap_content"
              android:layout_height="50dp">

    <com.whale.nangua.toquan.view.NGNormalCircleImageView
        android:src="@drawable/pumpkin"
        android:id="@+id/barrageview_item_img"
        android:layout_width="50dp"
        android:layout_height="match_parent"/>

    <TextView
        android:maxEms="10"
        android:textColor="@android:color/white"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:gravity="center"
        android:text="测试弹幕"
        android:singleLine="true"
        android:id="@+id/barrageview_item_tv"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>
</LinearLayout>
3、实现测试用的Activity

package com.whale.nangua.toquan;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

import com.whale.nangua.toquan.bean.Barrage;
import com.whale.nangua.toquan.view.BarrageView;

import java.util.ArrayList;

/**
 * Created by nangua on 2016/7/18.
 */
public class TestAty extends Activity {
    BarrageView barrageview;
    ArrayList<Barrage> date;

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

    private void initView() {
        date = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            date.add(new Barrage(
                    "测试弹幕" + i, "http://pic.818today.com/imgsy/image/2016/0215/6359114592207963687677523.jpg"));
        }

        barrageview = (BarrageView) findViewById(R.id.barrageview);
        Log.d("xiaojingyu", date.size() + "");
        barrageview.setSentenceList(date);
    }
}

4、弹幕类BarrageView.java

package com.whale.nangua.toquan.view;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.whale.nangua.toquan.R;
import com.whale.nangua.toquan.bean.Barrage;

import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

/**
 * 2秒一条
 * 屏幕上同时存在5条
 * Created by nangua on 2016/7/28.
 */
public class BarrageView extends FrameLayout {
    private static ArrayList<Barrage> date = new ArrayList<>(); //数据
    private int nowIndex = 0; //date的下标
    private Bitmap nowBitmap; //当前图片
    int width;    //控件宽
    int height;  //控件高
    float scale;    //像素密度
    FrameLayout frameLayout;
    FrameLayout.LayoutParams tvParams;

    static boolean IS_START = false;    //判断是否开始

    long alltime; //视频总时长
    long starttime; //开始时间

    //    LinearLayout layout;

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Barrage barrage = (Barrage) msg.getData().getSerializable("barrage");
            final LinearLayout layout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.barrageview_item, null);
            layout.setLayoutParams(tvParams);
            //随机获得Y值
            layout.setY(getRamdomY());
            layout.setX(width + layout.getWidth());

            //设置文字
            TextView textView = (TextView) layout.findViewById(R.id.barrageview_item_tv);
            textView.setText(barrage.getBarrageInfo());

            //设置图片
            NGNormalCircleImageView ngNormalCircleImageView = (NGNormalCircleImageView) layout.findViewById(R.id.barrageview_item_img);
            if (nowBitmap != null) {

                ngNormalCircleImageView.setImageBitmap(nowBitmap);
            }

            frameLayout.addView(layout);

            final ObjectAnimator anim = ObjectAnimator.ofFloat(layout, "translationX", -width);
            anim.setDuration(10000);

            //释放资源
            anim.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                }
                @Override
                public void onAnimationEnd(Animator animation) {
                    anim.cancel();
                    layout.clearAnimation();
                    frameLayout.removeView(layout);
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            anim.start();
        }
    };

    /**
     * 使用httprulconnection通过发送网络请求path获得bitmap
     * @param path
     * @return
     */
    public static Bitmap getBitmapFromUrl(String path) {
        try {
            //获得url
            URL url = new URL(path);
            //打开httprulconnection获得实例
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            //设置超时时间
            conn.setConnectTimeout(5000);
            //设置Get
            conn.setRequestMethod("GET");
            //连接成功
            if (conn.getResponseCode() == 200) {
                //获得输入流
                InputStream inputStream = conn.getInputStream();
                //得到bitmap
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                if (bitmap == null) {
                }
                //返回
                return bitmap;
            }
            //错误信息处理
        } catch (Exception e) {
            //打印错误信息
            e.printStackTrace();
        }
        return null;
    }

    int lastY;//上一次出现的Y值
    /**
     * 获得随机的Y轴的值
     *
     * @return
     */
    private float getRamdomY() {
        int tempY;
        int rY;
        int result = 0;
        // height * 2 / 4 - 25
        //首先随机选择一条道路
        int nowY = (int) (Math.random() * 3);
        switch (nowY) {
            case 0:
                nowY = avoidTheSameY(nowY,lastY);
                //第一条
                tempY = height / 4 - 25;
                rY = (int) (Math.random() * height / 4);
                if (rY >= height / 8) {
                    result = tempY + rY;
                } else {
                    result = tempY - rY + 50 ;
                }
                lastY = nowY;
                break;
            case 1:
                nowY = avoidTheSameY(nowY,lastY);
                //第二条
                tempY = height / 2 - 25;
                rY = (int) (Math.random() * height / 4);
                if (rY >= height / 8) {
                    result = tempY + rY;
                } else {
                    result = tempY - rY;
                }
                lastY = nowY;
                break;
            case 2:
                nowY = avoidTheSameY(nowY,lastY);
                //第三条
                tempY = height * 3 / 4 - 25;
                rY = (int) (Math.random() * height / 4);
                if (rY >= height / 8) {
                    result = tempY + rY -50;
                } else {
                    result = tempY - rY;
                }
                lastY = nowY;
                break;
        }
        return result;
    }

    /**
     * 避免Y重合的方法
     * @param lastY
     * @return
     */
    private int avoidTheSameY(int nowY,int lastY) {
        if (nowY == lastY) {
            nowY ++;
        }
        if (nowY == 4) {
            nowY = 0;
        }
        return nowY;
    }


    public BarrageView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        width = getWidth(); //宽度
        height = getHeight();   //高度
        init();
    }

    private void init() {
        setTime(600000);    //设置初始时长,改完记得删

        starttime = System.currentTimeMillis();

        scale = this.getResources().getDisplayMetrics().density;
        //获得自身实例
        frameLayout = (FrameLayout) findViewById(R.id.barrageview);
        tvParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, 50);

        if (IS_START) {
            //开始动画线程
            startBarrageView();
            IS_START = false;
        }
    }

    public void startBarrageView() {
        //开启线程发送弹幕
        new Thread() {
            @Override
            public void run() {

                while ((System.currentTimeMillis() - starttime < alltime)
                        && (nowIndex <= date.size() - 1)
                        ){

                    try {
                        nowBitmap = getBitmapFromUrl(date.get(nowIndex).getBarrageUrl());
                        Message message = new Message();
                        Bundle bundle = new Bundle();
                        bundle.putSerializable("barrage",date.get(nowIndex));
                        nowIndex ++;
                        message.setData(bundle);
                        handler.sendMessage(message);
                        Thread.sleep((long) (Math.random() * 3000) + 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return;
            }
        }.start();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    //设置数据
    public void setSentenceList(ArrayList<Barrage> date1) {
        date = date1;
        IS_START = true;
    }

    //获得视频总时长
    public void setTime(long time) {
        alltime = time;
    }

}
实现的思路大概跟开头的描述是一样的,但是这还只是一个比较简陋的雏形,有大量可模块化的功能点进行扩充,

比如可设置的视频时长,弹幕速率,弹幕字体颜色,弹幕排列方式等等等等都可以自己定制,

代码注释详细思路清晰,不懂得评论@我。

然而明天还要上班,后天才放假,晚安了~



评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值