1. 程式人生 > >【Android效果集】彈幕效果

【Android效果集】彈幕效果

之前在網上有看到過iOS的彈幕效果實現,搜了一下發現Android實現彈幕效果的帖子比較少,而且寫得都不是很好理解,於是嘗試自己做了一下,寫成這篇部落格,分享出來。

最終效果展示:
這裡寫圖片描述

實現思路:

1.自定義一個彈幕View,繼承自TextView,專門用來顯示一條彈幕
2.彈幕View能夠自動從最右邊勻速滾動到最左邊
3.彈幕的顏色和大小設定為隨機值
4.彈幕View的高度隨機,區域在螢幕範圍內
5.在Activity中迴圈定時加入自定義彈幕View,形成最後的彈幕
6.自定義文字資源,隨機從檔案資源中讀取文字顯示

詳細過程:

1.改變應用屬性為橫屏,無標題欄,黑色背景

AndroidManifest.xml檔案中,讓MainActivity方向屬性為landscape,並且加上主題設定

<activity android:name=".MainActivity"
            android:screenOrientation="landscape"
            android:theme="@style/AppTheme">

然後在styles.xml檔案中,設定如下(parent是系統自動生成的,我用的版本比較新,可能大家的不一樣,這個不要緊)

    <style name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:windowBackground">@color/black</item> <item name="android:windowFullscreen">true</item> <item name="android:windowNoTitle">true</item> </style>

2.新建BarrageView

首先新建一個彈幕View,我取名為BarrageView

,它繼承自TextView
需要為它實現兩個構造方法和onDraw()方法。
在onDraw方法裡我們繪製文字。

public class BarrageView extends TextView {
    private Paint paint = new Paint(); //畫布引數

    public BarrageView(Context context) {
        super(context);
        init();
    }

    public BarrageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    /**
     * 初始化
     */
    protected void init() {}

    @Override
    protected void onDraw(Canvas canvas) {
        paint.setTextSize(30);
        paint.setColor(0xffffffff); //白色
        canvas.drawText(getText(), 0, 30, paint);
    }
}

PS:這裡需要注意一點,就是y值我們沒有設定為0而是30,是因為文字的座標是從左下角開始算的,文字大小設為了30,y也要設為30文字才會剛剛好顯示在螢幕的(0, 0)處。

我們把自定義View加到主佈局中,因為需要從螢幕左側滾動到右側,我把寬高設定為螢幕寬高

    <com.azz.azbarrage.BarrageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="hello"
        />

3.滾動動畫

移動可以用Animation動畫,但是我這裡用的是執行緒重繪,只要能實現最終效果,都是可以的。

