弹幕的开发(DanmakuFlameMaster)


记一次弹幕的开发

越来越多的app增加了直播的功能,既然是直播那么弹幕也是一个逃不过的话题。最近项目也添加了弹幕,前前后后也忙活了好几天。本文就简单的记录弹幕的开发。

目标

先来看一下,弹幕要做成什么样子的。图片来源于美芽。

danmu_target

根据这张图片我们可以将分成几个需要完成的小目标:

  • 弹幕方向从右向左
  • 只显示两行弹幕
  • 两种显示类型:1 包含头像和一个心形;2 包含头像和文字,同时有三种背景颜色
  • 上下弹幕之间的间距以及两条弹幕之间的间距

开发

本着不重复造轮子的麻烦事,从github上找到了大名鼎鼎的B站开源的弹幕库DanmakuFlameMaster

配置

根据demo我们来进行开发。首先是添加显示弹幕的控件。提供了三个控件:’DanmakuSurfaceView ‘、’ DanmakuTextureView’以及’ DanmakuView’。我们这里采用’ DanmakuView’。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<FrameLayout
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_centerInParent="true">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@mipmap/wat"/>

        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/danmakuView"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:layout_gravity="bottom"/>
    </FrameLayout>

接下来是做一些初始化的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 初始化配置
*/
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(1.2f)//越大速度越慢
               .setScaleTextSize(1.2f)
               .setCacheStuffer(new BackgroundCacheStuffer(), mCacheStufferAdapter)
               .setMaximumLines(maxLinesPair)
               .preventOverlapping(overlappingEnablePair);
   }

然后是对DanmuView进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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() {

        }
    });
}
//这里原本是一个解析器,可以使用库里提供A站或B站的解析器,也可以自己写一个,但由于项目中获取到的数据已经是model,这里就没有使用这个。
mDanmakuView.prepare(new BaseDanmakuParser() {
      @Override
      protected Danmakus parse() {
          return new Danmakus();
      }
}, mDanmakuContext);
mDanmakuView.enableDanmakuDrawingCache(true);

好了,到这里我们初始化的配置就已经完成。但是在配置‘DanmakuContext’的时候还有两个没介绍‘BackgroundCacheStuffer’和‘BaseCacheStuffer.Proxy’。

1 ’BackgroundCacheStuffer’从这个命名就可以看出这个用来做一些背景绘制的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 绘制背景(自定义弹幕样式)
*/
private class BackgroundCacheStuffer extends SpannedCacheStuffer {
       // 通过扩展SimpleTextCacheStuffer或SpannedCacheStuffer个性化你的弹幕样式
       final Paint paint = new Paint();

       @Override
       public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
//            danmaku.padding = 20;  // 在背景绘制模式下增加padding
           super.measure(danmaku, paint, fromWorkerThread);
       }

       @Override
       public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
           paint.setAntiAlias(true);
           if (!danmaku.isGuest && danmaku.userId == mGoodUserId && mGoodUserId != 0) {
               paint.setColor(PINK_COLOR);//粉红 楼主
           } else if (!danmaku.isGuest && danmaku.userId == mMyUserId
                   && danmaku.userId != 0) {
               paint.setColor(ORANGE_COLOR);//橙色 我
           } else {
               paint.setColor(BLACK_COLOR);//黑色 普通
           }
           if (danmaku.isGuest) {//如果是赞 就不要设置背景
               paint.setColor(Color.TRANSPARENT);
           }
           //由于该库并没有提供margin的设置,所以我这边试出这种方法:将danmaku.padding也就是内间距设置大一点,并在这里的RectF中设置绘制弹幕的位置,就可以形成类似margin的效果
           canvas.drawRoundRect(new RectF(left + DANMU_PADDING_INNER, top + DANMU_PADDING_INNER
                           , left + danmaku.paintWidth - DANMU_PADDING_INNER + 6,
                           top + danmaku.paintHeight - DANMU_PADDING_INNER + 6),//+6 主要是底部被截得太厉害了,+6是增加padding的效果
                   DANMU_RADIUS, DANMU_RADIUS, paint);
       }

       @Override
       public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) {
           // 禁用描边绘制
       }
}

2 ‘BaseCacheStuffer.Proxy ’这个类提供了两个方法‘prepareDrawing ’在弹幕绘制之前是否要做一些更新操作,比如更换图片、文字。‘releaseResource ’这个顾名思义就是释放一些资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private BaseCacheStuffer.Proxy mCacheStufferAdapter = new BaseCacheStuffer.Proxy() {

        @Override
        public void prepareDrawing(final BaseDanmaku danmaku, boolean fromWorkerThread) {
//            if (danmaku.text instanceof Spanned) { // 根据你的条件检查是否需要需要更新弹幕
//            }
        }

        @Override
        public void releaseResource(BaseDanmaku danmaku) {
            // TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源 例如drawable
            if (danmaku.text instanceof Spanned) {
                danmaku.text = "";
            }
        }
    };

弹幕显示效果开发

完成了配置之后,我们可以发现背景的绘制、上下之间的间距和行数已经完成了。还剩下‘两种显示类型:1 包含头像和一个心形;2 包含头像和文字,同时有三种背景颜色’。

