基于原生MediaPlayer的自定义UI视频播放器控件开发

问题定义

最近在做视频播放器相关的开发,想要达到的一个目标是,利用原生的播放器组件实现在线视频的播放。在做了一番探索后,我完成了两套视频播放器的组件,均支持自定义播放界面的UI。其中一套是基于Android系统原生的MediaPlayer和VideoView,另一套是基于vitamio,这两套组件支持的视频格式是不相同的,而我做的工作是将其组件化,增加了可以支持自定义UI的特性。

原生VideoView实际上是将一个MediaPlayer和一个操作栏相结合,增加了一系列视频播放控制的操作而实现。其中,VideoView的视频控制栏的UI是Framework层实现的,用户无法进行自定义的布局。关于这一点可以参考这篇文章:Android Framework中的PolicyManager简介。因此,需要寻找一种办法,实现自定义UI的特性。

探索基础

首先第一步要做的就是将原生VideoView中利用Framework层实现的UI布局部分替换成自己的实现方案。在这一部分,我参考了这篇文章:

Custom Android media controller

通过以上这篇文章的步骤,我们可以得到一个通过布局文件生成的播放器控制栏。这样就为自定义的UI实现了可能。

我所做的封装和改进

我做的就是在上面这个库的基础上,将通过布局文件生成控制栏的功能进行了进一步的封装,改进成了一个比较容易、灵活的支持自定义UI的组件。我做了如下事情:

1. 将生成UI的过程从MeidaController中抽离出来

Chris的框架中,自定义UI的部分是在MediaController中生成的,并且该UI生成部分是和MediaControler的其他控制逻辑混在一起的,没有做到很好的解耦。我们在实际开发中,不可能专门重写这样一个完整的MediaController,尤其是在除UI部分以外其他视频控制功能上完全一致的前提下。于是我将这一部分抽离出来,只提供一个给MediaController进行UI绑定的接口出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 公共接口:自定义控制条布局生成
*/
public interface MediaControllerGenerator {
/**
* 从布局文件生成一个控制条的自定义布局
* @return BaseMediaControllerHolder对象,控制条控件的集合
*/
BaseMediaControllerHolder generateMediaController();
}
private MediaControllerGenerator mUIGenerator;
public void setUIGenerator(MediaControllerGenerator generator) {
this.mUIGenerator = generator;
}

如上所示,我增加了一个MediaControllerGenerator接口,用来实现自定义UI的生成。我们只要在用到视频播放器的时候实现这个接口,通过generateMediaController()方法生成一个控制栏控件的集合,MediaController就可以自动将控制功能和自定义的UI布局实现绑定。

2. 封装了一个播放控制控件的集合

