1. 程式人生 > >如何寫一個播放器-解析MNVideoPlayer(二)

如何寫一個播放器-解析MNVideoPlayer(二)

注:本文適合初學Android或未接觸過系統自帶的MediaPlayer人群,閱讀之前請下載相關程式碼

MNVideoPlayer程式碼:http://blog.csdn.net/wenqiang0718/article/details/78615715

由於此專案程式碼結構非常清晰,所以我們這次採用一個與眾不同的方式進行解讀,從下開始,之後從上開始,最終核心視訊播放及銷燬的方式進行程式碼解析。其實我們很多時候也需要這樣,因為如果接手了別人的程式碼之後,並不會所有的程式碼都是從開始- ->結束,這樣的順序來讓我們完整的吸收,而是遇到問題-->找到問題所在-->分析問題原因-->找到解決方式-->分析解決影響-->解決問題,總是這樣一個順序去解決已有工程的bug,包括我們自己寫出來的bug也一樣,短時間內還好,時間長了可能也只記得一個大概了。好了,廢話不多說,下面我們進行demo程式碼解析:

最下面是事件監聽的定義,很明顯我們事件的監聽使用的觀察者模式:

//網路監聽回撥
    private OnNetChangeListener onNetChangeListener;

    public void setOnNetChangeListener(OnNetChangeListener onNetChangeListener) {
        this.onNetChangeListener = onNetChangeListener;
    }

    public interface OnNetChangeListener {
        //wifi
        void onWifi(MediaPlayer mediaPlayer);

        //手機
        void onMobile(MediaPlayer mediaPlayer);

        //不可用
        void onNoAvailable(MediaPlayer mediaPlayer);
    }

    //SurfaceView初始化完成回撥
    private OnPlayerCreatedListener onPlayerCreatedListener;

    public void setOnPlayerCreatedListener(OnPlayerCreatedListener onPlayerCreatedListener) {
        this.onPlayerCreatedListener = onPlayerCreatedListener;
    }

    public interface OnPlayerCreatedListener {
        //不可用
        void onPlayerCreated(String url, String title);
    }

    //-----------------------播放完回撥
    private OnCompletionListener onCompletionListener;

    public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
        this.onCompletionListener = onCompletionListener;
    }

    public interface OnCompletionListener {
        void onCompletion(MediaPlayer mediaPlayer);
    }

這些事件監聽很簡單,一個介面定義,一個set方法,在使用的時候判定常量是否為null,如果不為null則觸發相關方法,幾乎所有的觀察者模式都是這樣,非常簡單,這也是觀察者的魅力所在。

那麼有設定的地方,最好就有銷燬的地方,定義一個銷燬所有監聽的方法(這個是很有必要的):

private void removeAllListener() {
        if (onNetChangeListener != null) {
            onNetChangeListener = null;
        }
        if (onPlayerCreatedListener != null) {
            onPlayerCreatedListener = null;
        }
    }

因為demo中就設定了這兩個方法,所以作者也就在remove中寫了這兩個方法,我們自己可以適量的增加。如果使用觀察者的地方比較多,那麼我建議使用集合來儲存所有的監聽,例如:
List<OnNetChangeListener> onNetChangeListenerList = new ArrayList<>();
    
    public void registerNetChangeListener(OnNetChangeListener onNetChangeListener){
        synchronized (onNetChangeListenerList){
            if(!onNetChangeListenerList.contains(onNetChangeListener)){
                onNetChangeListenerList.add(onNetChangeListener);
            }
        }
    }
    
    public void unRegisterNetChangeListener(OnNetChangeListener onNetChangeListener){
        synchronized (onNetChangeListenerList){
            if(onNetChangeListenerList.contains(onNetChangeListener)){
                onNetChangeListenerList.remove(onNetChangeListener);
            }
        }
    }
    
    public void clearNetChangeListener(){
        onNetChangeListenerList.clear();
    }

之所以使用Synchronized關鍵字,是防止同時呼叫

接下來是網路變化監聽,採用的動態廣播的方式,優點是靈活,但是不要忘記登出就可以,而且我們可以在網路監聽的時候DIY自己的功能,例如重新恢復網路時自動播放,網路切換為4G時彈窗提醒,網路斷開後如果還有快取則不會立即停止視訊播放等,根據需求靈活編寫即可:

