前言
最近公司要做一個釋出文章的功能可以添加圖檔和視訊,由于視訊功能在項目中應用的不是很多,是以就打算直接用原生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對應控件也做相應調整。