在onDraw裡面新建一個執行緒,該執行緒會一直執行,每次執行主函式時會對BarrageViewx值產生影響(減少)。

    private int posX; //x座標

    class RollThread extends Thread {
        @Override
        public void run() {
            while(true) {
                //1.動畫邏輯
                animLogic();
                //2.繪製圖像
                postInvalidate();
                //3.延遲,不然會造成執行太快動畫一閃而過
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 動畫邏輯處理
     */
    private void animLogic() {
        posX -= 8;
    }

可以看到執行緒裡面就三步,1.把x座標減少8畫素,2.呼叫postInvalidate()方法重繪,該方法會自動呼叫onDraw()方法,3.停頓30毫秒,程式碼執行非常快,如果不停頓的話,我們甚至看不到滾動動畫。

接下來我們把滾動執行緒加到上面的程式碼裡去。

在加之前,需要思考一下,我們的posX第一次繪製時應該為螢幕寬,表示從螢幕最右邊開始移動,然後呼叫滾動執行緒,直到滾出螢幕。

這裡要提一下getWidth()方法,這個方法如果在建構函式裡面呼叫,得到的是0,在onDraw()方法裡面呼叫得到的是本view的寬度,這是因為自定義View的機制,要在呼叫過onMeasure()後才能得到自身的寬高。

我這裡用getWindowVisibleDisplayFrame(rect);能在初始化時就得到螢幕寬高。

    private int posX; //x座標

    private int windowWidth; //螢幕寬
    private int windowHeight; //螢幕高

    private RollThread rollThread; //滾動執行緒

    /**
     * 初始化
     */
    protected void init() {
        //得到螢幕寬高
        Rect rect = new Rect();
        getWindowVisibleDisplayFrame(rect);
        windowWidth = rect.width();
        windowHeight = rect.height();

        //設定x為螢幕寬
        posX = windowWidth;
    }

    protected void onDraw(Canvas canvas) {
        paint.setTextSize(30);
        paint.setColor(0xffffffff); //白色
        canvas.drawText(getText(), posX, 30, paint);

        if (rollThread == null) {
            rollThread = new RollThread();
            rollThread.start();
        }
    }

現在的效果是這個樣子:
這裡寫圖片描述

當彈幕從左邊完全滾出時,其實執行緒還是在執行的,這樣積累多了執行緒對系統負荷加大,我們需要加上判斷,如果滾出了螢幕,執行緒也跳出while(true)迴圈,然後由系統自己回收。

    class RollThread extends Thread {
        @Override
        public void run() {
            while(true) {

                ...

                //關閉執行緒邏輯判斷
                if (needStopRollThread()) {
                    Log.i("azzz", getText() + "   -執行緒停止!");
                    break;
                }
            }
        }
    }

    private boolean needStopRollThread() {
        if (posX <= -paint.measureText(getText())) {
            return true;
        }
        return false;
    }

paintmeasureText(String text)方法來得到文字的寬度,進行判斷是否需要退出執行緒迴圈。

4.隨機大小和顏色

在初始化的時候,我們就給定一條彈幕一個隨機的大小和顏色,使得每一條彈幕看起來都不一樣。

    private int textSize = 30; //字型大小
    public static final int TEXT_MIN = 10;
    public static final int TEXT_MAX = 60;
    //字型顏色
    private int color = 0xffffffff;

   /**
     * 初始化
     */
    protected void init() {
        //1.設定文字大小
        textSize = TEXT_MIN + random.nextInt(TEXT_MAX - TEXT_MIN);
        paint.setTextSize(textSize);

        //2.設定文字顏色
        color = Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256));
        paint.setColor(color);

        ...
    }

    protected void onDraw(Canvas canvas) {
        //paint.setTextSize(30);
        //paint.setColor(0xffffffff); //白色
        canvas.drawText(getText(), posX, 30, paint);

        ...
    }

這裡得到隨機數用到的是randomnextInt(n)方法,其中值域是[0,n)

把通過隨機數生成的字型大小和顏色加到paint裡,在onDraw()方法裡面就能起效果了。別忘了把之前在onDraw()裡設定的顏色和字型大小去掉。

5.隨機高度

高度也是在init()方法通過隨機數生成。

    /**
     * 初始化
     */
    protected void init() {
        ...

        //3.得到螢幕寬高
        Rect rect = new Rect();
        getWindowVisibleDisplayFrame(rect);
        windowWidth = rect.width();
        windowHeight = rect.height();

        //4.設定x為螢幕寬
        posX = windowWidth;

        //5.設定y為螢幕高度內內隨機,需要注意的是,文字是以左下角為起始點計算座標的,所以要加上TextSize的大小
        posY = textSize + random.nextInt(windowHeight - textSize);
    }

之前說過文字的座標系是在左下角,當y為0時文字是看不到的,所以這裡高度的初始化要寫在文字大小的後面,在隨機數前加上一個字型大小的高度,同時最大值也應該減去一個字型大小的高度,不然最大ytextSize + windowHeight就超出了螢幕顯示。

6.動態生成彈幕

單條彈幕屬性差不多定義完了,現在我們去MainActivity中動態加入多條彈幕。

    //兩兩彈幕之間的間隔時間
    public static final int DELAY_TIME = 800;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        //設定寬高全屏
        final ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

        final Handler handler = new Handler();
        Runnable createBarrageView = new Runnable() {
            @Override
            public void run() {
                //新建一條彈幕,並設定文字
                final BarrageView barrageView = new BarrageView(MainActivity.this);
                barrageView.setText("你好");
                addContentView(barrageView, lp);

                //傳送下一條訊息
                handler.postDelayed(this, DELAY_TIME);
            }
        };
        handler.post(createBarrageView);
    }