//-------------------------網路變化監聽
    public class NetChangeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (onNetChangeListener == null || !isNeedNetChangeListen) {
                return;
            }
            ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
            if (netInfo != null && netInfo.isAvailable()) {
                if (netInfo.getType() == ConnectivityManager.TYPE_WIFI) { //WiFi網路
                    onNetChangeListener.onWifi(mediaPlayer);
                } else if (netInfo.getType() == ConnectivityManager.TYPE_MOBILE) {   //3g網路
                    onNetChangeListener.onMobile(mediaPlayer);
                } else {    //其他
                    Log.i(TAG, "其他網路");
                }
            } else {
                onNetChangeListener.onNoAvailable(mediaPlayer);
            }
        }
    }

    private NetChangeReceiver netChangeReceiver;

    private void registerNetReceiver() {
        if (netChangeReceiver == null) {
            IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
            netChangeReceiver = new NetChangeReceiver();
            context.registerReceiver(netChangeReceiver, filter);
        }
    }

    private void unregisterNetReceiver() {
        if (netChangeReceiver != null) {
            context.unregisterReceiver(netChangeReceiver);
        }
    }

之後是電量監聽,同樣是動態廣播監聽:
/**
     * 電量廣播接受者
     */
    class BatteryReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //判斷它是否是為電量變化的Broadcast Action
            if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                //獲取當前電量
                int level = intent.getIntExtra("level", 0);
                //電量的總刻度
                int scale = intent.getIntExtra("scale", 100);

                int battery = (level * 100) / scale;

                //把它轉成百分比
                Log.i(TAG, "電池電量為" + battery + "%");

                mn_iv_battery.setVisibility(View.VISIBLE);
                if (battery > 0 && battery < 20) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_01);
                } else if (battery >= 20 && battery < 40) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_02);
                } else if (battery >= 40 && battery < 65) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_03);
                } else if (battery >= 65 && battery < 90) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_04);
                } else if (battery >= 90 && battery <= 100) {
                    mn_iv_battery.setImageResource(R.drawable.mn_player_battery_05);
                } else {
                    mn_iv_battery.setVisibility(View.GONE);
                }


            }
        }
    }

    private BatteryReceiver batteryReceiver;

    private void registerBatteryReceiver() {
        if (batteryReceiver == null) {
            //註冊廣播接受者
            IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            //建立廣播接受者物件
            batteryReceiver = new BatteryReceiver();
            //註冊receiver
            context.registerReceiver(batteryReceiver, intentFilter);
        }
    }

    private void unRegisterBatteryReceiver() {
        if (batteryReceiver != null) {
            context.unregisterReceiver(batteryReceiver);
        }
    }


非常簡單,一目瞭然,這也是我選擇從下而上為大家解析的原因,原作者的程式碼結構非常清晰,從方法定義入手反而更容易讓我們吸收,而且可以培養我們寫作程式碼的好習慣。寫程式碼跟寫文章一樣,當你還不能自己寫出或華麗、或深邃、或流暢的程式碼時,參考優雅的程式碼也是非常關鍵的開始。

下面我們再從開始看看作者為視訊播放準備了哪些事情:

1、獲取視訊是否自動播放(我在專案過程中,大半時間花費在了自動播放這裡,相容性真是讓人腦袋疼了又疼,大家以後寫自動播放一定要定義好架構,否則就會跟我一樣架構修改好幾次,因為產品改需求了,這個是真無解除非你揍他)

private void initAttrs(Context context, AttributeSet attrs) {
        //獲取自定義屬性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MNViderPlayer);
        //遍歷拿到自定義屬性
        for (int i = 0; i < typedArray.getIndexCount(); i++) {
            int index = typedArray.getIndex(i);
            if (index == R.styleable.MNViderPlayer_mnFirstNeedPlay) {
                isFirstPlay = typedArray.getBoolean(R.styleable.MNViderPlayer_mnFirstNeedPlay, false);
            }
        }
        //銷燬
        typedArray.recycle();
    }
2、轉屏的時候重新計算檢視大小(在實際過程中,這種方式只試用於非列表中,如果是列表,我使用的方式是定義一個全屏layout,在轉屏後將SurfaceView放到全屏layout中,轉屏回來後再設定回列表的parentview中,我會在講解MNVideoPlayer後,將自己寫的一個VideoPlayer再分享給大家):
@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        int screenWidth = PlayerUtils.getScreenWidth(activity);
        int screenHeight = PlayerUtils.getScreenHeight(activity);
        ViewGroup.LayoutParams layoutParams = getLayoutParams();

        //newConfig.orientation獲得當前螢幕狀態是橫向或者豎向
        //Configuration.ORIENTATION_PORTRAIT 表示豎向
        //Configuration.ORIENTATION_LANDSCAPE 表示橫屏
        if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            //計算視訊的大小16:9
            layoutParams.width = screenWidth;
            layoutParams.height = screenWidth * 9 / 16;

            setX(mediaPlayerX);
            setY(mediaPlayerY);
        }
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            layoutParams.width = screenWidth;
            layoutParams.height = screenHeight;

            setX(0);
            setY(0);
        }
        setLayoutParams(layoutParams);
    }

