Android实现弹幕效果

来源:互联网 发布:学为软件下载 编辑:程序博客网 时间:2024/05/01 22:17

相信大家看视频的时候都会有弹幕效果,这似乎已经成为视频软件的标配,接下来让我们来看看如何实现这个弹幕效果。


一.弹幕效果分析


我可以看到,弹幕效果是在屏幕上方飘过,即从屏幕外一端出现,一直在屏幕中飘动至屏幕另一端,我们将其实现逻辑进行分解如下:
1.一个viewGroup中包含多个textView
2.textView包含平移动画
经过分解发现其逻辑一点也不复杂,分析完


二.实现

上面我们分析了自定义弹幕效果View的逻辑:
这里我们让我们自定义的的BarrageView继承RelateLayout,并实现onLayout方法。
    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        int childCount = this.getChildCount();        for(int i = 0; i < childCount; i++){            final View view = getChildAt(i);            if (view != null){                RelativeLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams();                if (lp.leftMargin <= 0){                    if(mDirection == FROM_RIGNG_TO_LEFT){                        view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(),                                lp.topMargin+view.getMeasuredHeight());                    }else if(mDirection == FROM_LEFT_TO_RIGHT){                        view.layout(-view.getMeasuredWidth(),lp.topMargin,0,                                lp.topMargin+view.getMeasuredHeight());                    }                }            }        }    }
这里很多人可能会问为什么是实现onLayout方法,我们的BarrageView继承了RelateLayout,即继承了一个viewGroup,而飘屏的弹幕均属于这个viewGroup的子View,我们的弹幕效果要实现必须在我们需要的时候往这个BarrageView中添加一个子view,添加子view我们就必须要为其指定在父控件中的位置。而每当我们像BarrageView中加入一个子view,就会执行其onLayout方法,所以我们最终就在onLayout中确定子view的初始位置
    从代码中我们可以看到这边我支持了两个方向,从左到右(FROM_LEFT_TO_RIGHT)和从右到左(FROM_RIGNG_TO_LEFT),这里我们分析从右到左,毕竟目前较多的软件都是从右到左飘屏
view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(),                                lp.topMargin+view.getMeasuredHeight());
这段代码写的很清晰,既然是从右到左飘屏,那我们添加的子view最开始的位置当然就是在屏幕外右端,如下图



接下来我们要添加子view,并为其设置相应的动画效果,使其达到飘屏的效果
private void createBarrageItemView(Context context,String text,int textSize,int textColor){        final TextView textView = new TextView(context);        textView.setBackgroundResource(R.drawable.barrage_item_bg);        if (textColor != 0){            textView.setTextColor(getResources().getColor(textColor));        }        if (textSize != 0){            textView.setTextSize(textSize);        }        textView.setText(text);        int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);        int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);        textView.measure(w, h);        int height =textView.getMeasuredHeight();        int width = textView.getMeasuredWidth();        int row = new Random().nextInt(100) % mRowNum;        while (needResetRow(row)){            row = new Random().nextInt(100) % mRowNum;        }        mRowPosList.add(row);        RelativeLayout.LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);        lp.topMargin = row * (height + ToolUtils.dip2px(context,10));//        lastRow = row;        textView.setLayoutParams(lp);        textView.setPadding(ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2), ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2));        textView.getBackground().setAlpha(mAlpha);        this.addView(textView);        ViewPropertyAnimator animator = null;        if(mDirection == FROM_RIGNG_TO_LEFT){            animator = textView.animate()                    .translationXBy(-(mScreenWidth + width + 80));        }else if(mDirection == FROM_LEFT_TO_RIGHT){            animator = textView.animate()                    .translationXBy(mScreenWidth + width + 80);        }        animator.setDuration(mDuration);        animator.setInterpolator(new LinearInterpolator());        animator.start();        animator.setListener(new Animator.AnimatorListener() {            @Override            public void onAnimationStart(Animator animation) {            }            @Override            public void onAnimationEnd(Animator animation) {//                mChildView.remove(textView);                BarrageView.this.removeView(textView);            }            @Override            public void onAnimationCancel(Animator animation) {            }            @Override            public void onAnimationRepeat(Animator animation) {            }        });        mChildView.add(textView);    }
添加子view需要注意一下几点:
利用随机数,生成子view出现在第几行,
int row = new Random().nextInt(100) % mRowNum;        while (needResetRow(row)){            row = new Random().nextInt(100) % mRowNum;        }        mRowPosList.add(row);
然后在判断子view是否需要重新设置所在行(需要设置是为了不让后面的子view与前面的子view重叠),其实现如下:
    public boolean needResetRow(int row){        int size = mRowPosList.size();        int sameRowPos =-1;        for(int i = size; i > 0; i--){            if (row == mRowPosList.get(i-1)){                sameRowPos = i-1;                break;            }        }        if (sameRowPos != -1){            TextView tv = mChildView.get(sameRowPos);            if (mScreenWidth -tv.getX() < tv.getWidth()){                return true;            }        }        return false;    }
