android图文弹幕

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zxq614/article/details/52622792

android图文弹幕

**

注意:哔哩哔哩弹幕库新版已经支持图文混排,本篇文章仅供参考,推荐大家使用官方方式做图文弹幕。

**

本文主要介绍基于哔哩哔哩弹幕库实现的图文弹幕,包含头像,名称和弹幕内容,就像下图酱紫,其实也是起到抛砖引玉的作用,当你理解我的实现方式后,如果想实现单行文字啦,或者更加复杂的排版啦,都是改变一下绘制方式而已。

图文弹幕

废话不多说,先上代码,看着代码来讲解:

public class DanmuActivity extends Activity implements View.OnClickListener {

    private IDanmakuView mDanmakuView;
    private Button mBtnSendDanmaku;
    private EditText editText;
    private DanmuControl danmuControl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_danmuku);
        mDanmakuView = (IDanmakuView) findViewById(R.id.sv_danmaku);
        editText = (EditText) findViewById(R.id.editText);
        mBtnSendDanmaku = (Button) findViewById(R.id.btn_send);
        mBtnSendDanmaku.setOnClickListener(this);
        danmuControl = new DanmuControl(this, mDanmakuView);
    }

    @Override
    public void onClick(View v) {
        String avator = "http://g.hiphotos.baidu.com/image/h%3D200/sign=9b2f9371992397ddc9799f046983b216/dc54564e9258d1094dc90324d958ccbf6c814d7a.jpg";
        String name = "张三";
        String content = editText.getText().toString().trim();
        if (content != null && content.length() > 0) {
            danmuControl.addDanmu(avator, name, content);
        } else {
            Toast.makeText(this, "发送内容为空", Toast.LENGTH_LONG).show();
        }
    }
}



<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fffeabab">

    <master.flame.danmaku.ui.widget.DanmakuView
        android:id="@+id/sv_danmaku"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_gravity="bottom"
        android:background="#8acc0000"
        android:gravity="center_vertical"
        android:padding="5dp">

        <EditText
            android:id="@+id/editText"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <Button
            android:id="@+id/btn_send"
            android:layout_width="100dp"
            android:layout_height="match_parent"
            android:text="发送" />
    </LinearLayout>
</FrameLayout>

以上部分是主页面以及主页面的布局。

public class DanmuControl {
    private Context mContext;
    private IDanmakuView mDanmakuView;
    private DanmakuContext mDanmakuContext;

    public DanmuControl(Context context, IDanmakuView danmakuView) {
        this.mContext = context;
        this.mDanmakuView = danmakuView;
        initDanmuConfig();
    }

    /**
     * 初始化配置
     */
    private void initDanmuConfig() {
        // 设置最大显示行数
        HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>();
        maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 2); // 滚动弹幕最大显示2行
        // 设置是否禁止重叠
        HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>();
        overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
        overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);

        mDanmakuContext = DanmakuContext.create();
        mDanmakuContext
                .setDanmakuStyle(IDisplayer.DANMAKU_STYLE_NONE)
                .setDuplicateMergingEnabled(false)
                .setScrollSpeedFactor(3f)//越大速度越慢
                .setScaleTextSize(1.2f)
                .setCacheStuffer(new MyCacheStuffer(mContext), mCacheStufferAdapter)
                .setMaximumLines(maxLinesPair)
                .preventOverlapping(overlappingEnablePair);

        if (mDanmakuView != null) {
            mDanmakuView.setCallback(new DrawHandler.Callback() {
                @Override
                public void prepared() {
                    mDanmakuView.start();
                }

                @Override
                public void updateTimer(DanmakuTimer timer) {
                }

                @Override
                public void danmakuShown(BaseDanmaku danmaku) {
                }

                @Override
                public void drawingFinished() {
                }
            });
        }

        mDanmakuView.prepare(new BaseDanmakuParser() {

            @Override
            protected Danmakus parse() {
                return new Danmakus();
            }
        }, mDanmakuContext);
        mDanmakuView.enableDanmakuDrawingCache(true);
    }

    private BaseCacheStuffer.Proxy mCacheStufferAdapter = new BaseCacheStuffer.Proxy() {

        @Override
        public void prepareDrawing(final BaseDanmaku danmaku, boolean fromWorkerThread) {
        }

        @Override
        public void releaseResource(BaseDanmaku danmaku) {
            // tag包含bitmap,一定要清空
            danmaku.tag = null;
        }
    };

    public void addDanmu(final String avatorUrl, final String name, final String content) {
        final BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
        new Thread() {
            @Override
            public void run() {
                InputStream inputStream = null;
                try {
                    // 从网络获取图片并且保存到一个bitmap里
                    URLConnection urlConnection = new URL(avatorUrl).openConnection();
                    inputStream = urlConnection.getInputStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    bitmap = makeRoundCorner(bitmap);

                    // 组装需要传递给danmaku的数据
                    Map<String, Object> map = new HashMap<String, Object>();
                    map.put("name", name);
                    map.put("content", content);
                    map.put("bitmap", bitmap);
                    danmaku.tag = map;
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    IOUtils.closeQuietly(inputStream);
                }
                danmaku.text = "";
                danmaku.padding = 0;
                danmaku.priority = 1;  // 一定会显示, 一般用于本机发送的弹幕
                danmaku.isLive = true;
                danmaku.time = mDanmakuView.getCurrentTime() + 1000;
                danmaku.textSize = 0;
                mDanmakuView.addDanmaku(danmaku);
            }
        }.start();
    }

    /**
     * 将图片变成圆形
     *
     * @param bitmap
     * @return
     */
    private static Bitmap makeRoundCorner(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int left = 0, top = 0, right = width, bottom = height;
        float roundPx = height / 2;
        if (width > height) {
            left = (width - height) / 2;
            top = 0;
            right = left + height;
            bottom = height;
        } else if (height > width) {
            left = 0;
            top = (height - width) / 2;
            right = width;
            bottom = top + width;
            roundPx = width / 2;
        }
        Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        int color = 0xff424242;
        Paint paint = new Paint();
        Rect rect = new Rect(left, top, right, bottom);
        RectF rectF = new RectF(rect);
        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }
}

