Android游戏开发之旅九 VideoView类剖析

有关SurfaceView相关的内容今天Android123继续延用系统的示例类VideoView来让大家深入了解Android平台的图形绘制基础类的实现原理。大家可能会发现VideoView类的控制方面无法改变,我们可以通过重构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;

}

private void initVideoView() {
    mVideoWidth = 0;
    mVideoHeight = 0;
    getHolder().addCallback(mSHCallback);
    getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    setFocusable(true);
    setFocusableInTouchMode(true);
    requestFocus();
    mCurrentState = STATE_IDLE;
    mTargetState  = STATE_IDLE;
}

public void setVideoPath(String path) {
    setVideoURI(Uri.parse(path));
}

public void setVideoURI(Uri uri) {
    mUri = uri;
    mSeekWhenPrepared = 0;
    openVideo();
    requestLayout();
    invalidate();
}

public void stopPlayback() {
    if (mMediaPlayer != null) {
        mMediaPlayer.stop();
        mMediaPlayer.release();
        mMediaPlayer = null;
        mCurrentState = STATE_IDLE;
        mTargetState  = STATE_IDLE;
    }
}

private void openVideo() {
    if (mUri == null || mSurfaceHolder == null) {
        // not ready for playback just yet, will try again later
        return;
    }
    // Tell the music playback service to pause 
    // TODO: these constants need to be published somewhere in the framework.
    Intent i = new Intent("com.android.music.musicservicecommand");
    i.putExtra("command", "pause");
    mContext.sendBroadcast(i);

    // we shouldn't clear the target state, because somebody might have
    // called start() previously
    release(false);
    try {
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnPreparedListener(mPreparedListener);
        mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
        mDuration = -1;
        mMediaPlayer.setOnCompletionListener(mCompletionListener);
        mMediaPlayer.setOnErrorListener(mErrorListener);
        mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
        mCurrentBufferPercentage = 0;
        mMediaPlayer.setDataSource(mContext, mUri);
        mMediaPlayer.setDisplay(mSurfaceHolder);
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);
        mMediaPlayer.prepareAsync();
        // we don't set the target state here either, but preserve the
        // target state that was there before.
        mCurrentState = STATE_PREPARING;
        attachMediaController();
    } catch (IOException ex) {
        Log.w(TAG, "Unable to open content: " + mUri, ex);
        mCurrentState = STATE_ERROR;
        mTargetState = STATE_ERROR;
        mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        return;
    } catch (IllegalArgumentException ex) {
        Log.w(TAG, "Unable to open content: " + mUri, ex);
        mCurrentState = STATE_ERROR;
        mTargetState = STATE_ERROR;
        mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        return;
    }
}

public void setMediaController(MediaController controller) {
    if (mMediaController != null) {
        mMediaController.hide();
    }
    mMediaController = controller;
    attachMediaController();
}

private void attachMediaController() {
    if (mMediaPlayer != null && mMediaController != null) {
        mMediaController.setMediaPlayer(this);
        View anchorView = this.getParent() instanceof View ?
                (View)this.getParent() : this;
        mMediaController.setAnchorView(anchorView);
        mMediaController.setEnabled(isInPlaybackState());
    }
}

MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
    new MediaPlayer.OnVideoSizeChangedListener() {
        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
            mVideoWidth = mp.getVideoWidth();
            mVideoHeight = mp.getVideoHeight();
            if (mVideoWidth != 0 && mVideoHeight != 0) {
                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
            }
        }
};

MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
    public void onPrepared(MediaPlayer mp) {
        mCurrentState = STATE_PREPARED;

        // Get the capabilities of the player for this stream
        Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
                                  MediaPlayer.BYPASS_METADATA_FILTER);

        if (data != null) {
            mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
                    || data.getBoolean(Metadata.PAUSE_AVAILABLE);
            mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
                    || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
            mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
                    || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
        } else {
            mCanPause = mCanSeekForward = mCanSeekForward = true;
        }

        if (mOnPreparedListener != null) {
            mOnPreparedListener.onPrepared(mMediaPlayer);
        }
        if (mMediaController != null) {
            mMediaController.setEnabled(true);
        }
        mVideoWidth = mp.getVideoWidth();
        mVideoHeight = mp.getVideoHeight();

        int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
        if (seekToPosition != 0) {
            seekTo(seekToPosition);
        }
        if (mVideoWidth != 0 && mVideoHeight != 0) {
            //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
            getHolder().setFixedSize(mVideoWidth, mVideoHeight);
            if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
                // We didn't actually change the size (it was already at the size
                // we need), so we won't get a "surface changed" callback, so
                // start the video here instead of in the callback.
                if (mTargetState == STATE_PLAYING) {
                    start();
                    if (mMediaController != null) {
                        mMediaController.show();
                    }
                } else if (!isPlaying() &&
                           (seekToPosition != 0 || getCurrentPosition() > 0)) {
                   if (mMediaController != null) {
                       // Show the media controls when we're paused into a video and make 'em stick.
                       mMediaController.show(0);
                   }
               }
            }
        } else {
            // We don't know the video size yet, but should start anyway.
            // The video size might be reported to us later.
            if (mTargetState == STATE_PLAYING) {
                start();
            }
        }
    }
};

private MediaPlayer.OnCompletionListener mCompletionListener =
    new MediaPlayer.OnCompletionListener() {
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_PLAYBACK_COMPLETED;
        mTargetState = STATE_PLAYBACK_COMPLETED;
        if (mMediaController != null) {
            mMediaController.hide();
        }
        if (mOnCompletionListener != null) {
            mOnCompletionListener.onCompletion(mMediaPlayer);
        }
    }
};

private MediaPlayer.OnErrorListener mErrorListener =
    new MediaPlayer.OnErrorListener() {
    public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
        Log.d(TAG, "Error: " + framework_err + "," + impl_err);
        mCurrentState = STATE_ERROR;
        mTargetState = STATE_ERROR;
        if (mMediaController != null) {
            mMediaController.hide();
        }

        /* If an error handler has been supplied, use it and finish. */
        if (mOnErrorListener != null) {
            if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
                return true;
            }
        }

        /* Otherwise, pop up an error dialog so the user knows that
         * something bad has happened. Only try and pop up the dialog
         * if we're attached to a window. When we're going away and no
         * longer have a window, don't bother showing the user an error.
         */
        if (getWindowToken() != null) {
            Resources r = mContext.getResources();
            int messageId;

            if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
                messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
            } else {
                messageId = com.android.internal.R.string.VideoView_error_text_unknown;
            }

            new AlertDialog.Builder(mContext)
                    .setTitle(com.android.internal.R.string.VideoView_error_title)
                    .setMessage(messageId)
                    .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int whichButton) {
                                    /* If we get here, there is no onError listener, so
                                     * at least inform them that the video is over.
                                     */
                                    if (mOnCompletionListener != null) {
                                        mOnCompletionListener.onCompletion(mMediaPlayer);
                                    }
                                }
                            })
                    .setCancelable(false)
                    .show();
        }
        return true;
    }
};

private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
    new MediaPlayer.OnBufferingUpdateListener() {
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        mCurrentBufferPercentage = percent;
    }
};

/**
 * Register a callback to be invoked when the media file
 * is loaded and ready to go.
 *
 * @param l The callback that will be run
 */
public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
{
    mOnPreparedListener = l;
}

/**
 * Register a callback to be invoked when the end of a media file
 * has been reached during playback.
 *
 * @param l The callback that will be run
 */
public void setOnCompletionListener(OnCompletionListener l)
{
    mOnCompletionListener = l;
}

/**
 * Register a callback to be invoked when an error occurs
 * during playback or setup.  If no listener is specified,
 * or if the listener returned false, VideoView will inform
 * the user of any errors.
 *
 * @param l The callback that will be run
 */
public void setOnErrorListener(OnErrorListener l)
{
    mOnErrorListener = l;
}

SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
{
    public void surfaceChanged(SurfaceHolder holder, int format,
                                int w, int h)
    {
        mSurfaceWidth = w;
        mSurfaceHeight = h;
        boolean isValidState =  (mTargetState == STATE_PLAYING);
        boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
        if (mMediaPlayer != null && isValidState && hasValidSize) {
            if (mSeekWhenPrepared != 0) {
                seekTo(mSeekWhenPrepared);
            }
            start();
            if (mMediaController != null) {
                mMediaController.show();
            }
        }
    }

    public void surfaceCreated(SurfaceHolder holder)
    {
        mSurfaceHolder = holder;
        openVideo();
    }

    public void surfaceDestroyed(SurfaceHolder holder)
    {
        // after we return from this we can't use the surface any more
        mSurfaceHolder = null;
        if (mMediaController != null) mMediaController.hide();
        release(true);
    }
};

/*
 * release the media player in any state
 */
private void release(boolean cleartargetstate) {
    if (mMediaPlayer != null) {
        mMediaPlayer.reset();
        mMediaPlayer.release();
        mMediaPlayer = null;
        mCurrentState = STATE_IDLE;
        if (cleartargetstate) {
            mTargetState  = STATE_IDLE;
        }
    }
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (isInPlaybackState() && mMediaController != null) {
        toggleMediaControlsVisiblity();
    }
    return false;
}

@Override
public boolean onTrackballEvent(MotionEvent ev) {
    if (isInPlaybackState() && mMediaController != null) {
        toggleMediaControlsVisiblity();
    }
    return false;
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
    boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
                                 keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
                                 keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
                                 keyCode != KeyEvent.KEYCODE_MENU &&
                                 keyCode != KeyEvent.KEYCODE_CALL &&
                                 keyCode != KeyEvent.KEYCODE_ENDCALL;
    if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
                keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
            if (mMediaPlayer.isPlaying()) {
                pause();
                mMediaController.show();
            } else {
                start();
                mMediaController.hide();
            }
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 
                && mMediaPlayer.isPlaying()) {
            pause();
            mMediaController.show();
        } else {
            toggleMediaControlsVisiblity();
        }
    }

    return super.onKeyDown(keyCode, event);
}

private void toggleMediaControlsVisiblity() {
    if (mMediaController.isShowing()) { 
        mMediaController.hide();
    } else {
        mMediaController.show();
    }
}

public void start() {
    if (isInPlaybackState()) {
        mMediaPlayer.start();
        mCurrentState = STATE_PLAYING;
    }
    mTargetState = STATE_PLAYING;
}

public void pause() {
    if (isInPlaybackState()) {
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
            mCurrentState = STATE_PAUSED;
        }
    }
    mTargetState = STATE_PAUSED;
}

// cache duration as mDuration for faster access
public int getDuration() {
    if (isInPlaybackState()) {
        if (mDuration > 0) {
            return mDuration;
        }
        mDuration = mMediaPlayer.getDuration();
        return mDuration;
    }
    mDuration = -1;
    return mDuration;
}

public int getCurrentPosition() {
    if (isInPlaybackState()) {
        return mMediaPlayer.getCurrentPosition();
    }
    return 0;
}

public void seekTo(int msec) {
    if (isInPlaybackState()) {
        mMediaPlayer.seekTo(msec);
        mSeekWhenPrepared = 0;
    } else {
        mSeekWhenPrepared = msec;
    }
}    
        
public boolean isPlaying() {
    return isInPlaybackState() && mMediaPlayer.isPlaying();
}

public int getBufferPercentage() {
    if (mMediaPlayer != null) {
        return mCurrentBufferPercentage;
    }
    return 0;
}

private boolean isInPlaybackState() {
    return (mMediaPlayer != null &&
            mCurrentState != STATE_ERROR &&
            mCurrentState != STATE_IDLE &&
            mCurrentState != STATE_PREPARING);
}

public boolean canPause() {
    return mCanPause;
}

public boolean canSeekBackward() {
    return mCanSeekBack;
}

public boolean canSeekForward() {
    return mCanSeekForward;
}