3、例項化檢視、手勢、SurfaceView:
private void init() {
        View inflate = View.inflate(context, R.layout.mn_player_view, this);
        mn_rl_bottom_menu = (RelativeLayout) inflate.findViewById(R.id.mn_rl_bottom_menu);
        mn_palyer_surfaceView = (SurfaceView) inflate.findViewById(R.id.mn_palyer_surfaceView);
        mn_iv_play_pause = (ImageView) inflate.findViewById(R.id.mn_iv_play_pause);
        mn_iv_fullScreen = (ImageView) inflate.findViewById(R.id.mn_iv_fullScreen);
        mn_tv_time = (TextView) inflate.findViewById(R.id.mn_tv_time);
        mn_tv_system_time = (TextView) inflate.findViewById(R.id.mn_tv_system_time);
        mn_seekBar = (SeekBar) inflate.findViewById(R.id.mn_seekBar);
        mn_iv_back = (ImageView) inflate.findViewById(R.id.mn_iv_back);
        mn_tv_title = (TextView) inflate.findViewById(R.id.mn_tv_title);
        mn_rl_top_menu = (RelativeLayout) inflate.findViewById(R.id.mn_rl_top_menu);
        mn_player_rl_progress = (RelativeLayout) inflate.findViewById(R.id.mn_player_rl_progress);
        mn_player_iv_lock = (ImageView) inflate.findViewById(R.id.mn_player_iv_lock);
        mn_player_ll_error = (LinearLayout) inflate.findViewById(R.id.mn_player_ll_error);
        mn_player_ll_net = (LinearLayout) inflate.findViewById(R.id.mn_player_ll_net);
        mn_player_progressBar = (ProgressWheel) inflate.findViewById(R.id.mn_player_progressBar);
        mn_iv_battery = (ImageView) inflate.findViewById(R.id.mn_iv_battery);
        mn_player_iv_play_center = (ImageView) inflate.findViewById(R.id.mn_player_iv_play_center);

        mn_seekBar.setOnSeekBarChangeListener(this);
        mn_iv_play_pause.setOnClickListener(this);
        mn_iv_fullScreen.setOnClickListener(this);
        mn_iv_back.setOnClickListener(this);
        mn_player_iv_lock.setOnClickListener(this);
        mn_player_ll_error.setOnClickListener(this);
        mn_player_ll_net.setOnClickListener(this);
        mn_player_iv_play_center.setOnClickListener(this);

        //初始化
        initViews();

        if (!isFirstPlay) {
            mn_player_iv_play_center.setVisibility(View.VISIBLE);
            mn_player_progressBar.setVisibility(View.GONE);
        }

        //初始化SurfaceView
        initSurfaceView();

        //初始化手勢
        initGesture();

        //儲存控制元件的位置資訊
        myHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mediaPlayerX = getX();
                mediaPlayerY = getY();
                Log.i(TAG, "控制元件的位置---X:" + mediaPlayerX + ",Y:" + mediaPlayerY);
            }
        }, 1000);
    }

    private void initViews() {
        mn_tv_system_time.setText(PlayerUtils.getCurrentHHmmTime());
        mn_rl_bottom_menu.setVisibility(View.GONE);
        mn_rl_top_menu.setVisibility(View.GONE);
        mn_player_iv_lock.setVisibility(View.GONE);
        initLock();
        mn_player_rl_progress.setVisibility(View.VISIBLE);
        mn_player_progressBar.setVisibility(View.VISIBLE);
        mn_player_ll_error.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.GONE);
        mn_player_iv_play_center.setVisibility(View.GONE);
        initTopMenu();
    }

    private void initLock() {
        if (isFullscreen) {
            mn_player_iv_lock.setVisibility(View.VISIBLE);
        } else {
            mn_player_iv_lock.setVisibility(View.GONE);
        }
    }

    private void initSurfaceView() {
        Log.i(TAG, "initSurfaceView");
        // 得到SurfaceView容器,播放的內容就是顯示在這個容器裡面
        surfaceHolder = mn_palyer_surfaceView.getHolder();
        surfaceHolder.setKeepScreenOn(true);
        // SurfaceView的一個回撥方法
        surfaceHolder.addCallback(this);
    }

    private void initTopMenu() {
        mn_tv_title.setText(videoTitle);
        if (isFullscreen) {
            mn_rl_top_menu.setVisibility(View.VISIBLE);
        } else {
            mn_rl_top_menu.setVisibility(View.GONE);
        }
    }

