VideoView类继承自SurfaceView,我们可以通过重构VideoView类来实现需求。
- public class VideoView extends SurfaceView implements MediaPlayerControl {
- private String TAG = "VideoView";
- // settable by the client
- private Uri mUri;
- private int mDuration;
- // all possible internal states
- private static final int STATE_ERROR = -1;
- private static final int STATE_IDLE = 0;
- private static final int STATE_PREPARING = 1;
- private static final int STATE_PREPARED = 2;
- private static final int STATE_PLAYING = 3;
- private static final int STATE_PAUSED = 4;
- private static final int STATE_PLAYBACK_COMPLETED = 5;
- // mCurrentState is a VideoView object's current state.
- // mTargetState is the state that a method caller intends to reach.
- // For instance, regardless the VideoView object's current state,
- // calling pause() intends to bring the object to a target state
- // of STATE_PAUSED.
- private int mCurrentState = STATE_IDLE;
- private int mTargetState = STATE_IDLE;
- // All the stuff we need for playing and showing a video
- private SurfaceHolder mSurfaceHolder = null;
- private MediaPlayer mMediaPlayer = null;
- private int mVideoWidth;
- private int mVideoHeight;
- private int mSurfaceWidth;
- private int mSurfaceHeight;
- private MediaController mMediaController;
- private OnCompletionListener mOnCompletionListener;
- private MediaPlayer.OnPreparedListener mOnPreparedListener;
- private int mCurrentBufferPercentage;
- private OnErrorListener mOnErrorListener;
- private int mSeekWhenPrepared; // recording the seek position while preparing
- private boolean mCanPause;
- private boolean mCanSeekBack;
- private boolean mCanSeekForward;
- public VideoView(Context context) {
- super(context);
- initVideoView();
- }
- public VideoView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- initVideoView();
- }
- public VideoView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initVideoView();
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //Log.i("@@@@", "onMeasure");
- int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
- int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
- if (mVideoWidth > 0 && mVideoHeight > 0) {
- if ( mVideoWidth * height > width * mVideoHeight ) {
- //Log.i("@@@", "image too tall, correcting");
- height = width * mVideoHeight / mVideoWidth;
- } else if ( mVideoWidth * height < width * mVideoHeight ) {
- //Log.i("@@@", "image too wide, correcting");
- width = height * mVideoWidth / mVideoHeight;
- } else {
- //Log.i("@@@", "aspect ratio is correct: " +
- //width+"/"+height+"="+
- //mVideoWidth+"/"+mVideoHeight);
- }
- }
- //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);
- setMeasuredDimension(width, height);
- }
- public int resolveAdjustedSize(int desiredSize, int measureSpec) {
- int result = desiredSize;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- /* Parent says we can be as big as we want. Just don't be larger
- * than max size imposed on ourselves.
- */
- result = desiredSize;
- break;
- case MeasureSpec.AT_MOST:
- /* Parent says we can be as big as we want, up to specSize.
- * Don't be larger than specSize, and don't be larger than
- * the max size imposed on ourselves.
- */
- result = Math.min(desiredSize, specSize);
- break;
- case MeasureSpec.EXACTLY:
- // No choice. Do what we are told.
- result = specSize;
- break;
- }
- return result;
编写简单自定义VideoView
http://marshal.easymorse.com/archives/3160在简单定制VideoView中做了简单的VideoView定制,其实就是在布局上做了一些事情。要向更灵活的定制播放器的行为,必须写自己的VideoView。参考android VideoView源代码,写了个最简单的实现。
看起来和简单定制VideoView中的效果差不多,但是还有很多逻辑没有加进来,比如:
- 视频大小有问题,被拉长了,需要在后续版本中改进;
- 还没有加入MediaController,没有前进、后退、暂停等按钮界面。
自定义的VideoView源代码:
package com.easymorse.videoplayer;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.widget.MediaController;
import android.widget.MediaController.MediaPlayerControl;public class CustomerVideoView extends SurfaceView implements
MediaPlayerControl {private static String TAG = "customer.videoplayer";
private boolean pause;
private boolean seekBackward;
private boolean seekForward;
private Uri videoUri;
private MediaPlayer mediaPlayer;
private Context context;
private OnPreparedListener onPreparedListener;
private int videoWidth;
private int videoHeight;
private MediaController mediaController;
protected SurfaceHolder surfaceHolder;
private Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
public void surfaceChanged(SurfaceHolder holder, int format, int w,
int h) {
}public void surfaceCreated(SurfaceHolder holder) {
surfaceHolder = holder;
if (mediaPlayer != null) {
mediaPlayer.setDisplay(surfaceHolder);
resume();
} else {
openVideo();
}
}public void surfaceDestroyed(SurfaceHolder holder) {
surfaceHolder = null;
if (mediaController != null) {
mediaController.hide();
}release(true);
}
};private void release(boolean cleartargetstate) {
if (mediaPlayer != null) {
mediaPlayer.reset();
mediaPlayer.release();
mediaPlayer = null;
}
}public void resume() {
if (surfaceHolder == null) {
return;
}
if (mediaPlayer != null) {
return;
}
openVideo();
}public CustomerVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.context = context;
this.initVideoView();
}public CustomerVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.initVideoView();
}public CustomerVideoView(Context context) {
super(context);
this.context = context;
this.initVideoView();
}@Override
public boolean canPause() {
return this.pause;
}@Override
public boolean canSeekBackward() {
return this.seekBackward;
}@Override
public boolean canSeekForward() {
return this.seekForward;
}@Override
public int getBufferPercentage() {
return 0;
}@Override
public int getCurrentPosition() {
return mediaPlayer!=null?mediaPlayer.getCurrentPosition():0;
}@Override
public int getDuration() {
return mediaPlayer!=null?mediaPlayer.getDuration():0;
}@Override
public boolean isPlaying() {
return false;
}@Override
public void pause() {
}@Override
public void seekTo(int mSec) {
}@Override
public void start() {
}public void setVideoURI(Uri uri) {
this.videoUri = uri;
openVideo();
requestLayout();
invalidate();
}private void openVideo() {
this.mediaPlayer = new MediaPlayer();
try {
this.mediaPlayer.setDataSource(this.context, this.videoUri);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
throw new RuntimeException(e);
}
this.mediaPlayer.prepareAsync();
this.mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
this.mediaPlayer.setOnPreparedListener(onPreparedListener);
attachMediaController();}
private void attachMediaController() {
if (mediaPlayer != null && mediaController != null) {
mediaController.setMediaPlayer(this);
View anchorView = this.getParent() instanceof View ? (View) this
.getParent() : this;
mediaController.setAnchorView(anchorView);
mediaController.setEnabled(true);
}}
public void setMediaController(MediaController controller) {
if (mediaController != null) {
mediaController.hide();
}
mediaController = controller;
attachMediaController();
}public void setOnPreparedListener(OnPreparedListener onPreparedListener) {
this.onPreparedListener = onPreparedListener;
}@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(videoWidth, widthMeasureSpec);
int height = getDefaultSize(videoHeight, heightMeasureSpec);
if (videoWidth > 0 && videoHeight > 0) {
if (videoWidth * height > width * videoHeight) {
height = width * videoHeight / videoWidth;
} else if (videoWidth * height < width * videoHeight) {
width = height * videoWidth / videoHeight;
}
}
Log.i(TAG, "setting size: " + width + ‘x’ + height);
setMeasuredDimension(width, height);
}private void initVideoView() {
videoWidth = 0;
videoHeight = 0;
getHolder().addCallback(surfaceHolderCallback);
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
}}
和VideoView实现类似,继承了SurfaceView并且实现了MediaPlayerControl。
一般情况下,android界面的绘制和更新,要交给主ui线程来操作,通过Handler机制。但是播放视频,需要比较优先和实时的改变和绘制界面。android提供了使用单独线程绘制UI的机制,就是SurfaceView。
使用SurfaceView,需要实现SurfaceHolder.Callback接口:
- surfaceCreated,在Surface(SurfaceView内部包含一个Surface实例)创建后,会立即调用该方法,可在该方法中做绘制界面相关的初始化工作;
- surfaceChanged,当Surface的状态发生变化,比如大小,会调用该方法,在surfaceCreated方法调用过至少会调用一次该方法;
- surfaceDestroyed,当销毁Surface的时候调用。
开发者不能直接操作Surface实例,要通过SurfaceHandler,在SurfaceView中可以通过getHandler方法获取到SurfaceHandler实例。
SurfaceHander有一些类型,用来标识Surface实例界面数据来源,可以通过setType来操作:
- SURFACE_TYPE_NORMAL:RAM缓存的原生数据
- SURFACE_TYPE_HARDWARE:通过DMA,direct memory access,就是直接写屏技术获取到的数据,或者其他硬件加速的数据
- SURFACE_TYPE_GPU:通过GPU加速的数据
- SURFACE_TYPE_PUSH_BUFFERS:标识数据来源于其他对象,比如照相机,比如视频播放服务器(android内部有视频播放的服务器,所有播放视频相当于客户端)
CustomerVideoView的构造方法,使用超类的构造方法。都会执行initVideoView()方法用来初始化界面和参数。
另外一个主要的内容是openVideo()方法:
- mediaPlayer.prepareAsync(),用来异步准备播放,另外还有个prepare()方法,是同步的,也就是全部下载完毕才能播放,显然,在播放网上视频的时候需要用前者;
- 通过attachMediaController()方法,把控制条附加到播放视频的SurfaceView上,这里实现的不完全,因此还不能使用,仅仅是把MediaPlayerControl实例通过setMediaPlayer方法设置一下,供OnPreparedListener用来得到加载成功的回调,另外供外面代码调用得到视频的时长和当前时长。
源代码见:
自定义VideoView的演进
在编写简单自定义VideoView中尝试编写自己的VideoView实现类。这样对VideoView的实现机制有了一个比较深入的理解。经过整理发现,其实要自定义需求,还真不一定需要重新自己的VideoView实现。在本文中将原来的CustomerVideoView的方法全部删除,并继承VideoView,发现功能上没有什么不能实现的。继承的CustomerVideoView最后其实只剩下继承来的构造方法,也就是说直接使用VideoView也没问题。
这次演进,实现了自定义的播放控制条:
这个播放控制条,实际是替代了编写简单自定义VideoView中的MediaController。这样就可以自定义各种样式和风格的控制条界面了。
另外,本文示例中默认不出现播放控制条,当识别到横向手势的时候,才显示该滚动条,并且根据横向手势的x轴位移前进或者后退播放视频的位置。
如果手指单击视频,则停止播放,再单击继续播放。
CustomerVideoView已经简化到可以直接使用VideoView替代:
package com.easymorse.videoplayer;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.VideoView;public class CustomerVideoView extends VideoView {
private static String TAG = "customer.videoplayer";
public CustomerVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}public CustomerVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
}public CustomerVideoView(Context context) {
super(context);
}}
保留自定义CustomerVideoView的目的是:
- 以后可以通过覆盖方法增加更加个性化的功能;
- 为什么要继承VideoView,是否能直接写自己的VideoView实现?这是一个问题,当然是可行的,比如mVideoPlayer就是这么干的,但是再查看VideoView源代码的时候,发现一些在Android源代码中公开的(public)类,在android sdk api中并没有,比如找不到android.media.Metadata类,调用MediaPlayer的时候也无法找到源代码中公开的(public)方法getMetadata方法,因此直接使用继承可以获得很多好处,间接使用未公开的api。
有关播放控制条的布局:
<RelativeLayout android:id="@+id/mediaControllerLayout"
android:layout_height="55dip" android:layout_width="fill_parent"
android:visibility="invisible" android:layout_alignParentBottom="true">
<View android:background="#50878787" android:layout_width="fill_parent"
android:layout_height="1dip" />
<ImageButton android:id="@+id/playButton"
android:layout_width="wrap_content" android:src="@drawable/pause_button_gray"
android:layout_height="wrap_content" android:layout_marginRight="15.0dip"
android:layout_alignParentRight="true"
android:layout_centerVertical="true" />
<RelativeLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_marginLeft="15.0dip"
android:layout_marginRight="15.0dip" android:layout_centerVertical="true"
android:layout_toLeftOf="@id/playButton">
<SeekBar android:id="@+id/videoSeekBar" android:focusable="false"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_marginBottom="5.0dip"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</RelativeLayout>
在这里还能加入其他需要的信息,比如当前播放时间,总时间等等。SeekBar是使用默认样式的,可以指定自己的样式和thumb小图标。
为了实现横向手势指定播放进度功能,需要让activity实现OnGestureListener接口,以前写过一个简单的示例,编写android简单的手势切换视图示例,可先参考了解(见附录)。
在OnGestureListener中主要实现了以下方法。
onFling:
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
mediaControllerLayout.setVisibility(View.VISIBLE);
handler.postDelayed(new Runnable() {
@Override
public void run() {
mediaControllerLayout.setVisibility(View.INVISIBLE);
}
}, 1000);
return false;
}
其实这个方法中的内容也可以实现在下面提到的onScroll方法中。onFling主要是处理有有关速度横向和纵向的手势。这里用来触发显示播放控制条界面。并且通过postDelayed方法在1秒钟后让控制条不可见。这里还可以改进,用动画来处理出现和消失的效果。
onSingleTapUp:
@Override
public boolean onSingleTapUp(MotionEvent e) {
Toast.makeText(this, "taped", Toast.LENGTH_SHORT).show();
if (videoView.isPlaying()) {
videoView.pause();
} else {
videoView.start();
}
return false;
}
用来识别单击屏幕的手势,并做视频的暂停和继续播放。
onScroll:
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if (distanceX > 0) {
this.videoSeekBar
.setProgress(this.videoSeekBar.getProgress() – 1);
} else {
this.videoSeekBar
.setProgress(this.videoSeekBar.getProgress() + 1);
}
videoView.seekTo((int) (this.videoSeekBar.getProgress() * 1.0
/ videoSeekBar.getMax() * videoView.getDuration()));
return false;
}
识别手势横向左右位移,这里实现的很简单,进度条根据动作固定的为进度条加1%或者减1%。视频再根据滚动条做调整。正式的代码,应该根据位移的长度来适当的定位滚动条的位置。
手势的监听器要实现,还需要两件事情。
实例化GestureDetector:
private GestureDetector gestureDetector;
this.gestureDetector = new GestureDetector(this);
覆盖Activity的onTouchEvent方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
return this.gestureDetector.onTouchEvent(event);
}
这样才会把触摸事件转到手势监听器。
另外,也可以手动拨动滚动条上的thumb,需要通过下面方法支持:
@Override
public void onProgressChanged(SeekBar seekBar,
int progress, boolean fromUser) {
if (fromUser) {
videoView.seekTo((int) (progress * 1.0
/ seekBar.getMax() * videoView
.getDuration()));
seekBar.setProgress(progress);
}
}
源代码见:
进一步可以做的是:
- 在播放器中保存用户播放视频中止时的播放位置,再次播放的时候从中止的位置播放;
- 设置menu项,用户可以清除保存的位置,这样可以从头播放;
- 屏幕的适配,目前是按照视频原生大小播放,可以增加拉伸适配全屏功能,并通过menu项切换;
- 视频的横屏和竖屏切花;
- 设置intent,这样可以在用户播放视频的时候,可以出现应用列表,用户可以选择android内置播放器以外的应用播放;
- 做一个比较好的动画,在单击暂停的时候播放。
编写android简单的手势切换视图示例
android的home screen,可以通过手指的向左拖动和向右拖动,切换屏幕视图。
这样做的好处是用户体验比较好,比向下滚屏或者使用tab切换视图。
在自己的代码中要用到这个动作效果。这里需要用到:
android.widget.ViewFlipper
ViewFlipper是一种Layout,可以在xml中声明。我的例子只做到:
两个视图,手势左右移动,两个视图切换。
xml文件中的内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="@color/back_ground">
<ViewFlipper android:id="@+id/ViewFlipper01"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<LinearLayout android:id="@+id/LinearLayout01"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<TextView android:text="第1屏" android:id="@+id/TextView01"
android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
</LinearLayout>
<LinearLayout android:id="@+id/LinearLayout02"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<TextView android:text="第2屏" android:id="@+id/TextView02"
android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
</LinearLayout>
</ViewFlipper>
</LinearLayout>
代码也不复杂,需要:
- 实现OnGestureListener接口,在onFling方法中判断手势的左右移动并给出相应的动作;
- 创建一个GestureDetector实例,把实现的OnGestureListener实例通过构造方法赋值给它;
- 覆盖Activity的onTouchEvent方法,在方法内部,调用GestureDetector实例的onTouchEvent方法。
代码:
package com.easymorse;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.GestureDetector.OnGestureListener;
import android.widget.ViewFlipper;public class MainActivity extends Activity implements
OnGestureListener {private ViewFlipper flipper;
private GestureDetector detector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
detector = new GestureDetector(this);
flipper = (ViewFlipper) this.findViewById(R.id.ViewFlipper01);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("golf", "touched");
return this.detector.onTouchEvent(event);
}@Override
public boolean onDown(MotionEvent e) {
return false;
}@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
Log.i("golf", "fling…");
if (e1.getX() > e2.getX()) {
this.flipper.showNext();
} else if (e1.getX() < e2.getX()) {
this.flipper.showPrevious();
} else {
return false;
}
return true;
}@Override
public void onLongPress(MotionEvent e) {
}@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
return false;
}@Override
public void onShowPress(MotionEvent e) {
}@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
}