注:本文适合初学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,这篇文章到此结束,我不想一篇文章写的太长,结果大家看完之后脑袋都疼,下一篇我将为大家解析视频播放,缓存及预加载等相关信息