使用VideoView自定义一个播放器控件

时间:2023-03-08 16:39:05
使用VideoView自定义一个播放器控件

介绍

最近要使用播放器做一个简单的视频播放功能,开始学习VideoView,在横竖屏切换的时候碰到了点麻烦,不过在查阅资料后总算是解决了。在写VideoView播放视频时候定义控制的代码全写在Actvity里了,写完一看我靠代码好乱,于是就写了个自定义的播放器控件,支持指定大小,可以横竖屏切换,手动左右滑动快进快退。好了,下面开始。

效果图

效果图有点卡,我也不知道为啥。。。。。

使用VideoView自定义一个播放器控件

VideoView介绍

这个是我们实现视频播放最主要的控件,详细的介绍大家百度就去看,这里介绍几个常用的方法。

用于播放视频文件。 VideoView 类可以从不同的来源(例如资源文件或内容提供器) 读取图像,计算和维护视频的画面尺寸以使其适用于任何布局管理器, 并提供一些诸如缩放、着色之类的显示选项。

VideoView 常用的几个方法

public int getDuration ()

获得所播放视频的总时间

public int getCurrentPosition ()

获得当前的位置,我们可以用来设置播放时间的显示

public int getCurrentPosition ()

获得当前的位置,我们可以用来设置播放时间的显示

public int pause ()

暂停播放

public int seekTo ()

设置播放位置,我们用来总快进的时候就能用到

public int setOnCompletionListener(MediaPlayer.OnCompletionListener l)

注册在媒体文件播放完毕时调用的回调函数。

public int setOnErrorListener (MediaPlayer.OnErrorListener l)

注册在设置或播放过程中发生错误时调用的回调函数。如果未指定回调函数, 或回调函数返回false,会弹一个dialog提示用户不能播放

public void setOnPreparedListener (MediaPlayer.OnPreparedListener l)

注册在媒体文件加载完毕,可以播放时调用的回调函数。

public void setVideoURI (Uri uri)

设置播放的视频源,也可以用setVideoPath指定本地文件

public void start ()

开始播放

getHolder().setFixedSize(width,height);

设置VideoView的分辨率,如果我们的VideoView在开始播放的时候是竖屏的,当横屏的时候我们改变了VideoView的布局大小,就需要这个方法重新设置它的分辨率,否则你会发现改变了之后VideoView内部的视频部分还是原来的大小,这点要注意。

自定义播放器思路

说是自定义,其实无非就是把这些VideoView和用来显示的其它控件结合在一起,然后在内部处理它的事件交互,我们要做的就是以下几步:1、写好整个空间的布局。2、在自定义控件的内部获取到整个控件内部的各个小控件,并且为它们设置一些初始化事件。3、根据你自己的逻辑和想实现的效果在里面写自己的事件处理,需要在和外部进行交互就提供方法和接口咯。最后就是使用测试效果了。好了,我们就跟着这里说的4步去实现吧!

具体实现

1、第一步,写自己的布局文件

想要的效果就是在底部放一个状态栏显示时间等信息,播放进度,进入全屏,中间放一个快进快退的状态,布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants">
<com.qiangyu.test.commonvideoview.MyVideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"/> //底部状态栏
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#CC282828"
android:padding="3dip"
android:id="@+id/videoControllerLayout"
android:gravity="center"
android:layout_gravity="bottom">
<LinearLayout android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:id="@+id/videoPauseBtn"
android:paddingRight="10dip"
android:paddingLeft="10dp">
<ImageView android:layout_width="22dp"
android:layout_height="22dp"
android:id="@+id/videoPauseImg" />
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingRight="0dip"> <SeekBar android:layout_width="fill_parent"
android:id="@+id/videoSeekBar"
android:layout_weight="1"
style="@android:style/Widget.Holo.SeekBar"
android:layout_height="wrap_content"/>
<TextView android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center"
android:text="00:00"
android:textSize="12dp"
android:id="@+id/videoCurTime"
android:textColor="#FFF"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="12dp"
android:textColor="#FFF"
android:text="/"/>
<TextView android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center"
android:text="00:00"
android:textSize="12dp"
android:id="@+id/videoTotalTime"
android:textColor="#FFF"
android:layout_marginRight="10dp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/screen_status_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
<ImageView
android:id="@+id/screen_status_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/iconfont_enter_32"/>
</LinearLayout>
</LinearLayout> //VideoVIEW中间的开始按钮和进度条以及快进快退的提示
<ProgressBar android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.ProgressBar.Small"/>
<ImageView android:layout_width="30dip"
android:layout_height="30dip"
android:id="@+id/videoPlayImg"
android:layout_gravity="center"
android:src="@mipmap/video_box_play"/>
<LinearLayout
android:id="@+id/touch_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:visibility="invisible"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:background="#000">
<ImageView android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:id="@+id/touchStatusImg"/>
<TextView
android:id="@+id/touch_time"
android:layout_width="wrap_content"
android:text="25:00/59:00"
android:textSize="12sp"
android:textColor="#fff"
android:layout_height="wrap_content"/>
</LinearLayout>
</FrameLayout>

