VideoView笔记

VideoView类继承自SurfaceView,我们可以通过重构VideoView类来实现需求。

  1. public class VideoView extends SurfaceView implements MediaPlayerControl {  
  2.   private String TAG = "VideoView";  
  3.   // settable by the client  
  4.   private Uri        mUri;  
  5.   private int        mDuration;  
  6.   // all possible internal states  
  7.   private static final int STATE_ERROR              = -1;  
  8.   private static final int STATE_IDLE              = 0;  
  9.   private static final int STATE_PREPARING          = 1;  
  10.   private static final int STATE_PREPARED          = 2;  
  11.   private static final int STATE_PLAYING            = 3;  
  12.   private static final int STATE_PAUSED            = 4;  
  13.   private static final int STATE_PLAYBACK_COMPLETED = 5;  
  14.   // mCurrentState is a VideoView object's current state.  
  15.   // mTargetState is the state that a method caller intends to reach.  
  16.   // For instance, regardless the VideoView object's current state,  
  17.   // calling pause() intends to bring the object to a target state  
  18.   // of STATE_PAUSED.  
  19.   private int mCurrentState = STATE_IDLE;  
  20.   private int mTargetState  = STATE_IDLE;  
  21.   // All the stuff we need for playing and showing a video  
  22.   private SurfaceHolder mSurfaceHolder = null;  
  23.   private MediaPlayer mMediaPlayer = null;  
  24.   private int        mVideoWidth;  
  25.   private int        mVideoHeight;  
  26.   private int        mSurfaceWidth;  
  27.   private int        mSurfaceHeight;  
  28.   private MediaController mMediaController;  
  29.   private OnCompletionListener mOnCompletionListener;  
  30.   private MediaPlayer.OnPreparedListener mOnPreparedListener;  
  31.   private int        mCurrentBufferPercentage;  
  32.   private OnErrorListener mOnErrorListener;  
  33.   private int        mSeekWhenPrepared;  // recording the seek position while preparing  
  34.   private boolean    mCanPause;  
  35.   private boolean    mCanSeekBack;  
  36.   private boolean    mCanSeekForward;  
  37.   public VideoView(Context context) {  
  38.       super(context);  
  39.       initVideoView();  
  40.   }  
  41.   
  42.   public VideoView(Context context, AttributeSet attrs) {  
  43.       this(context, attrs, 0);  
  44.       initVideoView();  
  45.   }  
  46.   
  47.   public VideoView(Context context, AttributeSet attrs, int defStyle) {  
  48.       super(context, attrs, defStyle);  
  49.       initVideoView();  
  50.   }  
  51.   @Override  
  52.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  53.       //Log.i("@@@@", "onMeasure");  
  54.       int width = getDefaultSize(mVideoWidth, widthMeasureSpec);  
  55.       int height = getDefaultSize(mVideoHeight, heightMeasureSpec);  
  56.       if (mVideoWidth > 0 && mVideoHeight > 0) {  
  57.           if ( mVideoWidth * height  > width * mVideoHeight ) {  
  58.               //Log.i("@@@", "image too tall, correcting");  
  59.               height = width * mVideoHeight / mVideoWidth;  
  60.           } else if ( mVideoWidth * height  < width * mVideoHeight ) {  
  61.               //Log.i("@@@", "image too wide, correcting");  
  62.               width = height * mVideoWidth / mVideoHeight;  
  63.           } else {  
  64.               //Log.i("@@@", "aspect ratio is correct: " +  
  65.                       //width+"/"+height+"="+  
  66.                       //mVideoWidth+"/"+mVideoHeight);  
  67.           }  
  68.       }  
  69.       //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);  
  70.       setMeasuredDimension(width, height);  
  71.   }  
  72.   
  73.   public int resolveAdjustedSize(int desiredSize, int measureSpec) {  
  74.       int result = desiredSize;  
  75.       int specMode = MeasureSpec.getMode(measureSpec);  
  76.       int specSize =  MeasureSpec.getSize(measureSpec);  
  77.       switch (specMode) {  
  78.           case MeasureSpec.UNSPECIFIED:  
  79.               /* Parent says we can be as big as we want. Just don't be larger 
  80.               * than max size imposed on ourselves. 
  81.               */  
  82.               result = desiredSize;  
  83.               break;  
  84.           case MeasureSpec.AT_MOST:  
  85.               /* Parent says we can be as big as we want, up to specSize. 
  86.               * Don't be larger than specSize, and don't be larger than 
  87.               * the max size imposed on ourselves. 
  88.               */  
  89.               result = Math.min(desiredSize, specSize);  
  90.               break;  
  91.               
  92.           case MeasureSpec.EXACTLY:  
  93.               // No choice. Do what we are told.  
  94.               result = specSize;  
  95.               break;  
  96.       }  
  97.       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用来得到加载成功的回调,另外供外面代码调用得到视频的时长和当前时长。

源代码见:

http://easymorse.googlecode.com/svn/tags/videoplayer-0.4/


自定义VideoView的演进

编写简单自定义VideoView中尝试编写自己的VideoView实现类。这样对VideoView的实现机制有了一个比较深入的理解。经过整理发现,其实要自定义需求,还真不一定需要重新自己的VideoView实现。在本文中将原来的CustomerVideoView的方法全部删除,并继承VideoView,发现功能上没有什么不能实现的。继承的CustomerVideoView最后其实只剩下继承来的构造方法,也就是说直接使用VideoView也没问题。

这次演进,实现了自定义的播放控制条:

image

这个播放控制条,实际是替代了编写简单自定义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); 
    } 
}

 

源代码见:

http://easymorse.googlecode.com/svn/tags/videoplayer-0.5/

进一步可以做的是:

  • 在播放器中保存用户播放视频中止时的播放位置,再次播放的时候从中止的位置播放;
  • 设置menu项,用户可以清除保存的位置,这样可以从头播放;
  • 屏幕的适配,目前是按照视频原生大小播放,可以增加拉伸适配全屏功能,并通过menu项切换;
  • 视频的横屏和竖屏切花;
  • 设置intent,这样可以在用户播放视频的时候,可以出现应用列表,用户可以选择android内置播放器以外的应用播放;
  • 做一个比较好的动画,在单击暂停的时候播放。
附录:

编写android简单的手势切换视图示例

android的home screen,可以通过手指的向左拖动和向右拖动,切换屏幕视图。

这样做的好处是用户体验比较好,比向下滚屏或者使用tab切换视图。

在自己的代码中要用到这个动作效果。这里需要用到:

android.widget.ViewFlipper

ViewFlipper是一种Layout,可以在xml中声明。我的例子只做到:

image

两个视图,手势左右移动,两个视图切换。

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; 
    } 
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值