public class MyCacheStuffer extends BaseCacheStuffer {
    private int AVATAR_DIAMETER; //头像直径
    private int AVATAR_PADDING; // 头像边框宽度
    private int TEXT_LEFT_PADDING; // 文字和头像间距
    private int TEXT_RIGHT_PADDING; // 文字和右边线距离
    private int TEXT_SIZE; // 文字大小

    private int NICK_COLOR = 0xffff1874;//昵称 红色
    private int TEXT_COLOR = 0xffeeeeee;  //文字内容  白色
    private int TEXT_BG_COLOR = 0x66000000; // 文字灰色背景色值
    private int TEXT_BG_RADIUS; // 文字灰色背景圆角

    public MyCacheStuffer(Context context) {
        // 初始化固定参数,这些参数可以根据自己需求自行设定
        AVATAR_DIAMETER = Util.DensityUtil.dip2px(context, 33);
        AVATAR_PADDING = Util.DensityUtil.dip2px(context, 1);
        TEXT_LEFT_PADDING = Util.DensityUtil.dip2px(context, 2);
        TEXT_RIGHT_PADDING = Util.DensityUtil.dip2px(context, 8);
        TEXT_SIZE = Util.DensityUtil.dip2px(context, 13);
        TEXT_BG_RADIUS = Util.DensityUtil.dip2px(context, 8);
    }

    @Override
    public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
        // 初始化数据
        Map<String, Object> map = (Map<String, Object>) danmaku.tag;
        String name = (String) map.get("name");
        String content = (String) map.get("content");

        // 设置画笔
        paint.setTextSize(TEXT_SIZE);

        // 计算名字和内容的长度,取最大值
        float nameWidth = paint.measureText(name);
        float contentWidth = paint.measureText(content);
        float maxWidth = Math.max(nameWidth, contentWidth);