其中注意的是SurfaceView是非同步載入,所以我們需要監聽SurfaceHolder的CallBack來確保我們的MediaPlayer準確的播放在SurfaceView中,即:
 //播放
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated");
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mediaPlayer.setDisplay(holder); // 新增到容器中
        //播放完成的監聽
        mediaPlayer.setOnCompletionListener(this);
        // 非同步準備的一個監聽函式,準備好了就呼叫裡面的方法
        mediaPlayer.setOnPreparedListener(this);
        //播放錯誤的監聽
        mediaPlayer.setOnErrorListener(this);
        mediaPlayer.setOnBufferingUpdateListener(this);
        //第一次初始化需不需要主動播放
        if (isFirstPlay) {
            //判斷當前有沒有網路(播放的是網路視訊)
            if (!PlayerUtils.isNetworkConnected(context) && videoPath.startsWith("http")) {
                Toast.makeText(context, context.getString(R.string.mnPlayerNoNetHint), Toast.LENGTH_SHORT).show();
                showNoNetView();
            } else {
                //手機網路給提醒
                if (PlayerUtils.isMobileConnected(context)) {
                    Toast.makeText(context, context.getString(R.string.mnPlayerMobileNetHint), Toast.LENGTH_SHORT).show();
                }
                //新增播放路徑
                try {
                    mediaPlayer.setDataSource(videoPath);
                    // 準備開始,非同步準備,自動在子執行緒中
                    mediaPlayer.prepareAsync();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        isFirstPlay = true;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //儲存播放位置
        if (mediaPlayer != null) {
            video_position = mediaPlayer.getCurrentPosition();
        }
        destroyControllerTask(true);
        pauseVideo();
        Log.i(TAG, "surfaceDestroyed---video_position:" + video_position);
    }

demo作者將MediaPlayer例項化和銷燬放到了SurfaceView的create方法和destroy方法中,我不建議這麼做,而且這麼做很有問題,就是SurfaceView的生命週期不是跟隨Activity的,而是當SurfaceView檢視可見/不可見的時候就會反覆觸發create和destroy方法,所以,我們可以在MNVideoPlayer例項化的時候就將MediaPlayer例項化,之後在SurfaceView的create回撥用方法mediaPlayer.setDisplay(holder),在destroy方法中使用mediaplayer.setDisplay(null)來取消播放投影,這個大家會在我之後的專案中得到體現

4、各個檢視的控制、點選事件及顯示/隱藏、橫豎屏操作,簡單看一下即可:

private void unLockScreen() {
        isLockScreen = false;
        mn_player_iv_lock.setImageResource(R.drawable.mn_player_landscape_screen_lock_open);
    }

    private void lockScreen() {
        isLockScreen = true;
        mn_player_iv_lock.setImageResource(R.drawable.mn_player_landscape_screen_lock_close);
    }

    //下面選單的顯示和隱藏
    private void initBottomMenuState() {
        mn_tv_system_time.setText(PlayerUtils.getCurrentHHmmTime());
        if (mn_rl_bottom_menu.getVisibility() == View.GONE) {
            initControllerTask();
            mn_rl_bottom_menu.setVisibility(View.VISIBLE);
            if (isFullscreen) {
                mn_rl_top_menu.setVisibility(View.VISIBLE);
                mn_player_iv_lock.setVisibility(View.VISIBLE);
            }
        } else {
            destroyControllerTask(true);
        }
    }

    private void dismissControllerMenu() {
        if (isFullscreen && !isLockScreen) {
            mn_player_iv_lock.setVisibility(View.GONE);
        }
        mn_rl_top_menu.setVisibility(View.GONE);
        mn_rl_bottom_menu.setVisibility(View.GONE);
    }

    private void showErrorView() {
        mn_player_iv_play_center.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.GONE);
        mn_player_progressBar.setVisibility(View.GONE);
        mn_player_ll_error.setVisibility(View.VISIBLE);
    }

    private void showNoNetView() {
        mn_player_iv_play_center.setVisibility(View.GONE);
        mn_player_ll_net.setVisibility(View.VISIBLE);
        mn_player_progressBar.setVisibility(View.GONE);
        mn_player_ll_error.setVisibility(View.GONE);
    }

    private void setLandscape() {
        isFullscreen = true;
        //設定橫屏
        ((Activity) context).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        if (mn_rl_bottom_menu.getVisibility() == View.VISIBLE) {
            mn_rl_top_menu.setVisibility(View.VISIBLE);
        }
        initLock();
    }

    private void setProtrait() {
        isFullscreen = false;
        //設定橫屏
        ((Activity) context).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        mn_rl_top_menu.setVisibility(View.GONE);
        unLockScreen();
        initLock();
    }

OK,這篇文章到此結束,我不想一篇文章寫的太長,結果大家看完之後腦袋都疼,下一篇我將為大家解析視訊播放,快取及預載入等相關資訊