精华内容
下载资源
问答
  • Android仿网络直播弹幕功能的实现

    千次阅读 2016-11-06 15:48:25
    首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有...

    现在网络直播越来越火,网络主播也逐渐成为一种新兴职业,对于网络直播,弹幕功能是必须要有的,如下图:


    首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有人发弹幕消息时,再将消息绘制到弹幕的View上面就可以了,下方肯定还有有操作界面View,可以让用户来发弹幕和送礼物的功能,原理示意图如下所示:


    参照原理图,下面一步一步来实现这个功能。

    实现视频的播放

    activity_main.xml

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000">
    
        <VideoView
            android:id="@+id/video_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>
    </RelativeLayout>
    MainActivity.java
    package com.jackie.bombscreen;
    
    import android.os.Build;
    import android.os.Bundle;
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.VideoView;
    
    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            VideoView videoView = (VideoView) findViewById(R.id.video_view);
            videoView.setVideoPath(Environment.getExternalStorageDirectory() + "/xiaoxingyun.mp4");
            videoView.start();
        }
        
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus && Build.VERSION.SDK_INT >= 19) {
                View decorView = getWindow().getDecorView();
                decorView.setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
            }
        }
    }
    最后别忘了设置AndroidMainfest.xml


    效果如下:


    实现弹幕的效果

    接下来我们开始实现弹幕效果。弹幕其实也就是一个自定义的View,它的上面可以显示类似于跑马灯的文字效果。观众们发表的评论都会在弹幕上显示出来,但又会很快地移出屏幕,既可以起到互动的作用,同时又不会影响视频的正常观看。

    我们可以自己来编写这样的一个自定义View,当然也可以直接使用网上现成的开源项目。那么为了能够简单快速地实现弹幕效果,这里我就准备直接使用由哔哩哔哩开源的弹幕效果库DanmakuFlameMaster。

    DanmakuFlameMaster库的项目主页地址是:https://github.com/Bilibili/DanmakuFlameMaster

    添加build.gradle依赖

    compile 'com.github.ctiao:DanmakuFlameMaster:0.5.3'
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000">
    
        <VideoView
            android:id="@+id/video_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>
    
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/danmaku_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </RelativeLayout>
    
    修改MainActivity.java

    package com.jackie.bombscreen;
    
    import android.graphics.Color;
    import android.os.Build;
    import android.os.Bundle;
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.VideoView;
    
    import java.util.Random;
    
    import master.flame.danmaku.controller.DrawHandler;
    import master.flame.danmaku.danmaku.model.BaseDanmaku;
    import master.flame.danmaku.danmaku.model.DanmakuTimer;
    import master.flame.danmaku.danmaku.model.IDanmakus;
    import master.flame.danmaku.danmaku.model.android.DanmakuContext;
    import master.flame.danmaku.danmaku.model.android.Danmakus;
    import master.flame.danmaku.danmaku.parser.BaseDanmakuParser;
    import master.flame.danmaku.ui.widget.DanmakuView;
    
    public class MainActivity extends AppCompatActivity {
        private boolean mIsShowDanmaku;
        private DanmakuView mDanmakuView;
        private DanmakuContext mDanmakuContext;
    
        private BaseDanmakuParser parser = new BaseDanmakuParser() {
            @Override
            protected IDanmakus parse() {
                return new Danmakus();
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            VideoView videoView = (VideoView) findViewById(R.id.video_view);
            videoView.setVideoPath(Environment.getExternalStorageDirectory() + "/xiaoxingyun.mp4");
            videoView.start();
    
            mDanmakuView = (DanmakuView) findViewById(R.id.danmaku_view);
            mDanmakuView.enableDanmakuDrawingCache(true);
            mDanmakuView.setCallback(new DrawHandler.Callback() {
                @Override
                public void prepared() {
                    mIsShowDanmaku = true;
                    mDanmakuView.start();
                    generateSomeDanmaku();
                }
    
                @Override
                public void updateTimer(DanmakuTimer timer) {
    
                }
    
                @Override
                public void danmakuShown(BaseDanmaku danmaku) {
    
                }
    
                @Override
                public void drawingFinished() {
    
                }
            });
    
            mDanmakuContext = DanmakuContext.create();
            mDanmakuView.prepare(parser, mDanmakuContext);
        }
    
        /**
         * 向弹幕View中添加一条弹幕
         * @param content       弹幕的具体内容
         * @param  withBorder   弹幕是否有边框
         */
        private void addDanmaku(String content, boolean withBorder) {
            BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
            danmaku.text = content;
            danmaku.padding = 5;
            danmaku.textSize = sp2px(20);
            danmaku.textColor = Color.WHITE;
            danmaku.setTime(mDanmakuView.getCurrentTime());
            if (withBorder) {
                danmaku.borderColor = Color.GREEN;
            }
            mDanmakuView.addDanmaku(danmaku);
        }
    
        /**
         * 随机生成一些弹幕内容以供测试
         */
        private void generateSomeDanmaku() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(mIsShowDanmaku) {
                        int time = new Random().nextInt(300);
                        String content = "" + time + time;
                        addDanmaku(content, false);
                        try {
                            Thread.sleep(time);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
        /**
         * sp转px的方法。
         */
        public int sp2px(float spValue) {
            final float fontScale = getResources().getDisplayMetrics().scaledDensity;
            return (int) (spValue * fontScale + 0.5f);
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            if (mDanmakuView != null && mDanmakuView.isPrepared()) {
                mDanmakuView.pause();
            }
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
                mDanmakuView.resume();
            }
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mIsShowDanmaku = false;
            if (mDanmakuView != null) {
                mDanmakuView.release();
                mDanmakuView = null;
            }
        }
        
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus && Build.VERSION.SDK_INT >= 19) {
                View decorView = getWindow().getDecorView();
                decorView.setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
            }
        }
    }
    效果图如下:


    加入操作界面

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000">
    
        <VideoView
            android:id="@+id/video_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>
    
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/danmaku_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
        <LinearLayout
            android:id="@+id/operation_layout"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_alignParentBottom="true"
            android:background="#fff"
            android:visibility="gone">
    
            <EditText
                android:id="@+id/edit_text"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />
    
            <Button
                android:id="@+id/send"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="Send" />
        </LinearLayout>
    </RelativeLayout>
    package com.jackie.bombscreen;
    
    import android.graphics.Color;
    import android.os.Build;
    import android.os.Bundle;
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.text.TextUtils;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.LinearLayout;
    import android.widget.VideoView;
    
    import java.util.Random;
    
    import master.flame.danmaku.controller.DrawHandler;
    import master.flame.danmaku.danmaku.model.BaseDanmaku;
    import master.flame.danmaku.danmaku.model.DanmakuTimer;
    import master.flame.danmaku.danmaku.model.IDanmakus;
    import master.flame.danmaku.danmaku.model.android.DanmakuContext;
    import master.flame.danmaku.danmaku.model.android.Danmakus;
    import master.flame.danmaku.danmaku.parser.BaseDanmakuParser;
    import master.flame.danmaku.ui.widget.DanmakuView;
    
    public class MainActivity extends AppCompatActivity {
        private boolean mIsShowDanmaku;
        private DanmakuView mDanmakuView;
        private DanmakuContext mDanmakuContext;
    
        private BaseDanmakuParser parser = new BaseDanmakuParser() {
            @Override
            protected IDanmakus parse() {
                return new Danmakus();
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            VideoView videoView = (VideoView) findViewById(R.id.video_view);
            videoView.setVideoPath(Environment.getExternalStorageDirectory() + "/xiaoxingyun.mp4");
            videoView.start();
    
            mDanmakuView = (DanmakuView) findViewById(R.id.danmaku_view);
            mDanmakuView.enableDanmakuDrawingCache(true);
            mDanmakuView.setCallback(new DrawHandler.Callback() {
                @Override
                public void prepared() {
                    mIsShowDanmaku = true;
                    mDanmakuView.start();
                    generateSomeDanmaku();
                }
    
                @Override
                public void updateTimer(DanmakuTimer timer) {
    
                }
    
                @Override
                public void danmakuShown(BaseDanmaku danmaku) {
    
                }
    
                @Override
                public void drawingFinished() {
    
                }
            });
    
            mDanmakuContext = DanmakuContext.create();
            mDanmakuView.prepare(parser, mDanmakuContext);
    
            final LinearLayout operationLayout = (LinearLayout) findViewById(R.id.operation_layout);
            final Button send = (Button) findViewById(R.id.send);
            final EditText editText = (EditText) findViewById(R.id.edit_text);
            mDanmakuView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (operationLayout.getVisibility() == View.GONE) {
                        operationLayout.setVisibility(View.VISIBLE);
                    } else {
                        operationLayout.setVisibility(View.GONE);
                    }
                }
            });
            
            send.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    String content = editText.getText().toString();
                    if (!TextUtils.isEmpty(content)) {
                        addDanmaku(content, true);
                        editText.setText("");
                    }
                }
            });
    
            getWindow().getDecorView().setOnSystemUiVisibilityChangeListener (new View.OnSystemUiVisibilityChangeListener() {
                @Override
                public void onSystemUiVisibilityChange(int visibility) {
                    if (visibility == View.SYSTEM_UI_FLAG_VISIBLE) {
                        onWindowFocusChanged(true);
                    }
                }
            });
        }
    
        /**
         * 向弹幕View中添加一条弹幕
         * @param content       弹幕的具体内容
         * @param  withBorder   弹幕是否有边框
         */
        private void addDanmaku(String content, boolean withBorder) {
            BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
            danmaku.text = content;
            danmaku.padding = 5;
            danmaku.textSize = sp2px(20);
            danmaku.textColor = Color.WHITE;
            danmaku.setTime(mDanmakuView.getCurrentTime());
            if (withBorder) {
                danmaku.borderColor = Color.GREEN;
            }
            mDanmakuView.addDanmaku(danmaku);
        }
    
        /**
         * 随机生成一些弹幕内容以供测试
         */
        private void generateSomeDanmaku() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(mIsShowDanmaku) {
                        int time = new Random().nextInt(300);
                        String content = "" + time + time;
                        addDanmaku(content, false);
                        try {
                            Thread.sleep(time);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
        /**
         * sp转px的方法。
         */
        public int sp2px(float spValue) {
            final float fontScale = getResources().getDisplayMetrics().scaledDensity;
            return (int) (spValue * fontScale + 0.5f);
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            if (mDanmakuView != null && mDanmakuView.isPrepared()) {
                mDanmakuView.pause();
            }
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
                mDanmakuView.resume();
            }
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mIsShowDanmaku = false;
            if (mDanmakuView != null) {
                mDanmakuView.release();
                mDanmakuView = null;
            }
        }
    
    
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus && Build.VERSION.SDK_INT >= 19) {
                View decorView = getWindow().getDecorView();
                decorView.setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
            }
        }
    }
    效果图如下:

    自己发的弹幕有绿色边框,很容易区分。

    基本上实现了弹幕的功能,当然,里面的知识点还有很多,这只是最基本的功能。有时间的话,建议学学DanmakuFlameMaster,里面还有很多炫酷的功能。

    展开全文
  • 看直播的童鞋们应该会经常看到满屏幕的滚动弹幕,看到密密麻麻的弹幕第一印象就是怎么样高效加载来避免卡顿,弹幕组成部分包含用户头像、用户昵称、弹幕的内容、表情等,本文介绍的实现原理就是把这几部分绘制成一张...
  • 首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有...
  • 看直播的童鞋们应该会经常看到满屏幕的滚动弹幕,看到密密麻麻的弹幕第一印象就是怎么样高效加载来避免卡顿,弹幕组成部分包含用户头像、用户昵称、弹幕的内容、表情等,本文介绍的实现原理就是把这几部分绘制成一张...

    看直播的童鞋们应该会经常看到满屏幕的滚动弹幕,看到密密麻麻的弹幕第一印象就是怎么样高效加载来避免卡顿,弹幕组成部分包含用户头像、用户昵称、弹幕的内容、表情等,本文介绍的实现原理就是把这几部分绘制成一张图片,然后通过定时器移动弹幕图片,当图片不在屏幕范围内即销毁

    先看下效果


    下面我会详细介绍下实现原理

    • 1 .获取弹幕数据来源,因为我是模拟生成弹幕,弹幕的数据存放在工程里的plist文件中

    emotions存放这条弹幕的表情,type表示是否是自己发的,text表示弹幕内容,userName表示用户昵称。取出plist文件的数据并转换成model。

    #pragma mark - 获取数据源
    - (void)loadData{
        // 获取plist全路径
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"barrage.plist" ofType:nil];
        // 从指定的路径中加载数据
        NSArray *array = [NSArray arrayWithContentsOfFile:filePath];
        
        // 遍历数组
        for (NSDictionary *dict in array) {
            // 字典转模型
            BAModle *barrageM = [BAModle barrageWithDict:dict];
            [self.danMus addObject:barrageM];
        }
    }
    复制代码

    • 2 .根据模型生成弹幕图片,通过点击屏幕生成模型,根据模型绘制图片。
    #pragma mark - 触摸屏幕响应事件
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        // 获得一个随机整数
        NSInteger index = arc4random_uniform((u_int32_t)self.danMus.count);
        // 获得一个随机模型
        BAModle *danMu = self.danMus[index];
        // 根据模型生成图片
        BAImage *image = [self.danMuview imageWithBarrage:danMu];
        // 调整弹幕加载区域
        image.x = self.view.bounds.size.width;
        image.y = arc4random_uniform(self.danMuview.bounds.size.height - image.size.height);
        // 把图片加到弹幕view上
        [self.danMuview addImage:image]; 
    }
    复制代码

    下面是具体绘制弹幕图片过程,我先简单介绍下,首先在绘图之前要确定上下文的尺寸,相当于画板的大小,画板的长 = 头像的长 + 昵称的长 + 内容的长 + 表情的长 * 表情个数 + 间距。然后就是分别绘制背景图片,用户昵称,内容和表情,最后返回一张图片。 此处有两点需要注意: 1.由于头像是矩形,想显示成圆形,要先画一个圆,并设置超出圆形的部分要裁剪,再绘制头像。 2.由于上面设置超出圆形的部分要裁剪,那即将要绘制背景岂不是要被裁剪,所以在绘制圆形区域上一句执行了CGContextSaveGState(ctx)表示复制了一份画板(上下文)存到栈里,在绘制背景图片之前执行CGContextRestoreGState(ctx),表示用之前保存的画板替换当前的,因为之前保存的画板没有设置超出圆形区域要裁剪的需求,当然替换当前的画板,会把当前画板上的绘图也copy过去。

    #pragma mark - 绘制弹幕图片
    - (BAImage *)imageWithBarrage:(BAModle *)danMu{
        // 开启绘图上下文
        //
        UIFont *font = [UIFont systemFontOfSize:13];
        // 头像
        CGFloat iconH = 30;
        CGFloat iconW = iconH;
        // 间距
        CGFloat marginX = 5;
        
        // 表情的尺寸
        CGFloat emotionW = 25;
        CGFloat emotionH = emotionW;
        // 计算用户名占据的区域
        CGSize nameSize = [danMu.userName boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size;
        // 计算内容占据的区域
        CGSize textSize = [danMu.text boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size;
        
        // 位图上下文的尺寸
        CGFloat contentH = iconH;
        CGFloat contentW = iconW + 4 * marginX + nameSize.width + textSize.width + danMu.emotions.count * emotionH;
        
        CGSize contextSize = CGSizeMake(contentW, contentH);
        UIGraphicsBeginImageContextWithOptions(contextSize, NO, 0.0);
        
        // 获得位图上下文
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        // 将上下文保存到栈中
        CGContextSaveGState(ctx);
        // 1.绘制圆形区域
        CGRect iconFrame = CGRectMake(0, 0, iconW, iconH);
        // 绘制头像圆形
        CGContextAddEllipseInRect(ctx, iconFrame);
        // 超出圆形的要裁剪
        CGContextClip(ctx);
        // 2.绘制头像
        UIImage *icon = danMu.type ? [UIImage imageNamed:@"headImage_1"]:[UIImage imageNamed:@"headImage_2"];
        [icon drawInRect:iconFrame];
        // 将上下文出栈替换当前上下文
        CGContextRestoreGState(ctx);
        // 3.绘制背景图片
        CGFloat bgX = iconW + marginX;
        CGFloat bgY = 0;
        CGFloat bgW = contentW - bgX;
        CGFloat bgH = contentH;
        danMu.type ? [[UIColor orangeColor] set]:[[UIColor whiteColor] set];
        [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(bgX, bgY, bgW, bgH) cornerRadius:20.0] fill];
        
        // 4.绘制用户名
        CGFloat nameX = bgX + marginX;
        CGFloat nameY = (contentH - nameSize.height) * 0.5;
        [danMu.userName drawAtPoint:CGPointMake(nameX, nameY) withAttributes:@{NSAttachmentAttributeName:font,NSForegroundColorAttributeName:danMu.type == NO ? [UIColor orangeColor]:[UIColor blackColor]}];
        
        // 5.绘制内容
        CGFloat textX = nameX + nameSize.width + marginX;
        CGFloat textY = nameY;
        [danMu.text drawAtPoint:CGPointMake(textX, textY) withAttributes:@{NSAttachmentAttributeName:font,NSForegroundColorAttributeName:danMu.type == NO ? [UIColor blackColor]:[UIColor whiteColor]}];
        
        // 6.绘制表情
        __block CGFloat emotionX = textX + textSize.width;
        CGFloat emotionY = (contentH - emotionH) * 0.5;
        [danMu.emotions enumerateObjectsUsingBlock:^(NSString *emotionName, NSUInteger idx, BOOL * _Nonnull stop) {
            // 加载表情图片
            UIImage *emotion = [UIImage imageNamed:emotionName];
            [emotion drawInRect:CGRectMake(emotionX, emotionY, emotionW, emotionH)];
            // 修改emotionX
            emotionX += emotionW;
        }];
        // 从位图上下文中获得绘制好的图片
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        
        return [[BAImage alloc] initWithCGImage:image.CGImage scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp];
    }
    
    复制代码
    • 3 .开启绘图定时器,回调方法是setNeedsDisplay,这样就会执行- (void)drawRect:(CGRect)rect每次修改image.x(由于UIImage没有x、y属性,所以写了个类拓展BAImage),滚动不在屏幕范围内的会销毁
    #pragma mark - 添加定时器
    - (void)addTimer{
        if (self.link) {
            return;
        }
        // 每秒执行60次回调
        CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
        // 将定时器添加到runLoop
        [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
        self.link = link;
    }
    #pragma mark - 绘制移动
    - (void)drawRect:(CGRect)rect{
        
        for (BAImage *image in self.imageArray) {
            image.x -= 3;
            // 绘制图片
            [image drawAtPoint:CGPointMake(image.x, image.y)];
            // 判断图片是否超出屏幕
            if (image.x + image.size.width < 0) {
                [self.deleteImageArray addObject:image];
            }
        }
        // 移除超过屏幕的弹幕
        for (BAImage *image in self.deleteImageArray) {
            [self.imageArray removeObject:image];
        }
        [self.deleteImageArray removeAllObjects];
    }
    复制代码

    最后附上gitHub地址

    谢谢各位,欢迎指教!

    转载于:https://juejin.im/post/5a3b13a9518825698e723931

    展开全文
  • 前段时间做了游戏的相关业务,其中弹幕相关的内容自成一块。弹幕已经不只是最初的视频弹幕了,战火...尤其,在集团内部,怎么快速地搭建起一个可用的弹幕框架来?本文分3块来阐述。 弹幕渲染层 弹幕数据通道

    前段时间做了游戏的相关业务,其中弹幕相关的内容自成一块。弹幕已经不只是最初的视频弹幕了,战火已经烧到了评论区,烧到了手机淘宝的首页搜索结果。作为一种近几年迅速燃起的内容呈现形式,有必要适时引入,对于休闲化、娱乐化的业务更是如此。那么,要做出一个较为完整的弹幕效果来,需要哪几个部分呢?尤其是,在集团内部,怎么快速地搭建起一个可用的弹幕框架来?本文分3块来阐述。

    1. 弹幕渲染层
    2. 弹幕数据通道
    3. 弹幕服务逻辑

    弹幕渲染层

    目前弹幕的呈现载体主要是Web、无线客户端。因为我们的工作主要针对无线端,所以本文主要以无线端为例——包括iOS,Android两类系统。

    碰撞检测

    弹幕无非是动画,是分布在时间轴上图像的连续运动。自然可以用Native的动画来实现。不过弹幕动画有一个重要的特征,即保持动画元素(sprites)尽可能少地碰撞,以使弹幕承载的信息能够清晰地传达,执行碰撞检测是必须的。但弹幕里的碰撞检测相对简单,因为弹幕的运动轨迹相对简单并且容易预测,所以只需要在一条弹幕将要显示之前,根据已经显示的弹幕(位置、速度、活跃的时间等)来确定他的运动轨迹。以尽最大可能地在其生命周期内不与已有弹幕冲突。

    弹幕碰撞检测范例

    弹幕的同步问题

    弹幕不是超然而独立的,往往相伴业务场景而生,目前可见最多的场景是视频,直播或者录播皆有。到此时则涉及到一个时间同步的问题。比如,一位用户在看一段时间第314s的时候突然有感而发,发出了一条弹幕,自然希望其他观众能够在看到视频此刻看到他的弹幕。所以一条弹幕上屏的时间是需要明确的,想想那些年文不对题的字幕君吧。那么,如何实现呢?一般,可以为一条弹幕提供一个时间点delay,当到了这个时刻,由控制器把这条弹幕播放出去。但仅仅这些是不够的,因为视频还存在暂停,存在快进快退,所以你必须也为弹幕组件提供类似的接口,以期能和视频内容同步。其他的应用场景也是类似的。比如下面的样子(弹幕在向左运动):

    弹幕的样式

    弹幕的运动样式主要有两种,一种是横向的过场弹幕,一种是纵向的浮动弹幕。弹幕的内容形式不外乎一段文字或者图片,其中以文字为主。对于文字,则有文字的颜色、背景、字体、边框等属性,这一切必须是灵活可配的。当然实际应用中一个APP需要的是风格统一的、优雅美观的弹幕动画。所以弹幕的方向不要太混乱,不要有太多不一样的主题配置。你可以定义几类色调协调但样式不同的弹幕,然后由业务代码决定使用哪一种风格的弹幕。

    渲染效率

    性能直接关系到用户体验。在绝大多数场景中,锦上添花的弹幕往往伴随着具体的业务逻辑,业务逻辑会占用CPU——甚至很高的CPU,比如视频解码———所以弹幕动画应该尽可能地使用GPU渲染。为应对线上可能的大规模弹幕的情况,本地最好也能测试到大量弹幕的情况。可以使用一个定时器,模拟客户端频繁接受渲染弹幕的情况,看看实际中弹幕的性能究竟如何。

    限流

    弹幕稀稀疏疏地铺满半屏窗口,朦胧中犹抱琵琶半遮面的感觉,自然是最好的。但万一遇到弹幕决堤,内容疯狂涌来,那当如何应对?渲染内容层层堆叠,既看不清,又降低了系统应用性能,为此可以在业务或者组件中选择限流。

    弹幕数据层

    若不考虑弹幕在用户间共享,只需下图左侧的模块即可;若需引入弹幕共享、存储功能,则如下图右侧所示。

    简要结构图

    但实际情况往往比这复杂。弹幕很多时候是实时的,最好使用长连接来传输数据。业务导向的项目,很少从零开始开发专门的弹幕服务通道,而是尽可能地应用已有的服务组件。淘宝在长连通道上有多个选择,但其功能又是不尽相同的。这种不同也会带来弹幕实现方案的不同。比如通道A支持订阅功能,消息会根据订阅关系分发;而通道B是单纯的通道,订阅关系由业务方维护,凡是发送到客户端的消息都会接收,所以流量需要业务服务端来控制。

    经过服务端的必要性

    仅仅使用长连接通道是不够的,还需引入业务服务器,其原因如下:

    • 如果长连通道不支持客户端发送消息,那么弹幕的发送要走其它的接口
    • 因业务原因,需要统一多个长连接通道,以便更好地做 多端同步,故引入中间服务器做协调
    • 一些业务相关的需求,不适合在长连接服务器上做,比如内容过滤、弹幕存储、服务端限流等

    整体的数据流如下图所示:

    详细结构图

    消息格式制定

    通过长连接传输的弹幕消息会有一些附加数据需要考虑,比如弹幕的样式、出现的时间,随着业务的扩展,可能需要更多的辅助字段。所以弹幕消息必须能够向后兼容,一般可设置为message,version两个字段,message为纯粹的json字符串,version表示消息的版本号。先解析version,根据判断得到的version选择响应的解析样式。太多的附加信息会降低数据的利用率,此是需要权衡的地方。当然,如果针对的是在线视频业务,弹幕的流量相比于视频流而言,就显得不那么重要了。

    自发的弹幕消息

    主要有两种:

    1. 用户发送了弹幕消息后,通过网络发送消息的同时直接将弹幕数据上屏,以提升用户所见即所得的体验;当收到相同的弹幕消息后,将消息抛弃。
    2. 用户发送了弹幕消息后,通过正常的网络接收消息然后渲染呈现,这样会因延时损失一定的用户体验,但逻辑简单,并且可以控制所有弹幕数据。

    弹幕服务层

    主题维护

    主题代表弹幕消息围绕的中心。在不同的业务场景中,主题的呈现方式可能是不同的。在视频直播业务中,主题代表了一个个直播房间,弹幕围绕着视频展开;在新闻咨询业务中,主题代表了一则则新闻,弹幕围绕着新闻展开。客户端与主题存在多对一的关系,如下图所示:

    主题房间维护

    用户U1、U2订阅了主题T1,用户U3、U4订阅了主题T2。由于处于不同的语境中,U1、U2发送的弹幕U3、U4应该是不能接收到的,反之亦然。很自然服务端需要维护一个用户到主题的映射表简单的实现是,客户端监测到用户进入特定主题之后,发送一条网络请求登记这样一条订阅知识;用户离开特定主题时发送网络请求注销登记。但由于实际客户端运行场景复杂,离开特定主题不一定来得及发送网络请求。补充方案是,由客户端每隔特定时间心跳一次,用以告知服务端维护映射表。一旦服务端一段时间没有监听到心跳信息,就取消映射表中的一条订阅。这里需要注意的是,服务端需要防止心跳的伪造,否则可能映射表可能会因攻击而混乱掉。一旦映射表正确建立,用户发送的弹幕消息就可以准确传达到相同主题的用户客户端了。

    弹幕存储

    对于直播等即时性业务,弹幕数据一般没有重播的必要;但是对于录播,则需要持久化弹幕,如是方能在其他用户看视频的时候看到其他人发出过的弹幕消息。持久化这类弹幕数据,必须在存储弹幕的时候带上弹幕对应的时间点。在用户进入了某一主题之后,批量返回给客户端对应的弹幕数据,由客户端将弹幕数据对应到视频业务响应的时间点上;如果此主题对应的弹幕数据很多,服务端可能实现做一定的筛选;对于录播同时新发送的弹幕,则由服务端记录并添加到对应的弹幕数据列表中。

    展开全文
  • 纯js 实现弹幕效果

    万次阅读 2017-08-02 10:05:17
    弹幕时近几年新兴的一种技术,接下来我要展示的就是怎么实现类似弹幕的效果。 先贴下效果图: 说是效果,要拿出去用肯定是不行的。 先说一下是怎么样的实现方法。弹幕肯定是出现在屏幕上,所以我们暂时把出现...

    弹幕时近几年新兴的一种技术,接下来我要展示的就是怎么实现类似弹幕的效果。

    先贴下效果图:



    说是效果,要拿出去用肯定是不行的。

    先说一下是怎么样的实现方法。弹幕肯定是出现在屏幕上,所以我们暂时把出现弹幕的窗口称为背景。

    弹幕肯定尽量不能重叠在一起,所以我把整个弹幕出现的背景划分为几块,如下图:

    1
    2
    3
    4
    5

    什么意思呢?就是这里有5条弹道,弹幕就是出现在这5条弹道之间的一条。具体划多少条弹道,每条弹道的宽度要看你的背景和你自己的设计了。

    好了,弹道有了,怎么创建弹幕呢?这里我们用到了appendChild方法,每个弹幕的内容都是随机的。然后我固定了弹幕的数量,并且加了定时器,

    当弹幕到达左边时,弹幕内容再次随机,输入框发送的弹幕出现一次后,将内容加入到预备词库中,并将此弹幕删除。防止弹幕过多报错。

    同时每个弹幕出现的时机肯定不能相同,所以我在每个弹幕最开始出现时加了延迟。

    大概的设计就是这样了,这里还是总结一下存在的问题:

    1.弹幕有时会出现同时出现在同一个弹道上,甚至重叠,暂时还没找到原因。

    2.当输入框连续多次发送弹幕时,可能会因为弹道不够出现意外的情况。

    3.与真正的弹幕相比,只是简易版的,功能单一。


    代码贴上:

    <!--作者:natural_live 时间:2017-7-30 -->
    <!doctype html>
    <html lang="en">
     <head>
      <meta charset="UTF-8">
      <meta name="Generator" content="EditPlus®">
      <meta name="Author" content="natural_live">
      <meta name="Keywords" content="barrage">
      <meta name="Description" content="">
      <title>弹幕</title>
      <style>
    	*{margin:0;padding:0;}
    
    	#barrage{
    		margin:auto;
    		margin-top:50px;
    		position:relative;
    		width:800px;
    		height:400px;
    		background:#fff;
    		border:2px solid #ffcc00;
    	}
    
    	#barrage div{
    		width:100%;
    		height:20px;
    		line-height:20px;
    		position:absolute;
    	}
    
    	#btn{
    		margin:auto;
    		margin-top:30px;
    		height:50px;
    		width:300px;
    	}
    	#text{
    		font-size:20px;
    		height:30px;
    		border-radius:4px;
    		border:1px solid #c8cccf;
    		color:#6a6f77;
    	}
    	#submit{
    		padding:7px;
    		font-size:14px;
    		height:30px;
    		border-radius:4px;
    		border:1px solid #c8cccf;
    	}
      </style>
     </head>
     <body>
    	<div id="barrage"></div>
    	<div id="btn">
    		<input type="text" id="text"></input>
    		<input type="button" id="submit" value="发送"></input>
    	</div>
    	<script>
    		var timer=null;
    		var current=[];//存储当前输入框的内容
    		var newarr=[];//存储每个弹幕距左边框的距离
    		var flag=0;//标志量
    		var num=new Array();//数组,用来存储划分每个块的序号
    		//var t=12;
    		var words = ["富强","民主","文明","和谐","自由","平等","公正","法治","爱国","敬业","诚信","友善"];
    		function $(id){
    			return document.getElementById(id);
    		}
    		for(var i=0;i<$("barrage").offsetHeight/20 - 1;i++){
    			num.splice(i,0,i);//将整个显示框划分成多个块,并对每个块进行标号
    			//console.log(num)
    		}
    		//console.log(num)
    		//console.log(num.length)
    
    		window.onload = function(){//加载页面发生的事件
    
    			clearInterval(timer);//清除定时器
    			for(var i = 0;i<10;i++){
    				setTimeout(function(){
    					var word=words[random(0,words.length-1)];//随机产生一个弹幕的内容
    					create(word);//创建一个弹幕
    				},100*random(10,100))//给弹幕随机加一个延迟
    				
    			}
    			timer=setInterval(move,20);//开启定时器
    		}
    
    		function create(w){//创建一个弹幕
    				var node=document.createElement("div");//创建一个div元素,用来存储弹幕的信息
    				//console.log(words.length)
    				node.innerHTML=w;
    				//console.log($("barrage").offsetHeight)
    				var t= random(0,num.length-1);
    				//console.log(num)
    				node.style.top=num[t]*20+"px";//从划分的块中随机选中一块。
    				Delete(num[t]);//删除已被选中的块
    				//console.log(t)
    				//console.log(node.style.top);
    				node.style.left="800px";
    				node.style.color="#"+randomColor();//随机颜色
    				$("barrage").appendChild(node);//插入子节点
    				flag++;//创建了一个新弹幕时,更新为0
    				
    				//console.log(node.offsetLeft)
    		}
    
    		function move(){
    			var arr=$("barrage").getElementsByTagName("div");//获取所有的弹幕
                for(var i=0;i<arr.length;i++){
                    newarr.push(arr[i].offsetLeft);//将每个弹幕距左边边框的距离分别存储在newarr数组中
                    arr[i].style.left=newarr[i]+"px";//更新距离
                    newarr[i] = newarr[i] - 2;//每次减少2px
                    if(newarr[i]<0){
    					//console.log(arr[i].innerHTML)
    					if(currentTest(arr[i].innerHTML) && flag != 0){//当是从输入框发送的弹幕时而且是第一次时,将内容添加到预备的词库中,并删除这个div元素。这么做是为了将弹幕数量维持在一定数量,防止在输入框发送大量弹幕,导致出现错误。
    						//console.log(current)
    						words.push(arr[i].innerHTML);
    						$("barrage").removeChild(arr[i]);
    						newarr.splice(i,1);//在newarr中删除这个div
    						flag--;
    					}else{//当弹幕到达最左边时,弹幕内容再次随机,同时,将这个块加入到预选块中,并在预选块中随机再选一个,颜色也再次随机,这样就保持块的数量不变。
    						newarr[i]=800;
    						//console.log(parseInt(arr[i].style.top))
    						//console.log(arr[i].style.top/20)
    						arr[i].innerHTML=words[random(0,words.length-1)];
    						num.splice(num.length,0,parseInt(arr[i].style.top)/20);
    						
    						
    						var t= random(0,num.length);
    						arr[i].style.top=num[t]*20+"px";
    						Delete(num[t]);
    						//console.log(num)
    						//console.log(node.style.top);
    						arr[i].style.left="800px";
    						arr[i].style.color="#"+randomColor();
    					}
    				}
                }
    		}
    
    		$("submit").οnclick=function(){//输入款发送弹幕
    			create($("text").value);
    			current[current.length]=$("text").value;
    			//console.log(current)
    			$("text").value="";
    		}
    		//console.log(num)
    		function Delete(m){//从预选块中删除已被选择的块
    			for(var i = 0;i < num.length;i++){
    				if(num[i] == m){
    					//console.log(m)
    					num.splice(i,1);
    				}
    			}
    		}
    
    		function currentTest(m){
    			var fl=false;
    			for(var i = 0;i < current.length;i++){
    				if(current[i] == m){
    					//console.log(m)
    					current.splice(i,1);
    					fl=true;
    				}
    			}
    			return fl;
    		}
    
    		function randomColor(){//随机颜色
    			var color=Math.ceil(Math.random()*16777215).toString(16);
    
    			while(color.length < 6){
    				color = "0" + color;
    			}
    			return color;
    		}
    
    		function random(m,n){//随机在m、n之间的整数
    			return Math.round(Math.random()*(n - m)) + m;
    		}
    	</script>
     </body>
    </html>
    


    展开全文
  • 弹幕看得多了,告诉你直播程序源码怎么开发弹幕 如今各大直播平台都有弹幕功能,貌似不存在没有弹幕的视频直播平台。弹幕元素比起留言板等交互性和实时性更高,深受广大基友们喜欢。 了解发现,目前弹幕的主要实现...
  • 首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有...
  • B站智能防挡弹幕的一种python实现

    千次阅读 2019-01-16 14:23:11
    某天代码写得老眼昏花,去B站上摸鱼,突然发现奇怪的现象:...我不知道B站是怎么实现的,但我脑中闪过一个想法:能不能用 Python 实现?简单搜索了一下“python 前景提取”,发现 OpenCV 的 GrabCut 提供了这样的功...
  • 大家对B站不陌生吧,特别是他的弹幕系统是很多网站都在模仿的,但是你知道他是怎么制作的吗?今天小猿圈web前端讲师就用jQuery实现弹幕评论效果,希望对你有所启发。首先第一步:布局CSS部分<style> *{padding...
  • 某天代码写得老眼昏花,去B站上摸鱼,突然发现奇怪的现象: 哟... 我不知道B站是怎么实现的,但我脑中闪过一个想法:能不能用 Python 实现?简单搜索了一下“python 前景提取”,发现 OpenCV 的 GrabCut 提供了这样...
  • 目前最大的聊天群组2000人,直播中的弹幕说白了也就是聊天室 有大神知道他们用什么技术实现能让十几万人同时聊天的吗
  • <div><h2>写在最前 本次分享一下使用canvas来进行视频播放并且添加弹幕功能。 欢迎关注我的博客</a>,不定期更新中—— 效果图 示例源码见:源码地址 ...
  • 大家对B站不陌生吧,特别是他的弹幕系统是很多网站都在模仿的,但是你知道他是怎么制作的吗?今天小猿圈web前端讲师就用jQuery实现弹幕评论效果,希望对你有所启发。首先第一步:布局CSS部分<style> *{padding...
  • 目录一、使用sklearn里面的LatentDirichletAllocation做主题挖掘二、使用gensim的ldamodel做主题挖掘三、如何将结果可视化四、...关于《后浪》的B站弹幕分析总结(三)——怎么制作好看的交互式词云 这一步的实现是
  • 弹幕功能在很多平台上都有,直播平台,视频播放器,音乐等上面都有,直播平台发送弹幕和主播互动,那么弹幕效果是怎么实现的呢。案例主要是实现视频出现弹幕,可以文字和表情弹幕,启动和关闭弹幕,发布弹幕。效果图:...
  • 大家对B站不陌生吧,特别是他的弹幕系统是很多网站都在模仿的,但是你知道他是怎么制作的吗?今天小猿圈web前端讲师就用jQuery实现弹幕评论效果,希望对你有所启发。首先第一步:布局CSS部分<style> *{...
  • 大家对B站不陌生吧,特别是他的弹幕系统是很多网站都在模仿的,但是你知道他是怎么制作的吗?今天小猿圈web前端讲师就用jQuery实现弹幕评论效果,希望对你有所启发。首先第一步:布局CSS部分<style> *{padding...
  • 本文主要分享下直播中的弹幕、键盘还有带货卡片的交互效果的实现方式 效果图 实现思路 通过监听键盘的状态以及商品卡片的状态对弹幕区域做 translationY 动画。实现思路就很简单,下面有一些难点 监听键盘状态...
  • axure原型设计之弹幕

    2017-05-15 16:31:35
    相信大家对看视频时候的弹幕都不陌生,很多时候甚至会觉得弹幕比视频本身还有趣,因此,现在大多数...既然这个功能这么重要,那么在axure原型设计中又是怎么实现的呢?这篇文章就是教大家如何设计逼真的弹幕原型图~~
  • 怎么在Axure中,完成跑马灯广告,以及弹幕效果呢?一起来看看~方案1:所需元件:1个动态面板内含1个文本,动态面板控制文本显示区域(动态面板就是一个xy坐标轴,00代表与之接触初始位,正值时元件显示为负则隐藏。)...
  • 用Python分析b站弹幕 目录 1 环境 2 需求分析 3 代码实现 4 后记 纸巾再湿也干垃圾?瓜子皮再干也湿垃圾??最近大家都被垃圾分类折磨的不行,傻傻的你是否拎得清?自2019.07.01开始,上海已率先实施垃圾分类...
  • 用Python分析b站弹幕 目录 0 引言 1 环境 2 需求分析 3 代码实现 4 后记 0 引言 纸巾再湿也干垃圾?瓜子皮再干也湿垃圾??最近大家都被垃圾分类折磨的不行,傻傻的你是否拎得清?自2019.07.01...
  • 困扰我的我弄了半天了,不知道怎么设置再次按下热键,或者按下其他热键来停止刷屏。 望大神回答!!! #include <stdio.h> #include <windows.h> int main() { RegisterHotKey(NULL, 1, 0, VK_...

空空如也

空空如也

1 2 3 4
收藏数 66
精华内容 26
关键字:

弹幕是怎么实现的