Activity中有個addContentView(view, lp)方法能夠新加view到根檢視下。

lp動態設定為全屏的方法是new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

handler.postDelayed(this, DELAY_TIME);的意思是彈幕出來的時間不一致,這樣就不會像兵列一樣整齊地出來了。

7.自定義文字資源

我們可以在string.xml中自定義字串陣列<string-array>,然後再Activity中隨機引用。

    private Random random = new Random();

    protected void onCreate(Bundle savedInstanceState) {
        ...

       //讀取文字資源
        final String[] texts = getResources().getStringArray(R.array.default_text_array);

        Runnable createBarrageView = new Runnable() {
            @Override
            public void run() {
                //新建一條彈幕,並設定文字
                final BarrageView barrageView = new BarrageView(MainActivity.this);
                barrageView.setText(texts[random.nextInt(texts.length)]); //隨機設定文字
                addContentView(barrageView, lp);

                ..
            }
        };
        ...
    }

好了!再來一張效果:
這裡寫圖片描述

關於資源回收

在iOS中的UIView中有個方法叫removeFromSuperView,呼叫該方法就能達到資源回收的作用,在Android中沒有這種方法,只能從父控制元件呼叫remove方法,我查了很久資料,也沒查到是否這樣做可以達到銷燬View的作用,不過為此我還是在BarrageView中留了一個監聽器,用來做銷燬動作的。

    /**
     * 滾動結束接聽器
     */
    interface OnRollEndListener {
        void onRollEnd();
    }

    private OnRollEndListener mOnRollEndListener;

    /**
     * @param onRollEndListener 設定滾動結束監聽器
     */
    public void setOnRollEndListener(OnRollEndListener onRollEndListener) {
        this.mOnRollEndListener = onRollEndListener;
    }

     class RollThread extends Thread {
        @Override
        public void run() {
            while(true) {
                 ...

                //關閉執行緒邏輯判斷
                if (needStopRollThread()) {
                    Log.i("azzz", getText() + "   -執行緒停止!");
                    if (mOnRollEndListener != null) {
                        mOnRollEndListener.onRollEnd();
                    }
                    break;
                }
            }
        }
    }

後記:現在是2015年10月19日01:14:59,一興奮就忍不住把部落格寫完了,因為也擔心一拖再拖就成坑了,明天星期一還要上班,原始碼什麼的就明天再來弄了!~這篇部落格寫得還算有誠意,希望能有好評。

2015.10.19 更新:

關於以上最後一點「關於資源回收」,經過博樂的指導,知道可以在子控制元件中通過getParent()方法得到父控制元件,這樣就可以直接在子控制元件中把自己從父控制元件中移除,達到回收資源的效果。

    class RollThread extends Thread {
        @Override
        public void run() {
            while(true) {
                 ...

                 //關閉執行緒邏輯判斷
                 if (needStopRollThread()) {
                    ...

                    post(new Runnable() { //從父類中移除本view
                        @Override
                        public void run() {
                            ((ViewGroup) BarrageView.this.getParent()).removeView(BarrageView.this);
                        }
                    });
                    break;
                } //if-end
            } //while-end
        } //run-end
    } //RollThread-end

以上注意兩點,