上面的布局很简单,VideoView用了自定义是因为当布局改变的时候,要让VideoView重新获取布局位置,在里面设置它的分辨率为全屏.VideoView的代码如下

public class MyVideoView extends VideoView {

    public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
} public MyVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
} public MyVideoView(Context context) {
super(context);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
this.getHolder().setFixedSize(width,height);//设置分辨率
setMeasuredDimension(width, height);
} }

好,布局写好了我来第二步,获取内部控件初始化事件

2、第二步,onFinishInflate()中得到内部的控件,做初始化工作

onFinishInflate方法在xml解析完毕的时候会回调该方法,一般在做组合控件的时候最常用

@Override
protected void onFinishInflate() {
super.onFinishInflate();
initView();
} private void initView() {
View view = LayoutInflater.from(context).inflate(R.layout.common_video_view,null);
viewBox = (FrameLayout) view.findViewById(R.id.viewBox);
videoView = (MyVideoView) view.findViewById(R.id.videoView);
videoPauseBtn = (LinearLayout) view.findViewById(R.id.videoPauseBtn);
screenSwitchBtn = (LinearLayout) view.findViewById(R.id.screen_status_btn);
videoControllerLayout = (LinearLayout) view.findViewById(R.id.videoControllerLayout);
touchStatusView = (LinearLayout) view.findViewById(R.id.touch_view);
touchStatusImg = (ImageView) view.findViewById(R.id.touchStatusImg);
touchStatusTime = (TextView) view.findViewById(R.id.touch_time);
videoCurTimeText = (TextView) view.findViewById(R.id.videoCurTime);
videoTotalTimeText = (TextView) view.findViewById(R.id.videoTotalTime);
videoSeekBar = (SeekBar) view.findViewById(R.id.videoSeekBar);
videoPlayImg = (ImageView) view.findViewById(R.id.videoPlayImg);
videoPlayImg.setVisibility(GONE);
videoPauseImg = (ImageView) view.findViewById(R.id.videoPauseImg);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar); videoPauseBtn.setOnClickListener(this);
videoSeekBar.setOnSeekBarChangeListener(this);
videoPauseBtn.setOnClickListener(this);
videoView.setOnPreparedListener(this);
videoView.setOnCompletionListener(this);
screenSwitchBtn.setOnClickListener(this);
videoPlayImg.setOnClickListener(this);
//注册在设置或播放过程中发生错误时调用的回调函数。如果未指定回调函数,或回调函数返回false,VideoView 会通知用户发生了错误。
videoView.setOnErrorListener(this);
viewBox.setOnTouchListener(this);
viewBox.setOnClickListener(this);
addView(view);
}

很简单的做了代码初始化和videoView的播放事件的注册,这里的事件待会要处理的有viewBox.setOnTouchListener(this)注册的onTouch事件,我们要在里面写左右滑动快进快退的效果,videoSeekBar.setOnSeekBarChangeListener(this);处理拖动seekbar快进快退。

3、第三步,事件、效果处理

viewBox.setOnTouchListener(this);

1、onTouch里的实现快进快退

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//没播放的时候不处理
if (!videoView.isPlaying()){
return false;
}
float downX = event.getRawX();
touchLastX = downX;
Log.d("FilmDetailActivity", "downX" + downX);
//保存当前播放的位置用与做事件显示
this.position = videoView.getCurrentPosition();
break;
case MotionEvent.ACTION_MOVE:
//没播放的时候不处理
if (!videoView.isPlaying()){
return false;
}
float currentX = event.getRawX();
float deltaX = currentX - touchLastX;
float deltaXAbs = Math.abs(deltaX);
if (deltaXAbs>1){//正向移动,快进
if (touchStatusView.getVisibility()!=View.VISIBLE){
touchStatusView.setVisibility(View.VISIBLE);
//显示快进的时间view
}
touchLastX = currentX;
Log.d("FilmDetailActivity","deltaX"+deltaX);
if (deltaX > 1) {
position += touchStep;
if (position > duration) {
position = duration;
}
touchPosition = position;
touchStatusImg.setImageResource(R.mipmap.ic_fast_forward_white_24dp);
int[] time = getMinuteAndSecond(position);
touchStatusTime.setText(String.format("%02d:%02d/%s", time[0], time[1],formatTotalTime));
} else if (deltaX < -1) {//快退
position -= touchStep;
if (position < 0) {
position = 0;
}
touchPosition = position;
touchStatusImg.setImageResource(R.mipmap.ic_fast_rewind_white_24dp);
int[] time = getMinuteAndSecond(position);
touchStatusTime.setText(String.format("%02d:%02d/%s", time[0], time[1],formatTotalTime));
//mVideoView.seekTo(position);
}
}
break;
case MotionEvent.ACTION_UP:
if (touchPosition!=-1){
videoView.seekTo(touchPosition);
//放开手指的时候快进或快退到滑动决定的时间位置 touchStatusView.setVisibility(View.GONE);
touchPosition = -1;
if (videoControllerShow){
return true;
}
}
break;
}
return false;
}