该逻辑思想为,从所有子view中遍历,找到与当前需要添加的子view同一行的最后一个添加的子view,(我们用sameRowView表示),判断sameRowView是否显示完全,如果未显示完全就继续生成随机行数。(图画的丑 ,能看懂就行....)

这里指的一提的是,以上逻辑是存在问题的,如果每个子view都很长的话,当全部子view占满屏幕且都没显示完全时,我们将会陷入一个死循环,这样我们的应用就会卡死(为了制作gif图好看,采用这种方式),通过上网查阅,给出的方案是,只记录最后一个子view所在的位置,判断当前新家的子view是否与上一个子view同行,若是,则重选行数。
int row = new Random().nextInt(100) % mRowNum;        while (mLastRow == row){            row = new Random().nextInt(100) % mRowNum;        }        mLastRow = row;
很遗憾的是,上述的方法虽然不会卡死(不会卡死就很重要了,毕竟能卡死应用已经很严重,所以实际使用推荐使用这种方式,或者有其他更好的方式欢迎交流) 但是没有解决子view间相互覆盖的问题。如下图,我们1先加入BarrageView中,然后我们2加入并且我们2不会与1在同一行,如果同行就重选选行,同理3不会与2同行,但是3可以与1同行,因为1不是最后一个加入BarrageView的子view,但是确实存在了覆盖问题



