前言
最近公司要做一个发布文章的功能可以添加图片和视频,由于视频功能在项目中应用的不是很多,所以就打算直接用原生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方法,把布局改成我们自己定义的布局文件。
3.2 添加暂停按钮回调监听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.3 注意自定义Controller的布局文件最好用系统的然后调整布局(不要修改控件Id,否则还得修改源码)private final View.OnClickListener mPauseListener = new View.OnClickListener() { @Override public void onClick(View v) { if (mPlayListener != null) mPlayListener.onClick(v); doPauseResume(); show(sDefaultTimeout); } };
<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对应控件也做相应调整。