零、前言
對於視訊的播放,Android有內建的VideoView,用起來非常簡單
本篇從自定義VideoView來封裝MediaPlayer開始說起
<VideoView
android:id="@+id/id_vv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")
複製程式碼
本文聚焦
[1].自定義VideoView結合SurfaceView和MediaPlayer來播放視訊
[2].使用媒體庫的ContentProvider查詢手機中視訊,並列表顯示
[3].更改視訊的寬高以及適應橫豎屏切換
[4].自定義控制介面以及倍速播放
[5].視訊封面圖(視訊幀)的獲取
[6].播放網路視訊及seekBar的第二進度和快取進度監聽
複製程式碼
一、簡易版:MediaPlayer + SurfaceView + MediaController
角色:
MediaPlayer 視訊處理器
SurfaceView 視訊顯示介面
MediaController 視訊控制器
複製程式碼
1.自定義VideoView繼承自SurfaceView
/**
* 作者:張風捷特烈<br/>
* 時間:2019/3/8/008:12:43<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:視訊播放:MediaPlayer + SurfaceView + MediaController
*/
public class VideoView extends SurfaceView implements MediaController.MediaPlayerControl {
private SurfaceHolder mSurfaceHolder;//SurfaceHolder
private MediaPlayer mMediaPlayer;//媒體播放器
private MediaController mMediaController;//媒體控制器
private int mVideoHeight;//視訊寬高
private int mVideoWidth;//視訊高
private int mSurfaceHeight;//SurfaceView高
private int mSurfaceWidth;//SurfaceView寬
private boolean isPrepared;//是否已準備好
private Uri mUri;//播放的地址
private int mCurrentPos;//當前進度
private int mDuration = -1;//當前播放視訊時長
private int mCurrentBufferPer;//當前緩衝進度--網路
public VideoView(Context context) {
this(context, null);
}
public VideoView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
openVideo();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mSurfaceHeight = height;
mSurfaceWidth = width;
if (mMediaPlayer != null && isPrepared) {
initPosition();
mMediaPlayer.start();//開始播放
showCtrl();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mSurfaceHolder = null;
hideController();
releasePlayer();
}
});
}
/**
* 顯示控制器
*/
private void showCtrl() {
if (mMediaController != null) {
mMediaController.show();
}
}
/**
* 隱藏控制器
*/
private void hideController() {
if (mMediaController != null) {
mMediaController.hide();
}
}
/**
* 初始化最初位置
*/
private void initPosition() {
if (mCurrentPos != 0) {
mMediaPlayer.seekTo(mCurrentPos);
mCurrentPos = 0;
}
}
private void openVideo() {
if (mUri == null || mSurfaceHolder == null) {
return;
}
isPrepared = false;//沒有準備完成
releasePlayer();
mMediaPlayer = new MediaPlayer();
try {
mMediaPlayer.setDataSource(getContext(), mUri);
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);//播放時螢幕一直亮著
mMediaPlayer.prepareAsync();//非同步準備
attach2Ctrl();//繫結媒體控制器
} catch (IOException e) {
e.printStackTrace();
}
//準備監聽
mMediaPlayer.setOnPreparedListener(mp -> {
isPrepared = true;
if (mMediaController != null) {//控制器可用
mMediaController.setEnabled(true);
}
if (mOnPreparedListener != null) {//補償回撥
mOnPreparedListener.onPrepared(mp);
}
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
if (mVideoWidth != 0 && mVideoHeight != 0) {
getHolder().setFixedSize(mVideoWidth, mVideoHeight);
//開始初始化
initPosition();
if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
if (!isPlaying() && mCurrentPos != 0 || getCurrentPosition() > 0) {
if (mMediaController != null) {
mMediaController.show(0);
}
}
}
}
});
//尺寸改變監聽
mMediaPlayer.setOnVideoSizeChangedListener((mp, width, height) -> {
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
if (mOnSizeChanged != null) {
mOnSizeChanged.onSizeChange();
}
if (mVideoWidth != 0 && mVideoHeight != 0) {
getHolder().setFixedSize(mVideoWidth, mVideoHeight);
}
});
//完成監聽
mMediaPlayer.setOnCompletionListener(mp -> {
hideController();
start();
if (mOnCompletionListener != null) {
mOnCompletionListener.onCompletion(mp);
}
});
//錯誤監聽
mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
hideController();
if (mOnErrorListener != null) {
mOnErrorListener.onError(mp, what, extra);
}
return true;
});
mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
mCurrentBufferPer = pre;
});
}
/**
* 釋放播放器
*/
private void releasePlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
private void attach2Ctrl() {
if (mMediaPlayer != null && mMediaController != null) {
mMediaController.setMediaPlayer(this);
View anchor = this.getParent() instanceof View ? (View) this.getParent() : this;
mMediaController.setAnchorView(anchor);
mMediaController.setEnabled(true);
}
}
public void setVideoPath(String path) {
mUri = Uri.parse(path);
setVideoURI(mUri);
}
public void setVideoURI(Uri uri) {
mUri = uri;
mCurrentPos = 0;
openVideo();//開啟視訊
requestLayout();//更新介面
invalidate();
}
public void setMediaController(MediaController mediaController) {
hideController();
mMediaController = mediaController;
attach2Ctrl();
}
public void stopPlay() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
private void toggle() {
if (mMediaController.isShowing()) {
mMediaController.hide();
} else {
mMediaController.show();
}
}
private boolean canPlay() {
return mMediaPlayer != null && isPrepared;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isPrepared && mMediaController != null && mMediaPlayer != null) {
toggle();
}
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = adjustSize(mVideoWidth, widthMeasureSpec);
int h = adjustSize(mVideoHeight, heightMeasureSpec);
setMeasuredDimension(w, h);
}
public int adjustSize(int size, int measureSpec) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int len = MeasureSpec.getMode(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, len);
break;
case MeasureSpec.EXACTLY:
result = len;
break;
}
return result;
}
//----------------------------------------------------------------
//------------MediaPlayerControl介面函式---------------------------
//----------------------------------------------------------------
@Override
public void start() {
if (canPlay()) {
mMediaPlayer.start();
}
}
@Override
public void pause() {
if (canPlay() && mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
}
}
@Override
public int getDuration() {
if (canPlay()) {
if (mDuration > 0) {
return mDuration;
}
mDuration = mMediaPlayer.getDuration();
return mDuration;
}
mDuration = -1;
return mDuration;
}
@Override
public int getCurrentPosition() {
if (canPlay()) {
return mMediaPlayer.getCurrentPosition();
}
return 0;
}
@Override
public void seekTo(int pos) {
if (canPlay()) {
mMediaPlayer.seekTo(pos);
} else {
mCurrentPos = pos;
}
}
@Override
public boolean isPlaying() {
if (canPlay()) {
return mMediaPlayer.isPlaying();
}
return false;
}
@Override
public int getBufferPercentage() {
if (canPlay()) {
return mCurrentBufferPer;
}
return 0;
}
@Override
public boolean canPause() {
return true;
}
@Override
public boolean canSeekBackward() {
return true;
}
@Override
public boolean canSeekForward() {
return true;
}
@Override
public int getAudioSessionId() {
return 0;
}
//----------------------------------------------------------------
//------------補償回撥---------------------------
//----------------------------------------------------------------
private MediaPlayer.OnPreparedListener mOnPreparedListener;
private MediaPlayer.OnCompletionListener mOnCompletionListener;
private MediaPlayer.OnErrorListener mOnErrorListener;
public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) {
mOnPreparedListener = onPreparedListener;
}
public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
mOnCompletionListener = onCompletionListener;
}
public void setOnErrorListener(MediaPlayer.OnErrorListener onErrorListener) {
mOnErrorListener = onErrorListener;
}
public interface OnSizeChanged {
void onSizeChange();
}
private OnSizeChanged mOnSizeChanged;
public void setOnSizeChanged(OnSizeChanged onSizeChanged) {
mOnSizeChanged = onSizeChanged;
}
}
複製程式碼
2.根據路徑使用測試
簡單一點,可以用系統自帶的控制器:MediaController,不過醜到爆炸
檔案許可權自理:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
---->[activity_main.xml]------------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.toly1994.ivideo.widget.VideoView
android:id="@+id/id_vv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")
複製程式碼
3.獲取所有的視訊並根據插入時間降序排列
/**
* 作者:張風捷特烈<br/>
* 時間:2018/10/30 0030:18:38<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:視訊ContentProvide相關操作---生成視訊List
*/
public class VideoScanner {
static String[] projection = new String[]{
MediaStore.Video.Media._ID,//ID
MediaStore.Video.Media.TITLE,//名稱
MediaStore.Video.Media.DURATION,//時長
MediaStore.Video.Media.DATA,//路徑
MediaStore.Video.Media.SIZE,//大小
MediaStore.Video.Media.DATE_ADDED//新增的時間
};
/**
* 歌曲集合
*/
private static List<VideoInfo> videos = new ArrayList<>();
/**
* 讀取音訊
*/
public static List<VideoInfo> loadVideo(final Context context) {
if (videos.size() != 0) {
return videos;
}
Cursor cursor = context.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection, "", null,
"date_added desc", null);
// 根據欄位獲取資料庫中資料的索引
int songIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
int titleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
int durationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION);
int dataUrlIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
int sizeIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE);
int addDateIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED);
while (cursor.moveToNext()) {
long videoId = cursor.getLong(songIdIdx);//獲取id
String title = cursor.getString(titleIdx);//獲取名字
String dataUrl = cursor.getString(dataUrlIdx);//獲取路徑
long duration = cursor.getLong(durationIdx);//獲取時長
long size = cursor.getLong(sizeIdx);//獲取大小
long addDate = cursor.getLong(addDateIdx);//加入時間
videos.add(new VideoInfo(videoId, title, dataUrl, duration, size, addDate));
}
return videos;
}
}
複製程式碼
4.RecyclerView裝一下Video資訊
關於封面預覽圖等會在倒騰,佈局什麼的就不貼了,自己寫
當點選的時候,跳轉到剛才的那個播放Activity,用Intent傳遞視訊路徑
---->[HomeAdapter#onBindViewHolder]-------------------------------------------
holder.mIvCover.setOnClickListener(v -> {
Intent intent = new Intent(mContext, PlayerActivity.class);
intent.putExtra("video-path", videoInfo.getDataUrl());
mContext.startActivity(intent);
});
---->[附贈一個視訊時間轉化的方法]----------------------------------------
private String format(long duration) {
long time = duration / 1000;
String result = "";
long minus = time / 60;
int hour = 0;
if (minus > 60) {
hour = (int) (minus / 60);
minus = minus % 60;
}
long second = time % 60;
if (hour < 60) {
result = handleNum(hour) + ":" + handleNum(minus)+":"+handleNum(second);
}
return result;
}
private String handleNum(long num) {
return num < 10 ? ("0" + num) : (num + "");
}
---->[PlayerActivity]-------------------------------------------
val path = intent.getStringExtra("video-path")
id_vv.setMediaController(MediaController(this))
id_vv.setUri(path)
複製程式碼
OK 簡易版的視訊播放器就OK了。
二、介面橫豎屏問題
這轉個屏,D 都變成 A 了,怎麼能忍,趕快修一下
1.關於縮放
getHolder().setFixedSize(w,h) 測試了一下,然並卵,解析度沒有改變
|-- 來翻一下原始碼
/**
* Make the surface a fixed size. It will never change from this size.
* When working with a {@link SurfaceView}, this must be called from the
* same thread running the SurfaceView's window.
* 使surface的大小固定。它的大小永遠不會改變。
* 當使用SurfaceView時,必須從執行SurfaceView視窗的同一執行緒呼叫它。
* @param width The surface's width. surface寬
* @param height The surface's height. surface高
*/
public void setFixedSize(int width, int height);
複製程式碼
看來此路不通,那隻能求他路
2.直接變更View的尺寸
public void changeVideoFitSize(int videoW, int videoH, int surfaceW, int surfaceH) {
float videoSizeRate = videoW * 1.0f / videoH;
//橫屏下的切換 -- 正常寬高比例
float widthRatePortrait = videoW * 1.0f / surfaceW;
float heightRatePortrait = videoH * 1.0f / surfaceH;
//橫屏下的切換 View寬高互換-- 寬高比例
float widthRateLand = videoW * 1.0f / surfaceH;
float heightRateLand = videoH * 1.0f / surfaceW;
float ratio;
if (getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {//橫屏
//豎屏模式下
ratio = Math.max(widthRatePortrait, heightRatePortrait);
} else {
//橫屏模式下
if (videoSizeRate > 1) {
ratio = Math.min(widthRateLand, heightRateLand);
} else {
ratio = Math.max(widthRateLand, heightRateLand);
}
}
//視訊寬高分別/最大倍數值 計算出放大後的視訊尺寸
videoW = (int) Math.ceil(videoW * 1.0f / ratio);
videoH = (int) Math.ceil(videoH * 1.0f / ratio);
//根據將視訊尺寸變更View
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
setLayoutParams(params);
}
|--- 使用:
---->[setOnVideoSizeChangedListener中]---------------------------------------------
changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight);
複製程式碼
3.不滿屏時居中
至於怎麼居中,我天真的以為在xml裡改一下就行了,but,並沒用,因為這裡是自己玩LayoutParams
所以居中也要用LayoutParams,沒辦法,走波原始碼唄。
---->[RelativeLayout#CENTER_IN_PARENT]---------------------
public static final int CENTER_IN_PARENT = 13;
CENTER_IN_PARENT是一個int型控制的,看一下LayoutParams的原始碼,暴露的方法就那幾個,
addRule恰只有一個int入參,應該就是它了
---->[RelativeLayout.LayoutParams#addRule(int)]---------------------
public void addRule(int verb) {
addRule(verb, TRUE);
}
---->[.VideoView#changeVideoFitSize(int, int, int, int)]-------------
---- 輕輕寫語句,即可
params.addRule(13);
複製程式碼
3.自定義寬高縮放比例
public void changeVideoSize(float rateX, float rateY) {
changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight, rateX, rateY);
}
public void changeVideoFitSize(
int videoW, int videoH, int surfaceW, int surfaceH,
float rateX, float rateY) {
...
//視訊寬高分別/最大倍數值 計算出放大後的視訊尺寸
videoW = (int) Math.ceil(videoW * 1.0f / ratio * rateX);
videoH = (int) Math.ceil(videoH * 1.0f / ratio * rateY);
//無法直接設定視訊尺寸,將計算出的視訊尺寸設定到surfaceView 讓視訊自動填充。
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
params.addRule(13);
setLayoutParams(params);
}
複製程式碼
三、定製操作介面
1.介面操作
自定義的介面就是根據VideoView中的Api自己實現控制邏輯,細心一點還是不難的,就是麻煩
介面如下,不貼布局了,比較簡單,也挺多的,這裡說一下顯示皮膚後5秒後隱藏的邏輯
private val mHandler = Handler(Looper.getMainLooper())
root.setOnClickListener {//點選顯示皮膚
showPanel(mHandler)
}
private fun hidePanel() {
id_ll_top.visibility = View.GONE
id_ll_bottom.visibility = View.GONE
id_iv_lock.visibility = View.GONE
}
private fun showPanel(handler: Handler) {
id_ll_top.visibility = View.VISIBLE
id_ll_bottom.visibility = View.VISIBLE
id_iv_lock.visibility = View.VISIBLE
handler.postDelayed(::hidePanel, 5000)
}
複製程式碼
2.倍速播放
二倍速聽mv挺搞笑的,API 23 + 也就是一句Api的事,很方便
/**
* 變速
* @param speed
*/
public void changeSpeed(float speed) {
//API 23 + 支援
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
} else {
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
mMediaPlayer.pause();
}
}
}
|-- 使用陣列來控制-----------------------
private var speeds = floatArrayOf(0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2.0f)
private var curSpeedIdx = 2
id_tv_speed.setOnClickListener {
curSpeedIdx++
if (curSpeedIdx == speeds.size) {
curSpeedIdx = 0
}
val speed = speeds[curSpeedIdx]
id_vv.changeSpeed(speed)
id_tv_speed.text = "$speed X"
}
複製程式碼
3.封面圖的獲取
基本上也就這麼多了,最後講一下視訊封面幀圖片的獲取:數了一下這幀大概在15秒
測試了一下秒數越大,獲取圖片的速度越慢,也就是越卡,所以還是給0吧
如果在Adapter裡實時載入會很卡,最好查詢的時候就把bitmap放到實體類裡,由於封面圖不要很大
別把原圖給放進去了,小心直接OOM。Bitmap的操作本文就不贅述了。
---->[HomeAdapter]------------------------
private final MediaMetadataRetriever retriever;
retriever = new MediaMetadataRetriever();
/**
* 獲取視訊某一幀
*
* @param path 路徑
* @param timeMs 毫秒
*/
public Bitmap decodeFrame(String path,long timeMs) {
retriever.setDataSource(path);
Bitmap bitmap = retriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
if (bitmap == null) {
return null;
}
return bitmap;
}
複製程式碼
此選項與{@link #getFrameAtTime(long,int)}一起使用,以檢索與位於給定時間附近或給定時間的資料來源相關聯的幀(不一定是關鍵幀)。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a frame (not necessarily a key frame) associated with a data source that
* is located closest to or at the given time.
public static final int OPTION_CLOSEST = 0x03;
此選項與{@link #getFrameAtTime(long,int)}一起使用,以檢索與位於(時間上)最接近或給定時間的資料來源相關聯的同步(或鍵)幀。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* closest to (in time) or at the given time.
public static final int OPTION_CLOSEST_SYNC = 0x02;
此選項與{@link #getFrameAtTime(long,int)}一起使用,以檢索與位於給定時間之後或指定時間的資料來源關聯的同步(或鍵)幀。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* right after or at the given time.
public static final int OPTION_NEXT_SYNC = 0x01;
此選項與{@link #getFrameAtTime(long,int)}一起使用,以檢索與位於給定時間之前或指定時間的資料來源關聯的同步(或鍵)幀。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* right before or at the given time.
public static final int OPTION_PREVIOUS_SYNC = 0x00;
複製程式碼
四、網路視訊的播放
1.網路視訊
放在伺服器上了,地址:
http://www.toly1994.com:8089/imgs/sh.mp4
,就一句話
id_vv.setVideoPath("http://www.toly1994.com:8089/imgs/sh.mp4")
複製程式碼
2.SeekBar的第二進度
---->[drawable/seekbar_bg.xml]--------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="#eee" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<solid android:color="#2db334"/>
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="@color/colorAccent"/>
</shape>
</clip>
</item>
</layer-list>
---->[layout/in_player_panel_bottom.xml]---------------------------
<SeekBar
...
android:progressDrawable="@drawable/seekbar_bg"
複製程式碼
3.快取監聽
---->[com.toly1994.ivideo.widget.VideoView]------------------
mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
mCurrentBufferPer = pre;
if (mOnBufferingUpdateListener != null) {
mOnBufferingUpdateListener.update(pre);
}
});
public interface OnBufferingUpdateListener {
void update(int pre);
}
private OnBufferingUpdateListener mOnBufferingUpdateListener;
public void setOnBufferingUpdateListener(OnBufferingUpdateListener onBufferingUpdateListener) {
mOnBufferingUpdateListener = onBufferingUpdateListener;
}
使用:
id_vv.setOnBufferingUpdateListener {
id_sb_progress.secondaryProgress = it
}
複製程式碼
Ok 這樣就完成了。本篇就這樣,更多的功能可以自己去擴充,
搭個後臺,弄個簡單的網路播放器也未嘗不可。
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
無 | 2018-3-9 | Android多媒體之視訊播放器(基於MediaPlayer) |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援