至此我们的自定义弹幕BarrageView就分析完了,下面贴上调用代码与自定义BarrageView代码
MainActivity.java
public class MainActivity extends AppCompatActivity {    private BarrageView barrageView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        barrageView = (BarrageView) findViewById(R.id.barrageView);        barrageView.setBackgroundResource(0);        new Thread(new Runnable() {            @Override            public void run() {                for(int i = 0; i < 10000; i++){                    final int finalI = i;                    runOnUiThread(new Runnable() {                        @Override                        public void run() {                            if (finalI%2 == 0){                                barrageView.addBarrageItemView(MainActivity.this,"第" + finalI +"条弹幕");                            }else{                                barrageView.addBarrageItemView(MainActivity.this,"第" + finalI +"条弹幕"+"第" + finalI +"条弹幕");                            }                        }                    });                    try {                        Thread.sleep(500);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }).start();    }}
BarrageView.java
public class BarrageView extends RelativeLayout{    private int mRowNum = 6;    private long mDuration = 5000;//弹幕在屏幕显示的时间 默认5s    private int mAlpha = 180;//背景的透明度0-255    private int mDirection = FROM_RIGNG_TO_LEFT;//当前弹幕活动方向 默认从右到左    public static final int FROM_LEFT_TO_RIGHT = 1;//从左到右    public static final int FROM_RIGNG_TO_LEFT = 2;//从右到左    private int mScreenWidth;    private List<TextView> mChildView;    private LinkedList mRowPosList;    public BarrageView(Context context) {        super(context);        init(context);    }    public BarrageView(Context context, AttributeSet attrs) {        super(context, attrs);        init(context);    }    public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init(context);    }    private void init(Context context){        mScreenWidth = ToolUtils.getScreenWidth(context);        mChildView = new ArrayList<TextView>();        mRowPosList = new LinkedList();    }    /**     * 设置弹幕飘动方向     * @param direction 弹幕飘动方向 默认从右到左 FROM_RIGNG_TO_LEFT     */    public void setDirection(int direction){        mDirection = direction;    }    /**     * 设置弹幕飘屏时间     * @param duration 弹幕飘屏时间 默认5s     */    public void setDuration(long duration){        mDuration = duration;    }    /**     * 设置飘屏行数     * @param rowNum 飘屏行数 默认6条     */    public void setRowNum(int rowNum){        mRowNum = rowNum;    }    /**     * 设置item的背景透明度 范围:0~255     * @param alpha 取值0~255  0为全透明     */    public void setBackgroundAlpha(int alpha){        mAlpha = alpha;    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        int childCount = this.getChildCount();        for(int i = 0; i < childCount; i++){            final View view = getChildAt(i);            if (view != null){                RelativeLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams();                if (lp.leftMargin <= 0){                    if(mDirection == FROM_RIGNG_TO_LEFT){                        view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(),                                lp.topMargin+view.getMeasuredHeight());                    }else if(mDirection == FROM_LEFT_TO_RIGHT){                        view.layout(-view.getMeasuredWidth(),lp.topMargin,0,                                lp.topMargin+view.getMeasuredHeight());                    }                }            }        }    }    public void addBarrageItemView(Context context,String text,int textSize,int textColor){        createBarrageItemView(context,text,textSize,textColor);    }    public void addBarrageItemView(Context context,String text){        createBarrageItemView(context,text,0,0);    }    private void createBarrageItemView(Context context,String text,int textSize,int textColor){        final TextView textView = new TextView(context);        textView.setBackgroundResource(R.drawable.barrage_item_bg);        if (textColor != 0){            textView.setTextColor(getResources().getColor(textColor));        }        if (textSize != 0){            textView.setTextSize(textSize);        }        textView.setText(text);        int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);        int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);        textView.measure(w, h);        int height =textView.getMeasuredHeight();        int width = textView.getMeasuredWidth();        int row = new Random().nextInt(100) % mRowNum;        while (needResetRow(row)){            row = new Random().nextInt(100) % mRowNum;        }        mRowPosList.add(row);        RelativeLayout.LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);        lp.topMargin = row * (height + ToolUtils.dip2px(context,10));//        lastRow = row;        textView.setLayoutParams(lp);        textView.setPadding(ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2), ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2));        textView.getBackground().setAlpha(mAlpha);        this.addView(textView);        ViewPropertyAnimator animator = null;        if(mDirection == FROM_RIGNG_TO_LEFT){            animator = textView.animate()                    .translationXBy(-(mScreenWidth + width + 80));        }else if(mDirection == FROM_LEFT_TO_RIGHT){            animator = textView.animate()                    .translationXBy(mScreenWidth + width + 80);        }        animator.setDuration(mDuration);        animator.setInterpolator(new LinearInterpolator());        animator.start();        animator.setListener(new Animator.AnimatorListener() {            @Override            public void onAnimationStart(Animator animation) {            }            @Override            public void onAnimationEnd(Animator animation) {//                mChildView.remove(textView);                BarrageView.this.removeView(textView);            }            @Override            public void onAnimationCancel(Animator animation) {            }            @Override            public void onAnimationRepeat(Animator animation) {            }        });        mChildView.add(textView);    }    public boolean needResetRow(int row){        int size = mRowPosList.size();        int sameRowPos =-1;        for(int i = size; i > 0; i--){            if (row == mRowPosList.get(i-1)){                sameRowPos = i-1;                break;            }        }        if (sameRowPos != -1){            TextView tv = mChildView.get(sameRowPos);            if (mScreenWidth -tv.getX() < tv.getWidth()){                return true;            }        }        return false;    }}