如何寫一個播放器-解析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,這篇文章到此結束,我不想一篇文章寫的太長,結果大家看完之後腦袋都疼,下一篇我將為大家解析視訊播放,快取及預載入等相關資訊