2、处理 seekBar的拖动事件

@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int[] time = getMinuteAndSecond(progress);
videoCurTimeText.setText(String.format("%02d:%02d", time[0], time[1]));
//设置底部时间显示
} @Override
public void onStartTrackingTouch(SeekBar seekBar) {
videoView.pause();
} @Override
public void onStopTrackingTouch(SeekBar seekBar) {
videoView.seekTo(videoSeekBar.getProgress());
videoView.start();
videoPlayImg.setVisibility(View.INVISIBLE);
videoPauseImg.setImageResource(R.mipmap.icon_video_pause);
//拖动之后到指定的时间位置
}

3、videoView的回调事件

@Override
public void onPrepared(MediaPlayer mp) {
duration = videoView.getDuration();
int[] time = getMinuteAndSecond(duration);
videoTotalTimeText.setText(String.format("%02d:%02d", time[0], time[1]));
formatTotalTime = String.format("%02d:%02d", time[0], time[1]);
videoSeekBar.setMax(duration);
progressBar.setVisibility(View.GONE); mp.start();
videoPauseBtn.setEnabled(true);
videoSeekBar.setEnabled(true); videoPauseImg.setImageResource(R.mipmap.icon_video_pause);
timer.schedule(timerTask, 0, 1000);
//初始化总时间等一些界面显示,同时用timer定时去修改时间进度textView
} @Override
public void onCompletion(MediaPlayer mp) {
videoView.seekTo(0);
videoSeekBar.setProgress(0);
videoPauseImg.setImageResource(R.mipmap.icon_video_play);
videoPlayImg.setVisibility(View.VISIBLE);
}

还有一些其它的点击时间就不放了,都是暂停,播放,点击显示隐藏底部状态栏,全屏切换等的事件。到了这里我们的事件处理完毕啦,接下来就要我们视频怎么播放呢?为了播放我们为外部提供一个方法

//开始播放
public void start(String url){
videoPauseBtn.setEnabled(false);
videoSeekBar.setEnabled(false);
videoView.setVideoURI(Uri.parse(url));
videoView.start();
} //进入全屏时候调用
public void setFullScreen(){
touchStatusImg.setImageResource(R.mipmap.iconfont_exit);
this.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
videoView.requestLayout();
} //退出全屏时候调用
public void setNormalScreen(){
touchStatusImg.setImageResource(R.mipmap.iconfont_enter_32);
this.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,400));
videoView.requestLayout();
}

上面提供的setFullScreen()和setNormalScreen()需要在Activity的 onConfigurationChanged(Configuration newConfig)横竖屏发生改变的 回调方法里面调用,还需要注意的是我这里写的是LinearLayout的LayoutParams,所以我们自定义的view的父空间要是LinearLayout,当然你也可以修改。

4、控件的使用

我们只需要在获得空间调用start方法,然后在onConfigurationChanged方法里调用setFullScreen和setNormalScreen就可以了,

布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.qiangyu.test.commonvideoview.MainActivity"> <android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/> <com.qiangyu.test.commonvideoview.CommonVideoView
android:id="@+id/common_videoView"
android:layout_width="match_parent"
android:layout_height="300dp" />
</LinearLayout>

activity代码

public class MainActivity extends AppCompatActivity {

    CommonVideoView videoView;

    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.content_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
videoView = (CommonVideoView) findViewById(R.id.common_videoView);
videoView.start("你的服务器视频地址");
} @Override public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
videoView.setFullScreen();
}else {
videoView.setNormalScreen();
}
}
}

最后为了防止你的Activity在横竖屏切换的时候重新创建别忘记在AndroidManifest.xml文件里面配置

android:configChanges=”orientation|screenSize|screenLayout”, 如果你这里有疑惑可以参考我的文章–>深入了解Activity-生命周期

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:configChanges="orientation|screenSize|screenLayout">
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

好了,整个控件到这里就完成了,源码下载请戳它—>欢迎戳我

如果你觉得有对你有帮助,请动动手给个赞吧,有问题记得反馈给我哦!