由于这个库是支持图文混排的,那么实现头像和文字的显示就简单了许多,我们只要将bitmap转成drawable,然后通过‘SpannableStringBuilder’和‘ImageSpan’来完成图文混排。

包含头像和一个心形:这个原本是想在背景绘制的时候同时绘制心形,但是发现绘制出来的心形处在图片的下方被挡住了,所以后面还是在drawable中同时将心形绘制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
 * 圆形的Drawable
 * Created by feiyang on 16/2/18.
 */
public class CircleDrawable extends Drawable {

    private Paint   mPaint;
    private Bitmap  mBitmap;
    private Bitmap  mBitmapHeart;
    private boolean mHasHeart;

    private static final int BLACK_COLOR          = 0xb2000000;//黑色 背景
    private static final int BLACKGROUDE_ADD_SIZE = 4;//背景比图片多出来的部分

    public CircleDrawable(Bitmap bitmap) {
        mBitmap = bitmap;
        BitmapShader bitmapShader = new BitmapShader(bitmap,
                Shader.TileMode.CLAMP,
                Shader.TileMode.CLAMP);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setShader(bitmapShader);
    }

    /**
     * 右下角包含一个‘心’的圆形drawable
     *
     * @param context
     * @param bitmap
     * @param hasHeart
     */
    public CircleDrawable(Context context, Bitmap bitmap, boolean hasHeart) {
        this(bitmap);
        mHasHeart = hasHeart;
        if (hasHeart) {
            setBitmapHeart(context);
        }
    }

    private void setBitmapHeart(Context context) {
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_liked);
        if (bitmap != null) {
            Matrix matrix = new Matrix();
            matrix.postScale(0.8f, 0.8f);
            mBitmapHeart = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        }
    }

    @Override
    public void setBounds(int left, int top, int right, int bottom) {
        super.setBounds(left, top, right, bottom);
    }

    @Override
    public void draw(Canvas canvas) {
        if (mHasHeart && mBitmapHeart != null) {
            //设置背景
            Paint backgroundPaint = new Paint();
            backgroundPaint.setAntiAlias(true);
            backgroundPaint.setColor(BLACK_COLOR);
            canvas.drawCircle(getIntrinsicWidth() / 2 + BLACKGROUDE_ADD_SIZE, getIntrinsicHeight() / 2 + BLACKGROUDE_ADD_SIZE,
                    getIntrinsicWidth() / 2 + BLACKGROUDE_ADD_SIZE, backgroundPaint);

            //先将画布平移,防止图片不在正中间,然后绘制图片
            canvas.translate(BLACKGROUDE_ADD_SIZE, BLACKGROUDE_ADD_SIZE);
            canvas.drawCircle(getIntrinsicWidth() / 2, getIntrinsicHeight() / 2, getIntrinsicWidth() / 2, mPaint);

            //在右下角绘制‘心’
            Rect srcRect = new Rect(0, 0, mBitmapHeart.getWidth(), mBitmapHeart.getHeight());
            Rect desRect = new Rect(getIntrinsicWidth() - mBitmapHeart.getWidth() + BLACKGROUDE_ADD_SIZE * 2,
                    getIntrinsicHeight() - mBitmapHeart.getHeight() + BLACKGROUDE_ADD_SIZE * 2,
                    getIntrinsicWidth() + BLACKGROUDE_ADD_SIZE * 2, getIntrinsicHeight() + BLACKGROUDE_ADD_SIZE * 2);
            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            paint.setFilterBitmap(true);
            paint.setDither(true);
            canvas.drawBitmap(mBitmapHeart, srcRect, desRect, paint);
        } else {
            canvas.drawCircle(getIntrinsicWidth() / 2, getIntrinsicHeight() / 2, getIntrinsicWidth() / 2, mPaint);
        }
    }

    @Override
    public int getIntrinsicWidth() {
        return mBitmap.getWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmap.getHeight();
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

这样就完成了效果的显示,但是这样还存在一个问题,就是图文混排的时候,文字在竖直方向上并不是处于中间位置,后面继承了‘ImageSpan’来完成效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 * 图文混排使图片文字基于中线对齐
 * Created by feiyang on 16/2/18.
 * 参考:http://stackoverflow.com/questions/25628258/align-text-around-imagespan-center-vertical
 */
public class CenteredImageSpan extends ImageSpan {

    private WeakReference<Drawable> mDrawableRef;

    public CenteredImageSpan(final Drawable drawable) {
        super(drawable);
    }

    @Override
    public int getSize(Paint paint, CharSequence text,
                       int start, int end,
                       Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();

        if (fm != null) {
            Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
            // keep it the same as paint's fm
            fm.ascent = pfm.ascent;
            fm.descent = pfm.descent;
            fm.top = pfm.top;
            fm.bottom = pfm.bottom;
        }

        return rect.right;
    }

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, @NonNull Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();

        int drawableHeight = b.getIntrinsicHeight();
        int fontAscent = paint.getFontMetricsInt().ascent;
        int fontDescent = paint.getFontMetricsInt().descent;
        int transY = bottom - b.getBounds().bottom +  // align bottom to bottom
                (drawableHeight - fontDescent + fontAscent) / 2;  // align center to center

        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;

        if (wr != null)
            d = wr.get();

        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference<>(d);
        }

        return d;
    }
}