我们所开发的视频播放器,一把都会有开始暂停按钮、下一个视频、上一个视频、全屏、取消全屏等等的控制按钮。为了在自定义这些控件时更方便,我封装了一个自定义控件集合类BaseMediaControllerHolder,包含了所有可能用到的播放控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* VideoView的控制操作栏控件集合,可以根据自定义的UI布局对该对象进行赋值。
* 该控件集合继承了如下内容:
* {@link #pauseButton} 开始/暂停按钮
* {@link #startResId} 开始ICON资源ID
* {@link #pauseButton} 暂停ICON资源ID
* {@link #stopButton} 停止播放按钮
* {@link #totalTimeView} 视频总时间
* {@link #currentTimeView} 当前播放时间
* {@link #seekbar} 进度条
* {@link #titleView} 视频标题
* {@link #fullScreenButton} 全屏按钮
* {@link #fullscreenResId} 全屏ICON资源ID
* {@link #unfullscreenResId} 取消全屏ICON资源ID
* {@link #nextButton} 下一个按钮
* {@link #preButton} 上一个按钮
*
* Created by Anchorer on 2014/8/12.
*/
public class BaseMediaControllerHolder {
public View parentLayout; //父控件
public ImageButton pauseButton; //开始/暂停按钮
public ImageButton stopButton; //停止按钮
public TextView totalTimeView; //视频总长度
public TextView currentTimeView; //当前播放时间
public SeekBar seekbar; //进度条
public TextView titleView; //视频标题
public ImageButton fullScreenButton; //全屏按钮
public ImageButton nextButton; //下一个按钮
public ImageButton preButton; //上一个按钮
public ImageButton forwardButton; //快进按钮
public ImageButton backwardButton; //后退按钮
public ImageButton likeButton ; // 收藏
public ImageView imageViewBack ; //反悔按钮
public ImageView imageViewShare ;// 分享
public int startResId; //开始按钮图片资源ID
public int pauseResId; //暂停按钮图片资源ID
public int fullscreenResId; //全屏按钮图片资源ID
public int unfullscreenResId; //取消全屏按钮图片资源ID
private boolean hasStopped; //视频是否已停止,可能是播放结束,也可能是手动停止
private List<View> views; //支持一系列自定义的视图,该列表视图实现显示与隐藏
public BaseMediaControllerHolder() {}
...
}

当我们自定义MediaControllerGenerator接口的实现时,生成的也是这样一个控件集合类的对象。这样一来,我们所要做的工作就变成了,从自定义的xml布局文件中生成一系列控件,然后将这些控件封装在一起生成一个BaseMediaControllerHolder类,将这个类与MediaController绑定即可。

3. 在Activity中的应用

至此,我们已经实现了一个自定义UI的MediaController,接下来只要再实现MediaPlayer和该MediaController绑定就可以了,而这个绑定过程其实也就是原生MediaPlayer的使用方法,也就变得顺理成章了。该组件可以在我的Github上查看:

Anchorer/NativeVideoPlayerComponent

其中,MediaPlayer的其他必需的控制操作,我专门实现了一个BaseNativeVideoPlayerActivity的基类Activity,我们只要继承这个Activity实现自己的视频播放Activity即可。

关于该组件的具体使用方法,可以参考example中的Demo。

不足与改进

如果你做过基于Android原生视频播放器的相关开发,你就会了解,Android原生的视频播放器支持的视频格式是非常有限的。(可以参考:Supported Media Formats

为了对各种类型的视频做一些支持,需要在此基础上多做一些工作。实际上,这就是vitamio所做的。我完成的第二套播放器的组件,也是在vitamio的基础上,增加了自己实现的一个自定义UI的特性。

State save and restore of Fragment in Android

随着Android 4.0+ SDK的推广,Fragment也得到了越来越广泛的使用,我个人也越来越喜欢用Fragment来实现各种页面片段。在一个Android工程中,往往很多Activity可以使用同一个Base Activity的框架,这样每个页面使用各自的Fragment就足够了,并且不同页面的Fragment往往可以达到重用的效果,这是之前用Activity时不太容易实现的。

最近在做开发的过程中遇到了Fragment由于内存回收被销毁而无法正常恢复到之前状态的问题。实际上,Android SDK提供了Activity和Fragment恢复状态的机制,因此特别整理一下。

基本作用

Activity提供了onSaveInstanceState()和onRestoreInstanceState()方法,用来实现数据保存和恢复的功能。Fragment中只有onSaveInstanceState()方法,其数据恢复可以在onActivityCreated()方法中实现。

onSaveInstanceState()和onRestoreInstanceState()并不是生命周期方法,并不一定会被触发。当应用遇到意外情况(如内存不足、用户直接按Home键)由系统销毁一个Activity时,onSaveInstanceState()会被调用。但是当用户主动去销毁一个Activity时(例如按返回键),onSaveInstanceState()方法就不会调用。

为了保证Activity(或Fragment)能够正常通过onRestoreInstanceState(Bundle)或者onActivityCreated(Bundle)方法恢复,我们需要在Activity(或Fragment)被杀掉之前保存每个实例的状态。

实例

在我开发的APP中,有一个页面用来展示用户的详细信息和用户发布的内容,该页面通过一个复用的Fragment来实现,其中包含了一个User的实例作为成员变量,来表示一个用户的信息。User类继承了Seriable接口,可以作为可序列化的对象来进行传递。

Fragment的一个简单说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserDetailFragment extends ... implements ... {
// 代表用户信息的一个实例
private User mUser;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(Bundle savedInstanceState);
// 使用User的对象做一些操作
// ...
mUser.getUserData();
// ...
}
/**
* 设置用户的ID,初始化Fragment时调用(调用时间在onActivityCreated之前)
*/
public void initParams(int uid) {
mUser = new User();
mUser.setUid(uid);
}
}

当我从该页面做了一些操作,跳转到另外的页面时,由于内存原因,该Fragment被系统回收了,这样就导致我回到该页面时,onActivityCreated()方法又被调用,Fragment被重建,但是User对象没有进行初始化,从而导致了NullPointerException,程序crash。

解决方案

研究了onSaveInstanceState()方法的用法,我发现应该在Fragment被回收之前,保存User对象的状态。该保存状态的操作是在onSavedInstance()方法中实现的:

1
2
3
4
5
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("user", mUser);
}

outState参数是一个Bundle对象,可以用来暂时保存页面上的关键数据。当Fragment被系统回收时,onSaveInstanceState()方法被调用,我们把User对象暂存在outState中。当页面回收时,outState对象将会作为参数传入onActivityCreated(Bundle)方法中,这时只要在onActivityCreated()方法中恢复User对象即可。

1
2
3
4
// 恢复User对象的实现
if(savedInstanceState != null) {
mUser = (User) savedInstanceState.getSerializable("user");
}

因此,完善后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UserDetailFragment extends ... implements ... {
// 代表用户信息的一个实例
private User mUser;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(Bundle savedInstanceState);
// 从Bundle对象中恢复User对象
if(savedInstanceState != null) {
mUser = (User) savedInstanceState.getSerializable("user");
}
// 使用User的对象做一些操作
// ...
mUser.getUserData();
// ...
}
/**
* 设置用户的ID,初始化Fragment时调用(调用时间在onActivityCreated之前)
*/
public void initParams(int uid) {
mUser = new User();
mUser.setUid(uid);
}
/**
* 页面被回收时,保存User对象的状态
*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("user", mUser);
}
}

一些参考文章:

  1. Activity的onSaveInstanceState方法详解
  2. Recreating an Activity