第一,一定要在主執行緒呼叫移除方法,因為只有主執行緒可以更改UI。View方法本身自帶post(runnable)方法能夠在主執行緒中執行。(關於快速切換到主執行緒的方法可看《【Android和iOS】快速切換到主執行緒更新UI》

第二,getParent()得到的返回型別是View,而View方法並沒有remove(view)方法,所以只要強制轉換成ViewGroup就可以了。

2015.11.02 更新:

感謝18樓網友提出的問題,今天把這個問題解決了一下
這裡寫圖片描述

問題非常好,這個問題也很容易復現,開啟AZBarrage應用,按Home鍵回到桌面,等待一分鐘(時間越長問題越明顯),然後通過最近訪問再次開啟應用,會看到右側有很多堆積的彈幕。

這裡寫圖片描述 (演示圖片做了暫停處理,實際中在Home介面等待了很久。)

在上示效果圖的下方可以看到列印“傳送彈幕”,這個列印寫在了MainAcitivity中的createBarrageView這個Runnable

protected void onCreate(Bundle savedInstanceState) {
    ...
    Runnable createBarrageView = new Runnable() {
            @Override
            public void run() {
                Log.e("azzz", "傳送彈幕");
                ...
            }
    };
    ...
}

其實問題的原因就在這,當按下Home鍵返回桌面時,應用的執行緒createBarrageView並不會停止,而是在後臺繼續執行,那麼就導致不停地建立彈幕,再開啟時就有上面的情況了。

基於此修改思路也很簡單,我們加一個pauseFlag,在onPause()的時候置為trueonRusume()的時候置為false,然後在createBarrageView執行緒中先進行判斷是否是pause狀態,再選擇是否要傳送彈幕。

public class MainActivity extends Activity {
    ...
    private boolean isOnPause = false;
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Runnable createBarrageView = new Runnable() {
            @Override
            public void run() {
                //加上判斷
                if (!isOnPause) {
                    Log.e("azzz", "傳送彈幕");
                    ...
                }
                //傳送下一條訊息
                handler.postDelayed(this, DELAY_TIME);
            }
        };
        handler.post(createBarrageView);
    }
    @Override
    protected void onPause() {
        super.onPause();
        isOnPause = true;
    }

    @Override
    protected void onResume() {
        super.onResume();
        isOnPause = false;
    }
}

現在的效果:

這裡寫圖片描述

發現光是這樣改後,還是有問題。本來正常播放的彈幕,在返回桌面後,還是繼續滾動(可以注意下面的列印,回到桌面後還是有執行緒停止的列印提示),導致再次開啟應用時,全部都重新開始。

這時候我們需要的是彈幕有個暫停功能就好了!當返回主頁時,彈幕滾到哪個位置就停到哪個位置,當我再次開啟時,又繼續滾動。

需求想清楚了,接下來想實現。彈幕的滾動是由一個執行緒決定的,在BarrageView中自定義了一個RollThread,我們可以給它加兩個方法,一個是暫停(掛起),一個是繼續(恢復)。(關於執行緒的掛起和恢復,不會的可以看《Android : 執行緒的結束,掛起和恢復(下)》

public class BarrageView extends TextView {
    ...
    private RollThread rollThread; //滾動執行緒

    class RollThread extends Thread {
        private Object mPauseLock; //執行緒鎖
        private boolean mPauseFlag; //標籤:是否暫停

        RollThread() {
            mPauseLock = new Object();
            mPauseFlag = false;
        }
        @Override
        public void run() {
            while (true) {
                //首先檢查是否掛起
                checkPause();
                ...
            }
        }
        public void onPause() {
            synchronized (mPauseLock) {
                mPauseFlag = true;
            }
        }
        public void onResume() {
            synchronized (mPauseLock) {
                mPauseFlag = false;
                Log.i(TAG, "執行緒恢復-" + getText());
                mPauseLock.notify();
            }
        }
        private void checkPause() {
            synchronized (mPauseLock) {
                if (mPauseFlag) {
                    try {
                        Log.e(TAG, "執行緒掛起-" + getText());
                        mPauseLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

那我們怎麼知道什麼時候暫停滾動(掛起執行緒)呢?

經我調查發現,View中有個方法叫onWindowVisibilityChanged(int visibility),當view顯示在視窗的時候,回撥的visibility等於View.VISIBLE,當view不顯示在視窗時,回撥的visibility等於View.GONE。基於此,我們可以在這裡進行判斷什麼時候暫停,什麼時候恢復。

public class BarrageView extends TextView {
    ...
    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        if (rollThread == null) {
            return;
        }
        if (View.GONE == visibility) {
            rollThread.onPause();
        } else {
            rollThread.onResume();
        }
    }
}

最終效果:(可以注意下列印,Home退出時執行緒掛起(且絕對不會有執行緒停止的列印),返回應用時執行緒恢復)

這裡寫圖片描述

如果你有任何問題,歡迎留言告訴我!~