前言
最近公司要做一个发布文章的功能可以添加图片和视频,由于视频功能在项目中应用的不是很多,所以就打算直接用原生VideoView进行视频的播放,写完我把通用的代码抽取出来,这里分享给大家。
效果图

代码
1.布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rlVideo"
android:layout_width="match_parent"
android:layout_height="210dp"
android:layout_marginLeft="@dimen/margin_two"
android:layout_marginRight="@dimen/margin_two"
android:background="@color/black">
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="210dp"/>
<RelativeLayout
android:id="@+id/rlMask"
android:layout_width="match_parent"
android:layout_height="210dp"
android:focusableInTouchMode="true">
<ImageView
android:id="@+id/ivPicture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="centerCrop"/>
<ImageView
android:id="@+id/ivPlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleType="centerCrop"
android:src="@drawable/btn_video_play_selector"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/rlLoading"
android:layout_width="match_parent"
android:layout_height="210dp"
android:focusableInTouchMode="true"
android:visibility="gone">
<ProgressBar
android:id="@+id/progressbar"
style="@android:style/Widget.ProgressBar.Small.Inverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progressbar"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_five"
android:text="缓冲中..."
android:textColor="@android:color/white"
android:textSize="@dimen/sp_10"/>
</RelativeLayout>
<FrameLayout
android:id="@+id/flDelete"
android:layout_width="@dimen/dp_36"
android:layout_height="@dimen/dp_36"
android:layout_alignParentRight="true"
android:clickable="true">
<ImageView
android:id="@+id/ivDelete"
android:layout_width="@dimen/margin_twenty"
android:layout_height="@dimen/margin_twenty"
android:layout_gravity="right|top"
android:scaleType="fitXY"
android:src="@drawable/btn_delete"
android:visibility="gone"/>
</FrameLayout>
</RelativeLayout>
2. 播放管理类
/**
* @Description 视频播放管理类
* Created by liuchao on 2017/10/24.
*/
public class VideoManager implements MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, View.OnClickListener {
private VideoView mVideoView;
private View mPlayButton;
private View mLoadingLayout;
private View mCompleteLayout;
private ImageView mThumbnailView;
private OnErrorListener mErrorListener;
private boolean mInitVideoView;
public interface OnErrorListener {
void onError(MediaPlayer mp, String errorMsg);
}
/**
* 设置播放出错回调
*
* @param listener
* @return
*/
public VideoManager setOnErrorListener(OnErrorListener listener) {
this.mErrorListener = listener;
return this;
}
/**
* 创建VideoView播放视频管理类
*
* @param videoView
* @return
*/
public static VideoManager create(VideoView videoView) {
if (null == videoView) return null;
return new VideoManager(videoView);
}
public VideoManager(VideoView videoView) {
this.mVideoView = videoView;
this.mVideoView.requestFocus();
}
/**
* @param playButton 播放按钮
* @param loadingLayout 缓冲进度布局
* @param completeLayout 播放完成布局
* @param thumbnailView 第一帧显示缩略图布局
* @return
*/
public VideoManager bindView(View playButton, View loadingLayout, View completeLayout, ImageView thumbnailView) {
this.mPlayButton = playButton;
this.mLoadingLayout = loadingLayout;
this.mCompleteLayout = completeLayout;
this.mThumbnailView = thumbnailView;
initController();
initlistener();
return this;
}
public void init(String videoPath) {
if (TextUtils.isEmpty(videoPath)) return;
mInitVideoView = true;
//加载显示缩略图
loadThumbnail(videoPath);
//设置网络视频路径
Uri uri;
if (-1 != "http://".indexOf(videoPath) || -1 != "https://".indexOf(videoPath))
uri = FileUtils.file2Uri(mVideoView.getContext(), new File(videoPath));
else
uri = Uri.parse(videoPath);
mVideoView.setVideoURI(uri);
setVideoViewLayoutParams(mVideoView, 0);
}
@Override
public void onClick(View v) {
mLoadingLayout.setVisibility(mInitVideoView ? View.VISIBLE : View.GONE);
mInitVideoView = false;
mVideoView.start();
mVideoView.requestFocus();
mCompleteLayout.setVisibility(View.GONE);
}
/**
* 初始化VideoView的监听
*/
private void initlistener() {
this.mVideoView.setOnErrorListener(this);
this.mVideoView.setOnPreparedListener(this);
this.mVideoView.setOnInfoListener(this);
this.mVideoView.setOnCompletionListener(this);
this.mPlayButton.setOnClickListener(this);
}
/**
* 初始化VideoView的进度控制器
*/
private void initController() {
//初始化videoview控制条
VideoController mediaController = new VideoController(mVideoView.getContext());
//设置videoview的控制条
this.mVideoView.setMediaController(mediaController);
//设置显示控制条/
mediaController.show(0);
mediaController.setPlayListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLoadingLayout.setVisibility(mVideoView.isPlaying() ? View.GONE : View.VISIBLE);
mCompleteLayout.setVisibility(mVideoView.isPlaying() ? View.VISIBLE : View.GONE);
}
});
}
@Override
public void onCompletion(MediaPlayer mp) {
mLoadingLayout.setVisibility(View.GONE);
mCompleteLayout.setVisibility(View.VISIBLE);
}
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
this.mVideoView.setBackgroundColor(Color.TRANSPARENT);
this.mLoadingLayout.setVisibility(View.VISIBLE);
} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
//此接口每次回调完START就回调END,若不加上判断就会出现缓冲图标一闪一闪的卡顿现象
if (mp.isPlaying()) {
mLoadingLayout.setVisibility(View.GONE);
}
}
return true;
}
@Override
public void onPrepared(MediaPlayer mp) {
mInitVideoView = false;
mLoadingLayout.setVisibility(View.GONE);//缓冲完成就隐藏
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
switch (what) {
case MediaPlayer.MEDIA_ERROR_UNKNOWN:
Log.e("text", "发生未知错误");
if (null != mErrorListener) mErrorListener.onError(mp, "发生未知错误");
break;
case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
if (null != mErrorListener) mErrorListener.onError(mp, "媒体服务器死机");
Log.e("text", "媒体服务器死机");
break;
default:
Log.e("text", "onError+" + what);
if (null != mErrorListener) mErrorListener.onError(mp, "未知错误");
break;
}
switch (extra) {
case MediaPlayer.MEDIA_ERROR_IO:
//io读写错误
Log.e("text", "文件或网络相关的IO操作错误");
if (null != mErrorListener) mErrorListener.onError(mp, "文件或网络相关的IO操作错误");
break;
case MediaPlayer.MEDIA_ERROR_MALFORMED:
//文件格式不支持
Log.e("text", "比特流编码标准或文件不符合相关规范");
if (null != mErrorListener) mErrorListener.onError(mp, "比特流编码标准或文件不符合相关规范");
break;
case MediaPlayer.MEDIA_ERROR_TIMED_OUT:
//一些操作需要太长时间来完成,通常超过3 - 5秒。
Log.e("text", "操作超时");
if (null != mErrorListener) mErrorListener.onError(mp, "操作超时");
break;
case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:
//比特流编码标准或文件符合相关规范,但媒体框架不支持该功能
Log.e("text", "比特流编码标准或文件合相关规范,但媒体框架不支持该功能");
if (null != mErrorListener)
mErrorListener.onError(mp, "比特流编码标准或文件符合相关规范,但媒体框架不支持该功能");
break;
default:
Log.e("text", "onError+" + extra);
if (null != mErrorListener) mErrorListener.onError(mp, "播放出错了");
break;
}
return false;
}
/**
* 加载缩略图
*
* @param videoPath
*/
private void loadThumbnail(final String videoPath) {
Observable.create(new Observable.OnSubscribe<Bitmap>() {
@Override
public void call(Subscriber<? super Bitmap> subscriber) {
int width = ScreenUtils.getScreenWidth(mVideoView.getContext()) - 50;
int height = DensityUtil.dip2px(mVideoView.getContext(), 220);
if (null != videoPath && -1 != videoPath.indexOf("http://")) {
subscriber.onNext(VideoUtil.createVideoThumbnail(videoPath, width, height));
} else {
subscriber.onNext(VideoUtil.getVideoThumbnail(videoPath, width, height, MediaStore.Images.Thumbnails.MICRO_KIND));
}
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
mThumbnailView.setImageBitmap(bitmap);
}
});
}
/**
* 设置videiview的全屏和窗口模式
*
* @param paramsType 标识 1为全屏模式 2为窗口模式
*/
public void setVideoViewLayoutParams(VideoView videoView, int paramsType) {
//全屏模式
if (1 == paramsType) {
//设置充满整个父布局
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
//设置相对于父布局四边对齐
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
//为VideoView添加属性
videoView.setLayoutParams(layoutParams);
} else {
//窗口模式
//设置窗口模式距离边框50
// int videoHeight = DensityUtil.getScreenHeight(videoView.getContext()) - 50;
int videoHeight = DensityUtil.dip2px(videoView.getContext(), 220);
// int videoWidth = DensityUtil.getScreenWidth(videoView.getContext()) - 50;
int videoWidth = RelativeLayout.LayoutParams.WRAP_CONTENT;
RelativeLayout.LayoutParams LayoutParams = new RelativeLayout.LayoutParams(videoWidth, videoHeight);
//设置居中
LayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
//为VideoView添加属性
videoView.setLayoutParams(LayoutParams);
}
}
}
3. 自定义MediaPlayerController
3.1 重新MediaPlayerController,修改makeControllerView方法,把布局改成我们自己定义的布局文件。
protected View makeControllerView() {
LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRoot = inflate.inflate(R.layout.view_video_controller, null);
initControllerView(mRoot);
return mRoot;
}
3.2 添加暂停按钮回调监听
private final View.OnClickListener mPauseListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mPlayListener != null) mPlayListener.onClick(v);
doPauseResume();
show(sDefaultTimeout);
}
};
3.3 注意自定义Controller的布局文件最好用系统的然后调整布局(不要修改控件Id,否则还得修改源码)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CC000000"
android:layoutDirection="ltr"
android:orientation="vertical">
<LinearLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingTop="4dip">
<ImageButton
android:id="@+id/prev"
style="@android:style/MediaButton.Previous"/>
<ImageButton
android:id="@+id/rew"
style="@android:style/MediaButton.Rew"/>
<!-- <ImageButton
android:id="@+id/pause"
style="@android:style/MediaButton.Play"/>-->
<ImageButton
android:id="@+id/ffwd"
style="@android:style/MediaButton.Ffwd"/>
<ImageButton
android:id="@+id/next"
style="@android:style/MediaButton.Next"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/pause"
android:layout_marginLeft="10dp"
android:layout_width="20dip"
android:layout_height="20dip"
android:layout_gravity="center_vertical"
style="@android:style/MediaButton.Play"/>
<SeekBar
android:id="@+id/mediacontroller_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dip"
android:layout_height="32dip"
android:layout_weight="1"/>
<TextView
android:id="@+id/time_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingEnd="4dip"
android:paddingStart="4dip"
android:paddingTop="4dip"
android:textColor="#bebebe"
android:textSize="14sp"
android:textStyle="bold"/>
<TextView
android:visibility="gone"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingEnd="4dip"
android:paddingStart="4dip"
android:paddingTop="4dip"
android:textColor="#bebebe"
android:textSize="14sp"
android:textStyle="bold"/>
</LinearLayout>
</LinearLayout>
4. 使用VideoManager播放视频
View rlMask = itemView.findViewById(R.id.rlMask);
View showPlayerLoading = itemView.findViewById(R.id.rlLoading);
ImageView ivPlay = (ImageView) itemView.findViewById(R.id.ivPlay);
ImageView thumbnailView = (ImageView) itemView.findViewById(R.id.ivPicture);
thumbnailView.setTag(R.id.tag_path, videoPath);
//初始化VideoView
VideoView videoView = (VideoView) itemView.findViewById(R.id.videoView);
rlMask.setOnClickListener(this);
VideoManager.create(videoView).bindView(ivPlay, showPlayerLoading, rlMask, thumbnailView).init(videoPath);
5. 最后补上VideoUtil中两个获取本地或服务端视频缩略图代码
/**
* 获取网路视频缩略
* 自己的后台线程中调用该方法得到网络视频的缩略图bitmap
* 然后在主线程中调用imageView.setImageBitmap(bitmap)即可
*
* @param url 网路视频地址
* @param width 指定输出视频缩略图的宽度
* @param height 指定输出视频缩略图的高度度
* @return
*/
public static Bitmap createVideoThumbnail(String url, int width, int height) {
Bitmap bitmap = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
int kind = MediaStore.Video.Thumbnails.MINI_KIND;
try {
if (Build.VERSION.SDK_INT >= 14) {
retriever.setDataSource(url, new HashMap<String, String>());
} else {
retriever.setDataSource(url);
}
bitmap = retriever.getFrameAtTime();
} catch (IllegalArgumentException ex) {
// Assume this is a corrupt video file
} catch (RuntimeException ex) {
// Assume this is a corrupt video file.
} finally {
try {
retriever.release();
} catch (RuntimeException ex) {
// Ignore failures while cleaning up.
}
}
if (kind == MediaStore.Images.Thumbnails.MICRO_KIND && bitmap != null) {
bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
}
return bitmap;
}
/**
* 获取本地视频的缩略图
* 先通过ThumbnailUtils来创建一个视频的缩略图,然后再利用ThumbnailUtils来生成指定大小的缩略图。
* 如果想要的缩略图的宽和高都小于MICRO_KIND,则类型要使用MICRO_KIND作为kind的值,这样会节省内存。
*
* @param videoPath 视频的路径
* @param width 指定输出视频缩略图的宽度
* @param height 指定输出视频缩略图的高度度
* @param kind 参照MediaStore.Images.Thumbnails类中的常量MINI_KIND和MICRO_KIND。
* 其中,MINI_KIND: 512 x 384,MICRO_KIND: 96 x 96
* @return 指定大小的视频缩略图
*/
public static Bitmap getVideoThumbnail(String videoPath, int width, int height, int kind) {
Bitmap bitmap = null;
// 获取视频的缩略图
bitmap = ThumbnailUtils.createVideoThumbnail(videoPath, kind);
System.out.println("w" + bitmap.getWidth());
System.out.println("h" + bitmap.getHeight());
bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
return bitmap;
}
总结
这里使用VideoManager进行播放视频就是必须使用指定布局,传入四个控件进行控制通过init方法设置播放路径,如果确定播放布局是这样的就可以重复利用布局文件,每次在需要播放的地方把步骤4的代码拷贝过去就好了,如果对布局要做调整,必须把VideoManager对应控件也做相应调整。