版权声明:本文为博主原创文章,未经博主允许不得转载。
//2016/08/03///
/by XBW///
/android studio//
先上图,看效果
DanmakuItem.java
- package com.xbw.danmu.danmu.opendanmaku;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.text.Layout;
- import android.text.SpannableString;
- import android.text.StaticLayout;
- import android.text.TextPaint;
- import java.util.Random;
- public class DanmakuItem implements IDanmakuItem {
- /** X axis base speed*/
- private static int sBaseSpeed = 3;
- private Context mContext;
- /**DanmakuView width, height*/
- private int mContainerWidth, mContainerHeight;
- private int mTextSize;
- private int mTextColor = Color.WHITE;
- private SpannableString mContent;
- private int mCurrX, mCurrY;
- /** X axis speed factor*/
- private float mFactor;
- private StaticLayout staticLayout;
- private StaticLayout borderStaticLayout;
- private static TextPaint strokePaint = new TextPaint();
- private int mContentWidth , mContentHeight;
- static {
- strokePaint.setARGB(255, 0, 0, 0);
- // strokePaint.setTextAlign(Paint.Align.CENTER);
- // strokePaint.setTextSize(16);
- // strokePaint.setTypeface(Typeface.DEFAULT_BOLD);
- strokePaint.setStyle(Paint.Style.STROKE);
- strokePaint.setStrokeWidth(4);
- strokePaint.setAntiAlias(true);
- }
- /**
- * construct a DanmakuItem
- * @param context Context
- * @param content paint text as content
- * @param startX start position of X axis,
- * normally should be the screen width, e.g. right side of the view).
- * the Y axis position will be assigned a channel by the DanmakuView randomly.
- */
- public DanmakuItem(Context context, CharSequence content, int startX) {
- this(context, new SpannableString(content), startX, 0, 0, 0, 1f);
- }
- public DanmakuItem(Context context, CharSequence content, int startX, int startY) {
- this(context, new SpannableString(content), startX, startY, 0, 0, 1f);
- }
- public DanmakuItem(Context context, SpannableString content, int startX, int startY,
- int textColorResId, int textSizeInDip, float speedFactor) {
- this.mContext = context;
- this.mContent = content;
- this.mCurrX = startX;
- this.mCurrY = startY;
- setTextColor(textColorResId);
- setTextSize(textSizeInDip);
- mFactor = speedFactor;
- measure();
- }
- private void measure() {
- TextPaint tp = new TextPaint();
- tp.setAntiAlias(true);
- tp.setColor(mTextColor);
- tp.setTextSize(mTextSize);
- strokePaint.setTextSize(mTextSize);
- // tp.setShadowLayer(4, 0, 0, Color.BLACK);
- mContentHeight = getFontHeight(tp);
- staticLayout = new StaticLayout(mContent,
- tp,
- (int) Layout.getDesiredWidth(mContent, 0, mContent.length(), tp) + 1,
- Layout.Alignment.ALIGN_NORMAL,
- 1.0f,
- 0.0f,
- false);
- mContentWidth = staticLayout.getWidth();
- borderStaticLayout = new StaticLayout(mContent,
- strokePaint,
- (int) Layout.getDesiredWidth(mContent, 0, mContent.length(), tp) + 1,
- Layout.Alignment.ALIGN_NORMAL,
- 1.0f,
- 0.0f,
- false);
- }
- @Override
- public void doDraw(Canvas canvas) {
- int canvasWidth = canvas.getWidth();
- int canvasHeight = canvas.getHeight();
- if (canvasWidth != this.mContainerWidth || canvasHeight != this.mContainerHeight) {//phone rotated !
- this.mContainerWidth = canvasWidth;
- this.mContainerHeight = canvasHeight;
- }
- canvas.save();
- canvas.translate(mCurrX,mCurrY);
- // for (int i = 0; i < 4; i++) { //加深阴影,产生描边效果. stroke/outline effect
- // staticLayout.draw(canvas);
- // }
- borderStaticLayout.draw(canvas);
- staticLayout.draw(canvas);
- canvas.restore();
- mCurrX = (int) (mCurrX - sBaseSpeed * mFactor);//only support moving along X axis
- }
- @Override
- public void setTextSize(int textSizeInDip) {
- if (textSizeInDip > 0) {
- this.mTextSize = dip2px(mContext, textSizeInDip);
- measure();
- } else {
- this.mTextSize = dip2px(mContext, 14); // textSize default to 12 dp
- }
- }
- @Override
- public void setTextColor(int textColorResId) {
- int[] colorss= {Color.RED, Color.BLACK, Color.BLUE, Color.WHITE, Color.YELLOW, Color.CYAN, Color.DKGRAY, Color.GRAY, Color.GREEN, Color.LTGRAY, Color.MAGENTA};
- Random random = new Random();
- int p = random.nextInt(colorss.length);
- if (textColorResId > 0) {
- this.mTextColor = colorss[p];
- measure();
- }
- }
- @Override
- public void setStartPosition(int x, int y) {
- this.mCurrX = x;
- this.mCurrY = y;
- }
- @Override
- public void setSpeedFactor(float factor) {
- this.mFactor = factor;
- }
- @Override
- public float getSpeedFactor() {
- return mFactor;
- }
- @Override
- public boolean isOut() {
- return mCurrX < 0 && Math.abs(mCurrX) > mContentWidth;
- }
- @Override
- public void release() {
- mContext = null;
- }
- @Override
- public int getWidth() {
- return mContentWidth;
- }
- @Override
- public int getHeight() {
- return mContentHeight;
- }
- @Override
- public int getCurrX() {
- return mCurrX;
- }
- @Override
- public int getCurrY() {
- return mCurrY;
- }
- /***
- * test whether this Danmaku Item would be hit the already running one or not
- * if it to be run on the same channel
- * @param runningItem item is already moving on the channel
- * @return hit or not
- */
- public boolean willHit(IDanmakuItem runningItem) {
- if (runningItem.getWidth() + runningItem.getCurrX() > mContainerWidth) {
- return true;
- }
- if (runningItem.getSpeedFactor()>= mFactor) {
- return false;
- }
- float len1 = runningItem.getCurrX() + runningItem.getWidth();
- float t1 = len1 / (runningItem.getSpeedFactor() * DanmakuItem.sBaseSpeed);
- float len2 = t1 * mFactor * DanmakuItem.sBaseSpeed;
- if (len2 > len1) {
- return true;
- } else {
- return false;
- }
- }
- public static int getBaseSpeed() {
- return sBaseSpeed;
- }
- public static void setBaseSpeed(int baseSpeed) {
- DanmakuItem.sBaseSpeed = baseSpeed;
- }
- private static int dip2px(Context context, float dipValue) {
- final float scale = context.getResources().getDisplayMetrics().density;
- return (int) (dipValue * scale + 0.5f);
- }
- private static int getFontHeight(TextPaint paint){
- Paint.FontMetrics fm = paint.getFontMetrics();
- return (int) Math.ceil(fm.descent - fm.top) + 2;
- }
- }
package com.xbw.danmu.danmu.opendanmaku; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.text.Layout; import android.text.SpannableString; import android.text.StaticLayout; import android.text.TextPaint; import java.util.Random; public class DanmakuItem implements IDanmakuItem { /** X axis base speed*/ private static int sBaseSpeed = 3; private Context mContext; /**DanmakuView width, height*/ private int mContainerWidth, mContainerHeight; private int mTextSize; private int mTextColor = Color.WHITE; private SpannableString mContent; private int mCurrX, mCurrY; /** X axis speed factor*/ private float mFactor; private StaticLayout staticLayout; private StaticLayout borderStaticLayout; private static TextPaint strokePaint = new TextPaint(); private int mContentWidth , mContentHeight; static { strokePaint.setARGB(255, 0, 0, 0); // strokePaint.setTextAlign(Paint.Align.CENTER); // strokePaint.setTextSize(16); // strokePaint.setTypeface(Typeface.DEFAULT_BOLD); strokePaint.setStyle(Paint.Style.STROKE); strokePaint.setStrokeWidth(4); strokePaint.setAntiAlias(true); } /** * construct a DanmakuItem * @param context Context * @param content paint text as content * @param startX start position of X axis, * normally should be the screen width, e.g. right side of the view). * the Y axis position will be assigned a channel by the DanmakuView randomly. */ public DanmakuItem(Context context, CharSequence content, int startX) { this(context, new SpannableString(content), startX, 0, 0, 0, 1f); } public DanmakuItem(Context context, CharSequence content, int startX, int startY) { this(context, new SpannableString(content), startX, startY, 0, 0, 1f); } public DanmakuItem(Context context, SpannableString content, int startX, int startY, int textColorResId, int textSizeInDip, float speedFactor) { this.mContext = context; this.mContent = content; this.mCurrX = startX; this.mCurrY = startY; setTextColor(textColorResId); setTextSize(textSizeInDip); mFactor = speedFactor; measure(); } private void measure() { TextPaint tp = new TextPaint(); tp.setAntiAlias(true); tp.setColor(mTextColor); tp.setTextSize(mTextSize); strokePaint.setTextSize(mTextSize); // tp.setShadowLayer(4, 0, 0, Color.BLACK); mContentHeight = getFontHeight(tp); staticLayout = new StaticLayout(mContent, tp, (int) Layout.getDesiredWidth(mContent, 0, mContent.length(), tp) + 1, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); mContentWidth = staticLayout.getWidth(); borderStaticLayout = new StaticLayout(mContent, strokePaint, (int) Layout.getDesiredWidth(mContent, 0, mContent.length(), tp) + 1, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } @Override public void doDraw(Canvas canvas) { int canvasWidth = canvas.getWidth(); int canvasHeight = canvas.getHeight(); if (canvasWidth != this.mContainerWidth || canvasHeight != this.mContainerHeight) {//phone rotated ! this.mContainerWidth = canvasWidth; this.mContainerHeight = canvasHeight; } canvas.save(); canvas.translate(mCurrX,mCurrY); // for (int i = 0; i < 4; i++) { //加深阴影,产生描边效果. stroke/outline effect // staticLayout.draw(canvas); // } borderStaticLayout.draw(canvas); staticLayout.draw(canvas); canvas.restore(); mCurrX = (int) (mCurrX - sBaseSpeed * mFactor);//only support moving along X axis } @Override public void setTextSize(int textSizeInDip) { if (textSizeInDip > 0) { this.mTextSize = dip2px(mContext, textSizeInDip); measure(); } else { this.mTextSize = dip2px(mContext, 14); // textSize default to 12 dp } } @Override public void setTextColor(int textColorResId) { int[] colorss= {Color.RED, Color.BLACK, Color.BLUE, Color.WHITE, Color.YELLOW, Color.CYAN, Color.DKGRAY, Color.GRAY, Color.GREEN, Color.LTGRAY, Color.MAGENTA}; Random random = new Random(); int p = random.nextInt(colorss.length); if (textColorResId > 0) { this.mTextColor = colorss[p]; measure(); } } @Override public void setStartPosition(int x, int y) { this.mCurrX = x; this.mCurrY = y; } @Override public void setSpeedFactor(float factor) { this.mFactor = factor; } @Override public float getSpeedFactor() { return mFactor; } @Override public boolean isOut() { return mCurrX < 0 && Math.abs(mCurrX) > mContentWidth; } @Override public void release() { mContext = null; } @Override public int getWidth() { return mContentWidth; } @Override public int getHeight() { return mContentHeight; } @Override public int getCurrX() { return mCurrX; } @Override public int getCurrY() { return mCurrY; } /*** * test whether this Danmaku Item would be hit the already running one or not * if it to be run on the same channel * @param runningItem item is already moving on the channel * @return hit or not */ public boolean willHit(IDanmakuItem runningItem) { if (runningItem.getWidth() + runningItem.getCurrX() > mContainerWidth) { return true; } if (runningItem.getSpeedFactor()>= mFactor) { return false; } float len1 = runningItem.getCurrX() + runningItem.getWidth(); float t1 = len1 / (runningItem.getSpeedFactor() * DanmakuItem.sBaseSpeed); float len2 = t1 * mFactor * DanmakuItem.sBaseSpeed; if (len2 > len1) { return true; } else { return false; } } public static int getBaseSpeed() { return sBaseSpeed; } public static void setBaseSpeed(int baseSpeed) { DanmakuItem.sBaseSpeed = baseSpeed; } private static int dip2px(Context context, float dipValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dipValue * scale + 0.5f); } private static int getFontHeight(TextPaint paint){ Paint.FontMetrics fm = paint.getFontMetrics(); return (int) Math.ceil(fm.descent - fm.top) + 2; } }
DanmakuView.java
- package com.xbw.danmu.danmu.opendanmaku;
- import android.content.Context;
- import android.content.res.TypedArray;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.graphics.PorterDuff;
- import android.text.TextPaint;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.View;
- import com.xbw.danmu.danmu.R;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Random;
- /**
- * 弹幕View
- */
- public class DanmakuView extends View {
- public static final String TAG = "DanmakuView";
- private final Context mContext;
- private int mMaxRow = 6; //最多几条弹道
- private int mPickItemInterval = 300;//每隔多长时间取出一条弹幕来播放.
- private int mMaxRunningPerRow = 8; //每条弹道上最多同时有几个弹幕在屏幕上运行
- private float mStartYOffset = 0.01f; //第一个弹道在Y轴上的偏移占整个View的百分比
- private float mEndYOffset = 0.9f;//最后一个弹道在Y轴上的偏移占整个View的百分比
- private HashMap<Integer,ArrayList<IDanmakuItem>> mChannelMap;
- private final java.util.Deque<IDanmakuItem> mWaitingItems = new LinkedList<>();
- private int[] mChannelY; //每条弹道的Y坐标
- private static final float mPartition = 0.95f; //仅View顶部的部分可以播放弹幕百分比
- private static final int STATUS_RUNNING = 1;
- private static final int STATUS_PAUSE = 2;
- private static final int STATUS_STOP = 3;
- private volatile int status = STATUS_STOP;
- private static Random random = new Random();
- private boolean mShowDebug = false;
- private LinkedList<Long> times;
- private Paint fpsPaint;
- private long previousTime = 0;
- private LinkedList<Float> lines;
- public DanmakuView(Context context) {
- this(context, null);
- }
- public DanmakuView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public DanmakuView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContext = context;
- TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.DanmakuView, 0, 0);
- mMaxRow = a.getInteger(R.styleable.DanmakuView_max_row, 1);
- mPickItemInterval = a.getInteger(R.styleable.DanmakuView_pick_interval, 1000);
- mMaxRunningPerRow = a.getInteger(R.styleable.DanmakuView_max_running_per_row, 1);
- mShowDebug = a.getBoolean(R.styleable.DanmakuView_show_debug, false);
- mStartYOffset = a.getFloat(R.styleable.DanmakuView_start_Y_offset, 0.01f);
- mEndYOffset = a.getFloat(R.styleable.DanmakuView_end_Y_offset, 0.9f);
- a.recycle();
- checkYOffset(mStartYOffset, mEndYOffset);
- init();
- }
- private void checkYOffset(float start, float end) {
- if (start >= end ){
- throw new IllegalArgumentException("start_Y_offset must < end_Y_offset");
- }
- if (start < 0f || start >= 1f || end < 0f || end > 1f) {
- throw new IllegalArgumentException("start_Y_offset and end_Y_offset must between 0 and 1)");
- }
- }
- private void init() {
- setBackgroundColor(Color.TRANSPARENT);
- setDrawingCacheBackgroundColor(Color.TRANSPARENT);
- calculation();
- }
- private void calculation() {
- if (mShowDebug) {
- fpsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
- fpsPaint.setColor(Color.YELLOW);
- fpsPaint.setTextSize(20);
- times = new LinkedList<>();
- lines = new LinkedList<>();
- }
- initChannelMap();
- initChannelY();
- }
- private void initChannelMap(){
- mChannelMap = new HashMap<>(mMaxRow);
- for (int i = 0; i < mMaxRow; i++) {
- ArrayList<IDanmakuItem> runningRow= new ArrayList<IDanmakuItem>(mMaxRunningPerRow);
- mChannelMap.put(i, runningRow);
- }
- }
- private void initChannelY() {
- if (mChannelY == null){
- mChannelY = new int[mMaxRow];
- }
- float rowHeight = getHeight() * (mEndYOffset - mStartYOffset) / mMaxRow;
- float baseOffset = getHeight() * mStartYOffset;
- for (int i = 0; i < mMaxRow; i++) {
- mChannelY[i] = (int) (baseOffset + rowHeight * (i + 1) - rowHeight * 7/ 8);//每一行空间顶部留1/4,剩下3/4显示文字
- }
- if (mShowDebug) {
- lines.add(baseOffset);
- for (int i = 0; i < mMaxRow; i++) {
- lines.add(baseOffset + rowHeight * (i + 1));
- }
- }
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (status == STATUS_RUNNING) {
- try {
- canvas.drawColor(Color.TRANSPARENT);
- //先绘制正在播放的弹幕
- for (int i = 0; i < mChannelMap.size(); i++) {
- ArrayList<IDanmakuItem> list = mChannelMap.get(i);
- for (Iterator<IDanmakuItem> it = list.iterator(); it.hasNext(); ) {
- IDanmakuItem item = it.next();
- if (item.isOut()) {
- it.remove();
- } else {
- item.doDraw(canvas);
- }
- }
- }
- //检查是否需要加载播放下一个弹幕
- if (System.currentTimeMillis() - previousTime > mPickItemInterval) {
- previousTime = System.currentTimeMillis();
- // Log.d(TAG, "start pick new item..");
- IDanmakuItem di = mWaitingItems.pollFirst();
- if (di != null) {
- int indexY = findVacant(di);
- if (indexY >= 0) {
- // Log.d(TAG, "find vacant channel");
- di.setStartPosition(canvas.getWidth() - 2, mChannelY[indexY]);
- // Log.d(TAG, "draw new, text:" + di.getText());
- //Log.d(TAG, String.format("doDraw, position,x=%s,y=%s", c.getWidth() - 1, mChannelY[indexY]));
- di.doDraw(canvas);
- mChannelMap.get(indexY).add(di);//不要忘记加入正运行的维护的列表中
- } else {
- // Log.d(TAG, "Not find vacant channel, add it back");
- addItemToHead(di);//找不到可以播放的弹道,则把它放回列表中
- }
- } else {
- //no item 弹幕播放完毕,
- }
- }
- if (mShowDebug) {
- int fps = (int) fps();
- canvas.drawText("FPS:" + fps, 5f, 20f, fpsPaint);
- for (float yp : lines) {
- canvas.drawLine(0f, yp, getWidth(), yp, fpsPaint);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- invalidate();
- } else {//暂停或停止,隐藏弹幕内容
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
- }
- }
- /**随机寻找一个可以播放弹幕而不会发生碰撞的弹道,返回弹道的Y坐标在mChannelY上的index,如果没有找到则返回-1*/
- private int findVacant(IDanmakuItem item) {
- try {//fix NPT exception
- for (int i = 0; i < mMaxRow; i++) {
- ArrayList<IDanmakuItem> list = mChannelMap.get(i);
- if (list.size() == 0) {
- return i;
- }
- }
- int ind = random.nextInt(mMaxRow);
- for (int i = 0; i < mMaxRow; i++) {
- ArrayList<IDanmakuItem> list = mChannelMap.get((i + ind) % mMaxRow);
- if (list.size() > mMaxRunningPerRow) {//每个弹道最多mMaxRunning个弹幕
- continue;
- }
- IDanmakuItem di = list.get(list.size() - 1);
- if (!item.willHit(di)) {
- return (i + ind) % mMaxRow;
- }
- }
- } catch (Exception e) {
- Log.w(TAG, "findVacant,Exception:" + e.toString());
- // e.printStackTrace();
- }
- return -1;
- }
- private void clearPlayingItems() {
- if (mChannelMap != null) {
- synchronized (mChannelMap) {
- for (int i = 0; i < mChannelMap.size(); i++) {
- ArrayList<IDanmakuItem> list = mChannelMap.get(i);
- if (list != null) {
- list.clear();
- }
- }
- }
- }
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- initChannelY();//可能屏幕方向切换了,得重新计算坐标
- }
- public boolean isPaused() {
- return STATUS_PAUSE == status;
- }
- /**播放显示弹幕*/
- public void show() {
- status = STATUS_RUNNING;
- invalidate();
- }
- /**隐藏弹幕,暂停播放*/
- public void hide() {
- status = STATUS_PAUSE;
- invalidate();
- }
- /**清空正在播放和等待播放的弹幕*/
- public void clear() {
- status = STATUS_STOP;
- clearItems();
- invalidate();
- }
- // /**清空弹幕等待队列,暂停播放*/
- // public void pauseAndClear() {
- // if (mWaitingItems != null) {
- // synchronized (mWaitingItems) {
- // mWaitingItems.clear();
- // }
- // }
- // clearPlayingItems();
- // }
- private void clearItems() {
- clearRunning();
- clearWaiting();
- }
- private void clearRunning() {
- if (null != mChannelMap && !mChannelMap.isEmpty()) {
- mChannelMap.clear();
- }
- }
- private void clearWaiting(){
- if (null != mWaitingItems && !mWaitingItems.isEmpty()) {
- mWaitingItems.clear();
- }
- }
- public void setMaxRow(int maxRow) {
- this.mMaxRow = maxRow;
- calculation();
- clearRunning();
- }
- public void setPickItemInterval(int pickItemInterval) {
- this.mPickItemInterval = pickItemInterval;
- }
- public void setMaxRunningPerRow(int maxRunningPerRow) {
- this.mMaxRunningPerRow = maxRunningPerRow;
- }
- public void setStartYOffset(float startYOffset, float endYOffset) {
- checkYOffset(startYOffset, endYOffset);
- clearRunning();
- this.mStartYOffset = startYOffset;
- this.mEndYOffset = endYOffset;
- calculation();
- }
- public void addItem(IDanmakuItem item) {
- synchronized (mWaitingItems) {
- this.mWaitingItems.add(item);
- }
- }
- public void addItemToHead(IDanmakuItem item) {
- synchronized (mWaitingItems) {
- this.mWaitingItems.offerFirst(item);
- }
- }
- /**是否新建后台线程来执行添加任务*/
- public void addItem(final List<IDanmakuItem> list, boolean backgroundLoad) {
- if (backgroundLoad) {
- new Thread(){
- @Override
- public void run() {
- synchronized (mWaitingItems) {
- mWaitingItems.addAll(list);
- }
- postInvalidate();
- }
- }.start();
- } else {
- this.mWaitingItems.addAll(list);
- }
- }
- /** Calculates and returns frames per second */
- private double fps() {
- long lastTime = System.nanoTime();
- times.addLast(lastTime);
- double NANOS = 1000000000.0;
- double difference = (lastTime - times.getFirst()) / NANOS;
- int size = times.size();
- int MAX_SIZE = 100;
- if (size > MAX_SIZE) {
- times.removeFirst();
- }
- return difference > 0 ? times.size() / difference : 0.0;
- }
- }
package com.xbw.danmu.danmu.opendanmaku; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.text.TextPaint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.xbw.danmu.danmu.R; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Random; /** * 弹幕View */ public class DanmakuView extends View { public static final String TAG = "DanmakuView"; private final Context mContext; private int mMaxRow = 6; //最多几条弹道 private int mPickItemInterval = 300;//每隔多长时间取出一条弹幕来播放. private int mMaxRunningPerRow = 8; //每条弹道上最多同时有几个弹幕在屏幕上运行 private float mStartYOffset = 0.01f; //第一个弹道在Y轴上的偏移占整个View的百分比 private float mEndYOffset = 0.9f;//最后一个弹道在Y轴上的偏移占整个View的百分比 private HashMap<Integer,ArrayList<IDanmakuItem>> mChannelMap; private final java.util.Deque<IDanmakuItem> mWaitingItems = new LinkedList<>(); private int[] mChannelY; //每条弹道的Y坐标 private static final float mPartition = 0.95f; //仅View顶部的部分可以播放弹幕百分比 private static final int STATUS_RUNNING = 1; private static final int STATUS_PAUSE = 2; private static final int STATUS_STOP = 3; private volatile int status = STATUS_STOP; private static Random random = new Random(); private boolean mShowDebug = false; private LinkedList<Long> times; private Paint fpsPaint; private long previousTime = 0; private LinkedList<Float> lines; public DanmakuView(Context context) { this(context, null); } public DanmakuView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DanmakuView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.DanmakuView, 0, 0); mMaxRow = a.getInteger(R.styleable.DanmakuView_max_row, 1); mPickItemInterval = a.getInteger(R.styleable.DanmakuView_pick_interval, 1000); mMaxRunningPerRow = a.getInteger(R.styleable.DanmakuView_max_running_per_row, 1); mShowDebug = a.getBoolean(R.styleable.DanmakuView_show_debug, false); mStartYOffset = a.getFloat(R.styleable.DanmakuView_start_Y_offset, 0.01f); mEndYOffset = a.getFloat(R.styleable.DanmakuView_end_Y_offset, 0.9f); a.recycle(); checkYOffset(mStartYOffset, mEndYOffset); init(); } private void checkYOffset(float start, float end) { if (start >= end ){ throw new IllegalArgumentException("start_Y_offset must < end_Y_offset"); } if (start < 0f || start >= 1f || end < 0f || end > 1f) { throw new IllegalArgumentException("start_Y_offset and end_Y_offset must between 0 and 1)"); } } private void init() { setBackgroundColor(Color.TRANSPARENT); setDrawingCacheBackgroundColor(Color.TRANSPARENT); calculation(); } private void calculation() { if (mShowDebug) { fpsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); fpsPaint.setColor(Color.YELLOW); fpsPaint.setTextSize(20); times = new LinkedList<>(); lines = new LinkedList<>(); } initChannelMap(); initChannelY(); } private void initChannelMap(){ mChannelMap = new HashMap<>(mMaxRow); for (int i = 0; i < mMaxRow; i++) { ArrayList<IDanmakuItem> runningRow= new ArrayList<IDanmakuItem>(mMaxRunningPerRow); mChannelMap.put(i, runningRow); } } private void initChannelY() { if (mChannelY == null){ mChannelY = new int[mMaxRow]; } float rowHeight = getHeight() * (mEndYOffset - mStartYOffset) / mMaxRow; float baseOffset = getHeight() * mStartYOffset; for (int i = 0; i < mMaxRow; i++) { mChannelY[i] = (int) (baseOffset + rowHeight * (i + 1) - rowHeight * 7/ 8);//每一行空间顶部留1/4,剩下3/4显示文字 } if (mShowDebug) { lines.add(baseOffset); for (int i = 0; i < mMaxRow; i++) { lines.add(baseOffset + rowHeight * (i + 1)); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (status == STATUS_RUNNING) { try { canvas.drawColor(Color.TRANSPARENT); //先绘制正在播放的弹幕 for (int i = 0; i < mChannelMap.size(); i++) { ArrayList<IDanmakuItem> list = mChannelMap.get(i); for (Iterator<IDanmakuItem> it = list.iterator(); it.hasNext(); ) { IDanmakuItem item = it.next(); if (item.isOut()) { it.remove(); } else { item.doDraw(canvas); } } } //检查是否需要加载播放下一个弹幕 if (System.currentTimeMillis() - previousTime > mPickItemInterval) { previousTime = System.currentTimeMillis(); // Log.d(TAG, "start pick new item.."); IDanmakuItem di = mWaitingItems.pollFirst(); if (di != null) { int indexY = findVacant(di); if (indexY >= 0) { // Log.d(TAG, "find vacant channel"); di.setStartPosition(canvas.getWidth() - 2, mChannelY[indexY]); // Log.d(TAG, "draw new, text:" + di.getText()); //Log.d(TAG, String.format("doDraw, position,x=%s,y=%s", c.getWidth() - 1, mChannelY[indexY])); di.doDraw(canvas); mChannelMap.get(indexY).add(di);//不要忘记加入正运行的维护的列表中 } else { // Log.d(TAG, "Not find vacant channel, add it back"); addItemToHead(di);//找不到可以播放的弹道,则把它放回列表中 } } else { //no item 弹幕播放完毕, } } if (mShowDebug) { int fps = (int) fps(); canvas.drawText("FPS:" + fps, 5f, 20f, fpsPaint); for (float yp : lines) { canvas.drawLine(0f, yp, getWidth(), yp, fpsPaint); } } } catch (Exception e) { e.printStackTrace(); } invalidate(); } else {//暂停或停止,隐藏弹幕内容 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } } /**随机寻找一个可以播放弹幕而不会发生碰撞的弹道,返回弹道的Y坐标在mChannelY上的index,如果没有找到则返回-1*/ private int findVacant(IDanmakuItem item) { try {//fix NPT exception for (int i = 0; i < mMaxRow; i++) { ArrayList<IDanmakuItem> list = mChannelMap.get(i); if (list.size() == 0) { return i; } } int ind = random.nextInt(mMaxRow); for (int i = 0; i < mMaxRow; i++) { ArrayList<IDanmakuItem> list = mChannelMap.get((i + ind) % mMaxRow); if (list.size() > mMaxRunningPerRow) {//每个弹道最多mMaxRunning个弹幕 continue; } IDanmakuItem di = list.get(list.size() - 1); if (!item.willHit(di)) { return (i + ind) % mMaxRow; } } } catch (Exception e) { Log.w(TAG, "findVacant,Exception:" + e.toString()); // e.printStackTrace(); } return -1; } private void clearPlayingItems() { if (mChannelMap != null) { synchronized (mChannelMap) { for (int i = 0; i < mChannelMap.size(); i++) { ArrayList<IDanmakuItem> list = mChannelMap.get(i); if (list != null) { list.clear(); } } } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); initChannelY();//可能屏幕方向切换了,得重新计算坐标 } public boolean isPaused() { return STATUS_PAUSE == status; } /**播放显示弹幕*/ public void show() { status = STATUS_RUNNING; invalidate(); } /**隐藏弹幕,暂停播放*/ public void hide() { status = STATUS_PAUSE; invalidate(); } /**清空正在播放和等待播放的弹幕*/ public void clear() { status = STATUS_STOP; clearItems(); invalidate(); } // /**清空弹幕等待队列,暂停播放*/ // public void pauseAndClear() { // if (mWaitingItems != null) { // synchronized (mWaitingItems) { // mWaitingItems.clear(); // } // } // clearPlayingItems(); // } private void clearItems() { clearRunning(); clearWaiting(); } private void clearRunning() { if (null != mChannelMap && !mChannelMap.isEmpty()) { mChannelMap.clear(); } } private void clearWaiting(){ if (null != mWaitingItems && !mWaitingItems.isEmpty()) { mWaitingItems.clear(); } } public void setMaxRow(int maxRow) { this.mMaxRow = maxRow; calculation(); clearRunning(); } public void setPickItemInterval(int pickItemInterval) { this.mPickItemInterval = pickItemInterval; } public void setMaxRunningPerRow(int maxRunningPerRow) { this.mMaxRunningPerRow = maxRunningPerRow; } public void setStartYOffset(float startYOffset, float endYOffset) { checkYOffset(startYOffset, endYOffset); clearRunning(); this.mStartYOffset = startYOffset; this.mEndYOffset = endYOffset; calculation(); } public void addItem(IDanmakuItem item) { synchronized (mWaitingItems) { this.mWaitingItems.add(item); } } public void addItemToHead(IDanmakuItem item) { synchronized (mWaitingItems) { this.mWaitingItems.offerFirst(item); } } /**是否新建后台线程来执行添加任务*/ public void addItem(final List<IDanmakuItem> list, boolean backgroundLoad) { if (backgroundLoad) { new Thread(){ @Override public void run() { synchronized (mWaitingItems) { mWaitingItems.addAll(list); } postInvalidate(); } }.start(); } else { this.mWaitingItems.addAll(list); } } /** Calculates and returns frames per second */ private double fps() { long lastTime = System.nanoTime(); times.addLast(lastTime); double NANOS = 1000000000.0; double difference = (lastTime - times.getFirst()) / NANOS; int size = times.size(); int MAX_SIZE = 100; if (size > MAX_SIZE) { times.removeFirst(); } return difference > 0 ? times.size() / difference : 0.0; } }
IDanmakuItem.java
- package com.xbw.danmu.danmu.opendanmaku;
- import android.graphics.Canvas;
- public interface IDanmakuItem {
- void doDraw(Canvas canvas);
- void setTextSize(int sizeInDip);
- void setTextColor(int colorResId);
- void setStartPosition(int x, int y);
- void setSpeedFactor(float factor);
- float getSpeedFactor();
- boolean isOut();
- boolean willHit(IDanmakuItem runningItem);
- void release();
- int getWidth();
- int getHeight();
- int getCurrX();
- int getCurrY();
- }
package com.xbw.danmu.danmu.opendanmaku;
import android.graphics.Canvas;
public interface IDanmakuItem {
void doDraw(Canvas canvas);
void setTextSize(int sizeInDip);
void setTextColor(int colorResId);
void setStartPosition(int x, int y);
void setSpeedFactor(float factor);
float getSpeedFactor();
boolean isOut();
boolean willHit(IDanmakuItem runningItem);
void release();
int getWidth();
int getHeight();
int getCurrX();
int getCurrY();
}
弹幕发送
- private void danmushow(String a,Context context){
- //int[] color= {Color.RED,Color.BLACK,Color.BLUE,Color.WHITE,Color.YELLOW,Color.CYAN,Color.DKGRAY,Color.GRAY,Color.GREEN,Color.LTGRAY,Color.MAGENTA};
- //Random random = new Random();
- //int p = random.nextInt(color.length);
- IDanmakuItem item = new DanmakuItem(context, new SpannableString(a), mDanmakuView.getWidth(),0,R.color.white,20,1);//参数 上下文,弹幕内容,位置x,位置y,颜色,字体大小,速度。
- //IDanmakuItem item = new DanmakuItem(context, new SpannableString(a), mDanmakuView.getWidth());
- //item.setTextColor(context.getResources().getColor(Color.parseColor(Colors[p])));
- //item.setTextSize(14);
- //item.setTextColor(getRandomColor());
- mDanmakuView.addItemToHead(item);
- }
private void danmushow(String a,Context context){
//int[] color= {Color.RED,Color.BLACK,Color.BLUE,Color.WHITE,Color.YELLOW,Color.CYAN,Color.DKGRAY,Color.GRAY,Color.GREEN,Color.LTGRAY,Color.MAGENTA};
//Random random = new Random();
//int p = random.nextInt(color.length);
IDanmakuItem item = new DanmakuItem(context, new SpannableString(a), mDanmakuView.getWidth(),0,R.color.white,20,1);//参数 上下文,弹幕内容,位置x,位置y,颜色,字体大小,速度。
//IDanmakuItem item = new DanmakuItem(context, new SpannableString(a), mDanmakuView.getWidth());
//item.setTextColor(context.getResources().getColor(Color.parseColor(Colors[p])));
//item.setTextSize(14);
//item.setTextColor(getRandomColor());
mDanmakuView.addItemToHead(item);
}
我把弹幕发送放在了消息透传里,这样就可以接受服务发送的实时消息了,把接收到的消息以弹幕的形式发送出来。
这只是弹幕部分,下边说消息透传。