添加弹幕

效果完成之后,我们就可以添加弹幕了。首先是创建一个弹幕

1
BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);

然后往显示弹幕的控件这边是‘mDanmakuView’添加该弹幕

1
mDanmakuView.addDanmaku(danmaku);

目前在这个库中提供了5中类型的弹幕了

1
2
3
4
5
TYPE_SCROLL_RL = 1;//从右向左
TYPE_SCROLL_LR = 6;//从左向右
TYPE_FIX_TOP = 5;//停留在顶部
TYPE_FIX_BOTTOM = 4;//停留在底部
TYPE_SPECIAL = 7;//特殊弹幕 注:这个没试过不知道是什么效果

添加弹幕部分完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public void addDanmu(Danmu danmu, int i) {
        BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);

        danmaku.userId = danmu.userId;
        danmaku.isGuest = danmu.type.equals("Like");//isGuest此处用来判断是赞还是评论

        SpannableStringBuilder spannable;
        Bitmap bitmap = getDefaultBitmap(danmu.avatarUrl);
        CircleDrawable circleDrawable = new CircleDrawable(mContext, bitmap, danmaku.isGuest);
        circleDrawable.setBounds(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
        spannable = createSpannable(circleDrawable, danmu.content);
        danmaku.text = spannable;

        danmaku.padding = DANMU_PADDING;
        danmaku.priority = 0;  // 1:一定会显示, 一般用于本机发送的弹幕,但会导致行数的限制失效
        danmaku.isLive = false;
        danmaku.time = mDanmakuView.getCurrentTime() + (i * ADD_DANMU_TIME);
        danmaku.textSize = DANMU_TEXT_SIZE/* * (mDanmakuContext.getDisplayer().getDensity() - 0.6f)*/;
        danmaku.textColor = Color.WHITE;
        danmaku.textShadowColor = 0; // 重要:如果有图文混排,最好不要设置描边(设textShadowColor=0),否则会进行两次复杂的绘制导致运行效率降低
        mDanmakuView.addDanmaku(danmaku);
    }

    private Bitmap getDefaultBitmap(int drawableId) {
        Bitmap mDefauleBitmap = null;
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), drawableId);
        if (bitmap != null) {
            int width = bitmap.getWidth();
            int height = bitmap.getHeight();
            Log.d(TAG, "width = " + width);
            Log.d(TAG, "height = " + height);
            Matrix matrix = new Matrix();
            matrix.postScale(((float) BITMAP_WIDTH) / width, ((float) BITMAP_HEIGHT) / height);
            mDefauleBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
            Log.d(TAG, "mDefauleBitmap getWidth = " + mDefauleBitmap.getWidth());
            Log.d(TAG, "mDefauleBitmap getHeight = " + mDefauleBitmap.getHeight());
        }
        return mDefauleBitmap;
    }

    private SpannableStringBuilder createSpannable(Drawable drawable, String content) {
        String text = "bitmap";
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
        CenteredImageSpan span = new CenteredImageSpan(drawable);
        spannableStringBuilder.setSpan(span, 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        if (!TextUtils.isEmpty(content)) {
            spannableStringBuilder.append(" ");
            spannableStringBuilder.append(content.trim());
        }
        return spannableStringBuilder;
    }

适配

完成了弹幕之后,千万别忘了适配、适配、适配,重要的事说三遍。适配一直都是android中很重要的一环,也是很痛苦的一个过程。

由于弹幕库使用的大部分是像素,所以我们可以通过dp来进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 对数值进行转换,适配手机,必须在初始化之前,否则有些数据不会起作用
*/
private void setSize(Context context) {
    BITMAP_WIDTH = DpOrSp2PxUtil.dp2pxConvertInt(context, BITMAP_HEIGHT);
    BITMAP_HEIGHT = DpOrSp2PxUtil.dp2pxConvertInt(context, BITMAP_HEIGHT);
//  EMOJI_SIZE = DpOrSp2PxUtil.dp2pxConvertInt(context, EMOJI_SIZE);
    DANMU_PADDING = DpOrSp2PxUtil.dp2pxConvertInt(context, DANMU_PADDING);
    DANMU_PADDING_INNER = DpOrSp2PxUtil.dp2pxConvertInt(context, DANMU_PADDING_INNER);
    DANMU_RADIUS = DpOrSp2PxUtil.dp2pxConvertInt(context, DANMU_RADIUS);
    DANMU_TEXT_SIZE = DpOrSp2PxUtil.sp2px(context, DANMU_TEXT_SIZE);
}

适配之后我们来看一下最终的效果。gif看起来有些卡顿,但在真机上并不会,有些模糊,将就一下。
danmuDemo

至此,我们就完成了弹幕的开发。

资源

原文:http://wangpeiyuan.cn/2016/02/24/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%BC%B9%E5%B9%95%E7%9A%84%E5%BC%80%E5%8F%91/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值