        // 设置弹幕区域的宽高
        danmaku.paintWidth = maxWidth + AVATAR_DIAMETER + AVATAR_PADDING * 2 + TEXT_LEFT_PADDING + TEXT_RIGHT_PADDING; // 设置弹幕区域的宽度
        danmaku.paintHeight = AVATAR_DIAMETER + AVATAR_PADDING * 2; // 设置弹幕区域的高度
    }

    @Override
    public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) {
    }

    @Override
    public void drawText(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, TextPaint paint, boolean fromWorkerThread) {
    }

    @Override
    public void clearCaches() {
    }

    @Override
    public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
        // 初始化数据
        Map<String, Object> map = (Map<String, Object>) danmaku.tag;
        String name = (String) map.get("name");
        String content = (String) map.get("content");
        Bitmap bitmap = (Bitmap) map.get("bitmap");

        // 设置画笔
        Paint paint = new Paint();
        paint.setTextSize(TEXT_SIZE);

        // 绘制文字灰色背景
        Rect rect = new Rect();
        paint.getTextBounds(content, 0, content.length(), rect);
        paint.setColor(TEXT_BG_COLOR);
        paint.setAntiAlias(true);
        float bgLeft = left + AVATAR_DIAMETER / 2 + AVATAR_PADDING;
        float bgTop = top + AVATAR_DIAMETER / 2 + AVATAR_PADDING;
        float bgRight = left + AVATAR_DIAMETER + AVATAR_PADDING * 2 + TEXT_LEFT_PADDING + rect.width() + TEXT_RIGHT_PADDING;
        float bgBottom = top + AVATAR_DIAMETER + AVATAR_PADDING;
        canvas.drawRoundRect(new RectF(bgLeft, bgTop, bgRight, bgBottom), TEXT_BG_RADIUS, TEXT_BG_RADIUS, paint);

        // 绘制头像背景
        paint.setColor(Color.WHITE);
        float centerX = left + AVATAR_DIAMETER / 2 + AVATAR_PADDING;
        float centerY = left + AVATAR_DIAMETER / 2 + AVATAR_PADDING;
        float radius = AVATAR_DIAMETER / 2 + AVATAR_PADDING; // 半径
        canvas.drawCircle(centerX, centerY, radius, paint);

        // 绘制头像
        float avatorLeft = left + AVATAR_PADDING;
        float avatorTop = top + AVATAR_PADDING;
        float avatorRight = left + AVATAR_PADDING + AVATAR_DIAMETER;
        float avatorBottom = top + AVATAR_PADDING + AVATAR_DIAMETER;
        canvas.drawBitmap(bitmap, null, new RectF(avatorLeft, avatorTop, avatorRight, avatorBottom), paint);

        // 绘制名字
        paint.setColor(NICK_COLOR);
        float nameLeft = left + AVATAR_DIAMETER + AVATAR_PADDING * 2 + TEXT_LEFT_PADDING;
        float nameBottom = top + rect.height() + AVATAR_PADDING + (AVATAR_DIAMETER / 2 - rect.height()) / 2;
        canvas.drawText(name, nameLeft, nameBottom, paint);

        // 绘制弹幕内容
        paint.setColor(TEXT_COLOR);
        float contentLeft = nameLeft;
        float contentBottom = top + AVATAR_PADDING + AVATAR_DIAMETER / 2 + rect.height() + (AVATAR_DIAMETER / 2 - rect.height()) / 2;
        canvas.drawText(content, contentLeft, contentBottom, paint);
    }
}

以上就是图文弹幕实现的核心内容,先来看看DanmuControl这类:
这个类主要做弹幕库的初始化设置,很多内容哔哩哔哩弹幕库的demo里都有介绍,我就不多说了,我们的重点是图文弹幕。

  1. 看第30行,这里使用了MyCacheStuffer。
  2. 看第86行,这里是开启子线程获取头像图片,并且把头像转换成圆形bitmap,然后和昵称name,content弹幕内容一起组装成map放在tag字段上保存起来,tag字段是哔哩哔哩弹幕库提供用来保存自定数据的,看BaseDanmaku的注释就可以明白。
  3. 从第104行开始设置BaseDanmaku数据,我们所有的数据都是通过tag保存,所以,text传空字符串即可。
  4. 注意第73行。

下面介绍MyCacheStuffer 这个类,这个是实现图文弹幕最核心的地方:
这个类继承自BaseCacheStuffer ,哔哩哔哩弹幕库提供的几个方法让我们用来进行内容,背景,边界的绘制。

  1. 第177行,measure()方法,这个方法里我们提取了name和content,然后使用paint的方法计算出文字的显示长度,取两者中的最大值用来计算弹幕总的绘制区域,也就是danmaku.paintWidth和danmaku.paintHeight,这两个参数决定了本条弹幕的总绘制区域,如果内容的绘制超出这个区域,将会被截断,所以,danmaku.paintWidth = 头像的直径 + 头像边圈的宽度*2 + 文字的长度 + 文字左边距 + 文字右边距。danmaku.paintHeight = 头像的直径 + 头像边圈的宽度*2;
  2. drawStroke()绘制边线,如果需要绘制边线可以试试这个方法
  3. drawText()绘制文字,没有必要使用,因为文字的绘制可以在drawBackground()方法中实现。
  4. 原则上你想绘制的所有东西都可以在drawBackground()中来进行,我也建议你使用这个方法,因为它提供的left和top参数是准确的相对参数,而drawText()所提供的top则是一个我没有弄懂的值,可以确定的是这个值是根据danmaku.textSize有关系的,但是我没有深究他们之间的关系。这个方法里的标注我也描述的比较准确,虽然参数比较多,但是耐心看是可以看懂的,分别绘制了文字的灰色背景,头像,头像的白色边圈,名字和弹幕内容。drawBackground()为你提供的画布,left和top的相对值(初始值都是0),画笔则是自定义的,这里需要注意的一点是,measure()方法和drawBackground()中用于计算文字显示长度的画笔一定要设置为相同的TextSize

源码下载

这是我的第一篇原创技术文章,做技术时间久了,就有很多东西想拿出来分享,希望这篇文章能帮助到一些用到弹幕的同学,文笔一般,如有表述不清晰的地方,请指正。

展开阅读全文

没有更多推荐了,返回首页