Android開發筆記(一百二十五)自定義視訊播放器

湖前琴亭發表於2016-09-12

視訊播放方式

在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開發筆記的完整目錄

相關文章