Android開發筆記(一百二十五)自定義視訊播放器
視訊播放方式
在Android中播放視訊的方式有兩種:1、使用MediaPlayer結合SurfaceView進行播放。其中通過SurfaceView顯示視訊的畫面,通過MediaPlayer來設定播放引數、並控制視訊的播放操作;該方式的具體說明參見《Android開發筆記(五十七)錄影錄音與播放》。
該方式的好處是靈活性強,可隨意定製。缺點是編碼複雜,連開始/暫停的按鈕都要自己實現。
2、使用VideoView結合MediaController進行播放。VideoView其實是從SurfaceView擴充套件而來,並在內部整合了MediaPlayer,從而實現視訊畫面與視訊操作的統一管理;而MediaController則是一個簡單的播放控制條,它實現了基本的控制按鈕,如開始/暫停按鈕、上一個/下一個按鈕、快進/快退按鈕,以及進度條等控制元件;把VideoView與MediaController關聯起來,便是一個類似於Window Media Player的精簡版播放器。
該方式的好處是簡單易用,編碼容易。缺點是可定製差,難以擴充套件,想給按鈕換個樣式都不行。
但是不積跬步無以至千里,如果我們要定製一個好用好看的播放器,還是得先把笨拙的VideoView與MediaController搞清楚才行。就像窮國一開始沒有汽車工業,那隻能從研究拖拉機開始,沒辦法一蹴而就強行大躍進呀。
VideoView結合MediaController
VideoView
前面說過,VideoView把SurfaceView與MediaPlayer整合在了一起,所以它不但提供SurfaceView的所有方法,而且提供MediaPlayer的主要方法。如果讀者已經用過MediaPlayer/SurfaceView的話,想必對VideoView的常用方法並不陌生,下面是它的常用方法說明:setVideoPath : 設定視訊檔案的路徑。
setMediaController : 設定播放控制條。
setOnPreparedListener : 設定預備播放監聽器。需要重寫onPrepared方法,該方法在準備播放時呼叫。
setOnCompletionListener : 設定結束播放監聽器。需要重寫onCompletion方法,該方法在結束播放時呼叫。
setOnErrorListener : 設定播放異常監聽器。需要重寫onError方法,該方法在播放出現異常時呼叫。
setOnInfoListener : 設定播放資訊監聽器。需要重寫onInfo方法,該方法在播放需要傳遞某種訊息時呼叫,如開始/結束緩衝。
requestFocus : 請求獲得焦點。該方法在start方法前呼叫。
start : 開始播放。
pause : 暫停播放。
resume : 恢復播放。
suspend : 結束播放並釋放資源。
seekTo : 拖動到指定進度開始播放。
getDuration : 獲得視訊的總時長。
getCurrentPosition : 獲得當前的播放位置。當該方法返回值與getDuration相等時,表示播放到了末尾。
isPlaying : 判斷是否在播放。
getBufferPercentage : 獲得已緩衝的比例。返回值在0到1之間。
MediaController
VideoView看起來只有光禿禿的視訊畫面,要想讓使用者與它進行互動,還得通過MediaController來中轉控制操作。MediaController的介面和功能跟Windows平臺上的簡單播放條几乎一模一樣,下面是它的常用方法說明:setMediaPlayer : 設定播放器。該方法與setAnchorView只能同時呼叫其中之一。
setAnchorView : 設定繫結的屬主檢視。該方法與setMediaPlayer只能同時呼叫其中之一。
show : 顯示控制條。
hide : 隱藏控制條。
isShowing : 判斷控制條是否顯示。
setPrevNextListeners : 設定前一個按鈕與後一個按鈕的點選監聽器。如果沒呼叫該方法,那麼前一個按鈕與後一個按鈕都不會展示。
整合VideoView和MediaController
VideoView繼承自SurfaceView,而MediaController繼承自FrameLayout,所以理論上這兩個控制元件是可以隨意擺放的,但是考慮到使用者的使用習慣,它們往往形成一個整體來展示,即MediaController固定位於VideoView的底部。因此我們不會在佈局檔案中宣告MediaController控制元件,只會宣告VideoView控制元件,然後讓控制條附著與視訊檢視之上。甚至佈局檔案中都不用宣告視訊檢視,而在程式碼中動態新增視訊畫面,由此便衍生出VideoView和MediaController的兩種整合方式:1、在佈局檔案中宣告VideoView。
VideoView物件的使用步驟不變,即先呼叫setVideoPath方法指定視訊檔案,然後呼叫setMediaController方法指定控制條,最後呼叫start方法開始播放。此時MediaController物件只需呼叫setMediaPlayer方法指定播放器即可。
2、在程式碼中動態新增VideoView。
VideoView物件的使用步驟同上。此時MediaController物件的使用步驟發生變化,它不再呼叫setMediaPlayer方法,改成呼叫setAnchorView方法,該方法的意思是把MediaController檢視新增到屬主檢視上,如果方法引數是個VideoView物件,則將MediaController檢視新增到VideoView物件的上級檢視。
兩種整合方式在手機螢幕的展示效果基本一樣,開發者可根據視訊的展示位置來決定採用哪種方式。
下面是VideoView和MediaController的播放效果截圖:
下面是在佈局檔案中宣告VideoView的程式碼例子:
import java.util.Map;
import com.aqi00.lib.dialog.FileSelectFragment;
import com.aqi00.lib.dialog.FileSelectFragment.FileSelectCallbacks;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
public class VideoPlayActivity extends Activity implements OnClickListener, FileSelectCallbacks {
private static final String TAG = "VideoPlayActivity";
private Button btn_open;
private VideoView vv_play;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_video_play);
btn_open = (Button) findViewById(R.id.btn_open);
btn_open.setOnClickListener(this);
vv_play = (VideoView) findViewById(R.id.vv_play);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_open) {
FileSelectFragment.show(this, new String[]{"mp4"}, null);
}
}
@Override
public void onConfirmSelect(String absolutePath, String fileName, Map<String, Object> map_param) {
Log.d(TAG, "onConfirmSelect absolutePath=" + absolutePath + ". fileName=" + fileName);
String file_path = "";
if (absolutePath != null && fileName != null) {
file_path = absolutePath + "/" + fileName;
}
Toast.makeText(this, "已開啟視訊", Toast.LENGTH_SHORT).show();
vv_play.setVideoPath(file_path);
vv_play.requestFocus();
MediaController mc_play = new MediaController(this);
vv_play.setMediaController(mc_play);
mc_play.setMediaPlayer(vv_play);
vv_play.start();
}
@Override
public boolean isFileValid(String absolutePath, String fileName, Map<String, Object> map_param) {
return true;
}
}
下面是在程式碼中動態新增VideoView的程式碼例子:
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
import com.aqi00.lib.dialog.FileSelectFragment;
import com.aqi00.lib.dialog.FileSelectFragment.FileSelectCallbacks;
public class VideoControllerActivity extends Activity implements OnClickListener, FileSelectCallbacks {
private static final String TAG = "VideoControllerActivity";
private Button btn_open;
private LinearLayout ll_play;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_video_controller);
btn_open = (Button) findViewById(R.id.btn_open);
btn_open.setOnClickListener(this);
ll_play = (LinearLayout) findViewById(R.id.ll_play);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_open) {
FileSelectFragment.show(this, new String[]{"mp4"}, null);
}
}
@Override
public void onConfirmSelect(String absolutePath, String fileName, Map<String, Object> map_param) {
Log.d(TAG, "onConfirmSelect absolutePath=" + absolutePath + ". fileName=" + fileName);
String file_path = "";
if (absolutePath != null && fileName != null) {
file_path = absolutePath + "/" + fileName;
}
Toast.makeText(this, "已開啟視訊", Toast.LENGTH_SHORT).show();
VideoView vv_play = new VideoView(this);
vv_play.setVideoPath(file_path);
vv_play.requestFocus();
MediaController mc_play = new MediaController(this);
mc_play.setAnchorView(vv_play);
mc_play.setKeepScreenOn(true);
vv_play.setMediaController(mc_play);
ll_play.addView(vv_play);
vv_play.start();
}
@Override
public boolean isFileValid(String absolutePath, String fileName, Map<String, Object> map_param) {
return true;
}
}
自定義視訊播放器
從上面VideoView和MediaController的播放效果來看,這個簡單播放器存在若干不足,包括:1、控制條分上下兩行,上面是控制按鈕,下面是進度條,高度太寬了;
2、按鈕樣式無法定製,且不能增加和刪除按鈕;
3、進度條與播放時間的樣式也不能定製;
4、播放器的視訊畫面不會自動全屏顯示;
5、播放器沒有實現調大和調小音量;
6、播放器不會自動設定標題和背景;
基於以上情況,我們要想讓視訊播放器生動活潑起來,勢必要自己寫一個既好看又好用的播放器。這裡既要對VideoView視訊檢視進行重寫,也要對控制條MediaController進行重寫。經過進一步的檢視原始碼與深入分析,我們發現播放器的改進主要分為兩個方面,一方面是對視訊畫面做功能方面的增強,另一方面是對控制條做樣式方面的定製,所以VideoView和MediaController的改造方案基本確定如下:
1、增強VideoView的功能,可以派生一個子類出來,重寫尺寸測量方法onMeasure,實現自動全屏;重寫觸控監聽方法onTouch,實現音量的調節;以及補充設定標題和背景的新方法;
2、定製MediaController的樣式,因為它的內部控制元件都是私有的,即使繼承了也無法修改,因此只能自己寫個全新的控制條。好在我們的需求只是更改控制條的樣式,沒有增加複雜的功能,增添幾個指定風格的控制元件想必大家都很熟練了,唯一的難點在於如何跟VideoVie物件同步當前的播放進度。對於視訊畫面向控制條通知播放進度,我們可以通過設定定時器來實現;對於控制條向視訊畫面通知具體操作,我們可以通過點選事件和拖動事件來實現。
如果只是修改程式碼,其實還不能完全實現自動全屏的功能,主要問題如下:
1、螢幕頂部的系統狀態列依然留在螢幕頂端;
2、App自身的導航欄也仍舊沒有隱藏;
3、在視訊播放途中,如果手機螢幕發生切換,例如從豎屏變為橫屏,那麼視訊播放就會停止,回到頁面剛進去的初始狀態;
對於前兩個問題,可通過設定頁面主題來予以調整,如下所示,設定屬性android:windowFullscreen來隱藏系統狀態列,設定屬性android:windowNoTitle來去除App的導航欄:
<style name="FullScreenTheme" parent="AppBaseTheme">
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
對於第三個問題,可通過給activity節點設定屬性android:configChanges來予以解決。因為預設情況下,App每次切換螢幕都會重啟Activity,即先執行原頁面的onDestroy方法,再執行新頁面的onCreate方法,這便導致還在播放當中的視訊被中斷返回了。而屬性configChanges的意思是螢幕切換時不用重啟Activity,只需呼叫onConfigurationChanged方法來重新設定顯示方式,所以給該屬性指定若干事件,就可以避免重啟Activity的操作了。下面是一個設定的xml例子,其中orientation表示豎屏/橫屏切換,keyboardHidden表示鍵盤彈出/隱藏,screenSize表示螢幕大小發生變化。
<activity
android:name=".VideoCustomActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="sensor"
android:theme="@style/FullScreenTheme" >
</activity>
下面是改造之後的視訊播放器介面截圖:
第一張是播放器啟動畫面:
第二張是播放器播放畫面(控制條彈出):
第二張是播放器播放畫面(控制條隱藏):
下面是自定義視訊檢視的程式碼例子:
import com.example.exmvideo.R;
import com.example.exmvideo.util.Utils;
import com.example.exmvideo.util.VolumnManager;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Animation.AnimationListener;
import android.widget.VideoView;
//支援以下功能:自動全屏、調節音量、收縮控制欄、設定背景
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) //setBackground需要
public class CustomVideoView extends VideoView implements OnTouchListener {
private Context mContext;
private AudioManager mAudioManager;
private VolumnManager mVolumnManager;
private int screenWidth;
private int screenHeight;
private int videoWidth;
private int videoHeight;
private int realWidth;
private int realHeight;
private float mLastMotionX;
private float mLastMotionY;
private int startX;
private int startY;
private int threshold;
private boolean isClick = true;
// 自動隱藏頂部和底部View的時間
public static final int HIDE_TIME = 5000;
private View mTopView;
private View mBottomView;
private Handler mHandler = new Handler();
public CustomVideoView(Context context) {
this(context, null);
}
public CustomVideoView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mVolumnManager = new VolumnManager(mContext);
screenWidth = Utils.getWidthInPx(mContext);
screenHeight = Utils.getHeightInPx(mContext);
threshold = Utils.dip2px(mContext, 18);
}
private void volumeDown(float delatY) {
int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int down = (int) (delatY / screenHeight * max * 3);
int volume = Math.max(current - down, 0);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
int transformatVolume = volume * 100 / max;
mVolumnManager.show(transformatVolume);
}
private void volumeUp(float delatY) {
int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int up = (int) ((delatY / screenHeight) * max * 3);
int volume = Math.min(current + up, max);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
int transformatVolume = volume * 100 / max;
mVolumnManager.show(transformatVolume);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(realWidth, widthMeasureSpec);
int height = getDefaultSize(realHeight, heightMeasureSpec);
if (realWidth > 0 && realHeight > 0) {
if (realWidth * height > width * realHeight) {
height = width * realHeight / realWidth;
} else if (realWidth * height < width * realHeight) {
width = height * realWidth / realHeight;
}
}
setMeasuredDimension(width, height);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
startX = (int) x;
startY = (int) y;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastMotionX;
float deltaY = y - mLastMotionY;
float absDeltaX = Math.abs(deltaX);
float absDeltaY = Math.abs(deltaY);
// 聲音調節標識
boolean isAdjustAudio = false;
if (absDeltaX > threshold && absDeltaY > threshold) {
if (absDeltaX < absDeltaY) {
isAdjustAudio = true;
} else {
isAdjustAudio = false;
}
} else if (absDeltaX < threshold && absDeltaY > threshold) {
isAdjustAudio = true;
} else if (absDeltaX > threshold && absDeltaY < threshold) {
isAdjustAudio = false;
} else {
return true;
}
if (isAdjustAudio) {
if (deltaY > 0) {
volumeDown(absDeltaY);
} else if (deltaY < 0) {
volumeUp(absDeltaY);
}
}
mLastMotionX = x;
mLastMotionY = y;
break;
case MotionEvent.ACTION_UP:
if (Math.abs(x - startX) > threshold || Math.abs(y - startY) > threshold) {
isClick = false;
}
mLastMotionX = 0;
mLastMotionY = 0;
startX = (int) 0;
if (isClick) {
showOrHide();
}
isClick = true;
break;
default:
break;
}
return true;
}
public void prepare(View topTiew, View bottomView) {
mTopView = topTiew;
mBottomView = bottomView;
setBackgroundResource(R.drawable.video_bg1);
}
public void begin(MediaPlayer mp) {
setBackground(null);
if (mp != null) {
videoWidth = mp.getVideoWidth();
videoHeight = mp.getVideoHeight();
}
realWidth = videoWidth;
realHeight = videoHeight;
start();
}
public void end(MediaPlayer mp) {
setBackgroundResource(R.drawable.video_bg3);
realWidth = screenWidth;
realHeight = screenHeight;
}
public void showOrHide() {
if (mTopView==null || mBottomView==null) {
return;
}
if (mTopView.getVisibility() == View.VISIBLE) {
mTopView.clearAnimation();
Animation animTop = AnimationUtils.loadAnimation(mContext, R.anim.leave_from_top);
animTop.setAnimationListener(new AnimationImp() {
@Override
public void onAnimationEnd(Animation animation) {
mTopView.setVisibility(View.GONE);
}
});
mTopView.startAnimation(animTop);
mBottomView.clearAnimation();
Animation animBottom = AnimationUtils.loadAnimation(mContext, R.anim.leave_from_bottom);
animBottom.setAnimationListener(new AnimationImp() {
@Override
public void onAnimationEnd(Animation animation) {
mBottomView.setVisibility(View.GONE);
}
});
mBottomView.startAnimation(animBottom);
} else {
mTopView.setVisibility(View.VISIBLE);
mTopView.clearAnimation();
Animation animTop = AnimationUtils.loadAnimation(mContext, R.anim.entry_from_top);
mTopView.startAnimation(animTop);
mBottomView.setVisibility(View.VISIBLE);
mBottomView.clearAnimation();
Animation animBottom = AnimationUtils.loadAnimation(mContext, R.anim.entry_from_bottom);
mBottomView.startAnimation(animBottom);
mHandler.removeCallbacks(hideRunnable);
mHandler.postDelayed(hideRunnable, HIDE_TIME);
}
}
private Runnable hideRunnable = new Runnable() {
@Override
public void run() {
showOrHide();
}
};
private class AnimationImp implements AnimationListener {
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}
}
}
下面是自定義播放控制條的程式碼例子:
import com.example.exmvideo.R;
import com.example.exmvideo.util.Utils;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
public class VideoController extends RelativeLayout implements OnClickListener, OnSeekBarChangeListener {
private static final String TAG = "VideoController";
private Context mContext;
private ImageView mImagePlay;
private TextView mCurrentTime;
private TextView mTotalTime;
private SeekBar mSeekBar;
private int mBeginViewId = 0x7F24FFF0;
private int dip_10, dip_40;
private CustomVideoView mVideoView;
private int mCurrent = 0;
private int mBuffer = 0;
private int mDuration = 0;
private boolean bPause = false;
public VideoController(Context context) {
this(context, null);
}
public VideoController(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
dip_10 = Utils.dip2px(mContext, 10);
dip_40 = Utils.dip2px(mContext, 40);
initView();
}
private TextView newTextView(Context context, int id) {
TextView tv = new TextView(context);
tv.setId(id);
tv.setGravity(Gravity.CENTER);
tv.setTextColor(Color.WHITE);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
params.addRule(RelativeLayout.CENTER_VERTICAL);
tv.setLayoutParams(params);
return tv;
}
private void initView() {
mImagePlay = new ImageView(mContext);
RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(dip_40, dip_40);
imageParams.addRule(RelativeLayout.CENTER_VERTICAL);
mImagePlay.setLayoutParams(imageParams);
mImagePlay.setId(mBeginViewId);
mImagePlay.setOnClickListener(this);
mCurrentTime = newTextView(mContext, mBeginViewId+1);
RelativeLayout.LayoutParams currentParams = (LayoutParams) mCurrentTime.getLayoutParams();
currentParams.setMargins(dip_10, 0, 0, 0);
currentParams.addRule(RelativeLayout.RIGHT_OF, mImagePlay.getId());
mCurrentTime.setLayoutParams(currentParams);
mTotalTime = newTextView(mContext, mBeginViewId+2);
RelativeLayout.LayoutParams totalParams = (LayoutParams) mTotalTime.getLayoutParams();
totalParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mTotalTime.setLayoutParams(totalParams);
mSeekBar = new SeekBar(mContext);
RelativeLayout.LayoutParams seekParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
totalParams.setMargins(dip_10, 0, dip_10, 0);
seekParams.addRule(RelativeLayout.CENTER_IN_PARENT);
seekParams.addRule(RelativeLayout.RIGHT_OF, mCurrentTime.getId());
seekParams.addRule(RelativeLayout.LEFT_OF, mTotalTime.getId());
mSeekBar.setLayoutParams(seekParams);
mSeekBar.setMax(100);
mSeekBar.setMinimumHeight(100);
mSeekBar.setThumbOffset(0);
mSeekBar.setId(mBeginViewId+3);
mSeekBar.setOnSeekBarChangeListener(this);
}
private void reset() {
if (mCurrent == 0 || bPause) {
mImagePlay.setImageResource(R.drawable.video_btn_down);
} else {
mImagePlay.setImageResource(R.drawable.video_btn_on);
}
mCurrentTime.setText(Utils.formatTime(mCurrent));
mTotalTime.setText(Utils.formatTime(mDuration));
mSeekBar.setProgress((mCurrent==0)?0:(mCurrent*100/mDuration));
mSeekBar.setSecondaryProgress(mBuffer);
}
private void refresh() {
invalidate();
requestLayout();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
removeAllViews();
reset();
addView(mImagePlay);
addView(mCurrentTime);
addView(mTotalTime);
addView(mSeekBar);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
int time = progress * mDuration / 100;
mVideoView.seekTo(time);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mSeekListener.onStartSeek();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mSeekListener.onStopSeek();
}
private onSeekChangeListener mSeekListener;
public static interface onSeekChangeListener {
public void onStartSeek();
public void onStopSeek();
}
public void setonSeekChangeListener(onSeekChangeListener listener) {
mSeekListener = listener;
}
@Override
public void onClick(View v) {
if (v.getId() == mImagePlay.getId()) {
if (mVideoView.isPlaying()) {
mVideoView.pause();
bPause = true;
} else {
if (mCurrent == 0) {
mVideoView.begin(null);
}
mVideoView.start();
bPause = false;
}
}
refresh();
}
public void setVideoView(CustomVideoView view) {
mVideoView = view;
mDuration = mVideoView.getDuration();
}
public void setCurrentTime(int current_time, int buffer_time) {
mCurrent = current_time;
mBuffer = buffer_time;
refresh();
}
}
下面是改造之後播放頁面的程式碼例子:
import java.util.Map;
import com.aqi00.lib.dialog.FileSelectFragment;
import com.aqi00.lib.dialog.FileSelectFragment.FileSelectCallbacks;
import com.example.exmvideo.widget.CustomVideoView;
import com.example.exmvideo.widget.VideoController;
import com.example.exmvideo.widget.VideoController.onSeekChangeListener;
import android.app.Activity;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
public class VideoCustomActivity extends Activity implements
OnClickListener, FileSelectCallbacks, onSeekChangeListener {
private static final String TAG = "VideoCustomActivity";
private CustomVideoView fsvv_content;
private TextView tv_open;
private RelativeLayout rl_top;
private VideoController mb_play;
private Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_custom);
fsvv_content = (CustomVideoView) findViewById(R.id.fsvv_content);
mb_play = (VideoController) findViewById(R.id.mb_play);
tv_open = (TextView) findViewById(R.id.tv_open);
rl_top = (RelativeLayout) findViewById(R.id.rl_top);
fsvv_content.prepare(rl_top, mb_play);
tv_open.setOnClickListener(this);
mb_play.setonSeekChangeListener(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
private void playVideo(String video_path) {
fsvv_content.setVideoPath(video_path);
fsvv_content.requestFocus();
fsvv_content.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
fsvv_content.begin(mp);
mb_play.setVideoView(fsvv_content);
mHandler.removeCallbacks(hideRunnable);
mHandler.postDelayed(hideRunnable, CustomVideoView.HIDE_TIME);
mHandler.post(refreshRunnable);
}
});
fsvv_content.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
fsvv_content.end(mp);
mb_play.setCurrentTime(0, 0);
}
});
fsvv_content.setOnTouchListener(fsvv_content);
}
private Runnable hideRunnable = new Runnable() {
@Override
public void run() {
fsvv_content.showOrHide();
}
};
private Runnable refreshRunnable = new Runnable() {
@Override
public void run() {
if (fsvv_content.isPlaying()) {
mb_play.setCurrentTime(fsvv_content.getCurrentPosition(), fsvv_content.getBufferPercentage());
}
mHandler.postDelayed(this, 500);
}
};
@Override
public void onClick(View v) {
int resid = v.getId();
if (resid == R.id.tv_open) {
FileSelectFragment.show(this, new String[]{"mp4"}, null);
}
}
@Override
public void onConfirmSelect(String absolutePath, String fileName, Map<String, Object> map_param) {
Log.d(TAG, "onConfirmSelect absolutePath=" + absolutePath + ". fileName=" + fileName);
String file_path = "";
if (absolutePath != null && fileName != null) {
file_path = absolutePath + "/" + fileName;
}
Toast.makeText(this, "已開啟視訊", Toast.LENGTH_SHORT).show();
playVideo(file_path);
}
@Override
public boolean isFileValid(String absolutePath, String fileName, Map<String, Object> map_param) {
return true;
}
@Override
public void onStartSeek() {
mHandler.removeCallbacks(hideRunnable);
}
@Override
public void onStopSeek() {
mHandler.postDelayed(hideRunnable, CustomVideoView.HIDE_TIME);
}
}
點選下載本文用到的自定義視訊播放器的工程程式碼
點此檢視Android開發筆記的完整目錄
相關文章
- Android進階:自定義視訊播放器開發(上)Android播放器
- Android進階:自定義視訊播放器開發(下)Android播放器
- 自定義視訊播放器播放器
- Android開發筆記(一百二十六)自定義音樂播放器Android筆記播放器
- Android進階:十、自定義視訊播放器 1Android播放器
- android短視訊開發,自定義下拉選單Android
- iOS 基於AVPlayer自定義視訊播放器iOS播放器
- iOS開發 AVFoundation 自定義視訊錄製iOS
- 自定義視訊播放器與慢放滾輪播放器
- Android開發筆記(一百二十四)自定義相簿Android筆記
- Android開發筆記(一百一十八)自定義懸浮窗Android筆記
- 短視訊開發app,webservice自定義加入攔截器APPWeb
- 短視訊開發app,自定義帶進度條的視訊播放按鈕APP
- Android開發之自定義SpinnerAndroid
- Vue + WebRTC 實現音視訊直播(附自定義播放器樣式)VueWeb播放器
- Swift中使用MPMoviePlayerController實現自定義視訊播放器介面SwiftController播放器
- android短視訊開發,自定義更改平臺主題以及狀態列樣式Android
- ios開發筆記--狀態列的自定義,隱藏iOS筆記
- Android開發之自定義View(一)AndroidView
- Android開發之自定義View(二)AndroidView
- iOS開發筆記 | 自定義具有內邊距的labeliOS筆記
- Android開發筆記Android筆記
- Android Studio NDK開發:自定義庫Android
- 網頁播放器開發系列筆記(一)網頁播放器筆記
- 網頁播放器開發系列筆記(二)網頁播放器筆記
- 網頁播放器開發系列筆記(三)網頁播放器筆記
- android 顯示flash視訊播放器Android播放器
- 【html5多媒體】自定義播放器(彈幕,水印,甚至在視訊開頭新增封面)HTML播放器
- Android音視訊開發筆記(二)--ffmpeg命令列的使用&相機預覽Android筆記命令列
- 直播軟體開發,Android自定義簡單的音訊波譜viewAndroid音訊View
- Android開發自定義View之滑動按鈕與自定義屬性AndroidView
- 你是否有一個夢想?用JavaScript[vue.js、react.js......]開發一款自定義配置視訊播放器JavaScriptVue.jsReact播放器
- 短視訊程式開發,簡易的自定義確認彈框AlertDialog
- STREAMS筆記(8) rule - 自定義筆記
- Android開發 - 使用自定義介面在新視窗中傳回資料Android
- 【Android開發點滴】自定義Toast樣式AndroidAST
- Android個人開發筆記Android筆記
- 前端筆記之HTML5&CSS3(上)新特性&音訊視訊&本地儲存&自定義屬性前端筆記HTMLCSSS3音訊