| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.pictureinpicture.widget; |
| |
| import android.content.Context; |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.TypedArray; |
| import android.graphics.Color; |
| import android.media.MediaPlayer; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.RawRes; |
| import android.transition.TransitionManager; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.Surface; |
| import android.view.SurfaceHolder; |
| import android.view.SurfaceView; |
| import android.view.View; |
| import android.widget.ImageButton; |
| import android.widget.RelativeLayout; |
| |
| import com.example.android.pictureinpicture.R; |
| |
| import java.io.IOException; |
| import java.lang.ref.WeakReference; |
| |
| /** |
| * Provides video playback. There is nothing directly related to Picture-in-Picture here. |
| * |
| * <p>This is similar to {@link android.widget.VideoView}, but it comes with a custom control |
| * (play/pause, fast forward, and fast rewind). |
| */ |
| public class MovieView extends RelativeLayout { |
| |
| /** Monitors all events related to {@link MovieView}. */ |
| public abstract static class MovieListener { |
| |
| /** Called when the video is started or resumed. */ |
| public void onMovieStarted() {} |
| |
| /** Called when the video is paused or finished. */ |
| public void onMovieStopped() {} |
| |
| /** Called when this view should be minimized. */ |
| public void onMovieMinimized() {} |
| } |
| |
| private static final String TAG = "MovieView"; |
| |
| /** The amount of time we are stepping forward or backward for fast-forward and fast-rewind. */ |
| private static final int FAST_FORWARD_REWIND_INTERVAL = 5000; // ms |
| |
| /** The amount of time until we fade out the controls. */ |
| private static final int TIMEOUT_CONTROLS = 3000; // ms |
| |
| /** Shows the video playback. */ |
| private final SurfaceView mSurfaceView; |
| |
| // Controls |
| private final ImageButton mToggle; |
| private final View mShade; |
| private final ImageButton mFastForward; |
| private final ImageButton mFastRewind; |
| private final ImageButton mMinimize; |
| |
| /** This plays the video. This will be null when no video is set. */ |
| MediaPlayer mMediaPlayer; |
| |
| /** The resource ID for the video to play. */ |
| @RawRes private int mVideoResourceId; |
| |
| /** Whether we adjust our view bounds or we fill the remaining area with black bars */ |
| private boolean mAdjustViewBounds; |
| |
| /** Handles timeout for media controls. */ |
| TimeoutHandler mTimeoutHandler; |
| |
| /** The listener for all the events we publish. */ |
| MovieListener mMovieListener; |
| |
| private int mSavedCurrentPosition; |
| |
| public MovieView(Context context) { |
| this(context, null); |
| } |
| |
| public MovieView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public MovieView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| setBackgroundColor(Color.BLACK); |
| |
| // Inflate the content |
| inflate(context, R.layout.view_movie, this); |
| mSurfaceView = findViewById(R.id.surface); |
| mShade = findViewById(R.id.shade); |
| mToggle = findViewById(R.id.toggle); |
| mFastForward = findViewById(R.id.fast_forward); |
| mFastRewind = findViewById(R.id.fast_rewind); |
| mMinimize = findViewById(R.id.minimize); |
| |
| // Attributes |
| final TypedArray a = |
| context.obtainStyledAttributes( |
| attrs, |
| R.styleable.MovieView, |
| defStyleAttr, |
| R.style.Widget_PictureInPicture_MovieView); |
| setVideoResourceId(a.getResourceId(R.styleable.MovieView_android_src, 0)); |
| setAdjustViewBounds(a.getBoolean(R.styleable.MovieView_android_adjustViewBounds, false)); |
| a.recycle(); |
| |
| // Bind view events |
| final OnClickListener listener = |
| new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| switch (view.getId()) { |
| case R.id.surface: |
| toggleControls(); |
| break; |
| case R.id.toggle: |
| toggle(); |
| break; |
| case R.id.fast_forward: |
| fastForward(); |
| break; |
| case R.id.fast_rewind: |
| fastRewind(); |
| break; |
| case R.id.minimize: |
| if (mMovieListener != null) { |
| mMovieListener.onMovieMinimized(); |
| } |
| break; |
| } |
| // Start or reset the timeout to hide controls |
| if (mMediaPlayer != null) { |
| if (mTimeoutHandler == null) { |
| mTimeoutHandler = new TimeoutHandler(MovieView.this); |
| } |
| mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS); |
| if (mMediaPlayer.isPlaying()) { |
| mTimeoutHandler.sendEmptyMessageDelayed( |
| TimeoutHandler.MESSAGE_HIDE_CONTROLS, TIMEOUT_CONTROLS); |
| } |
| } |
| } |
| }; |
| mSurfaceView.setOnClickListener(listener); |
| mToggle.setOnClickListener(listener); |
| mFastForward.setOnClickListener(listener); |
| mFastRewind.setOnClickListener(listener); |
| mMinimize.setOnClickListener(listener); |
| |
| // Prepare video playback |
| mSurfaceView |
| .getHolder() |
| .addCallback( |
| new SurfaceHolder.Callback() { |
| @Override |
| public void surfaceCreated(SurfaceHolder holder) { |
| openVideo(holder.getSurface()); |
| } |
| |
| @Override |
| public void surfaceChanged( |
| SurfaceHolder holder, int format, int width, int height) { |
| // Do nothing |
| } |
| |
| @Override |
| public void surfaceDestroyed(SurfaceHolder holder) { |
| if (mMediaPlayer != null) { |
| mSavedCurrentPosition = mMediaPlayer.getCurrentPosition(); |
| } |
| closeVideo(); |
| } |
| }); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| if (mMediaPlayer != null) { |
| final int videoWidth = mMediaPlayer.getVideoWidth(); |
| final int videoHeight = mMediaPlayer.getVideoHeight(); |
| if (videoWidth != 0 && videoHeight != 0) { |
| final float aspectRatio = (float) videoHeight / videoWidth; |
| final int width = MeasureSpec.getSize(widthMeasureSpec); |
| final int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| final int height = MeasureSpec.getSize(heightMeasureSpec); |
| final int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| if (mAdjustViewBounds) { |
| if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) { |
| super.onMeasure( |
| widthMeasureSpec, |
| MeasureSpec.makeMeasureSpec( |
| (int) (width * aspectRatio), MeasureSpec.EXACTLY)); |
| } else if (widthMode != MeasureSpec.EXACTLY |
| && heightMode == MeasureSpec.EXACTLY) { |
| super.onMeasure( |
| MeasureSpec.makeMeasureSpec( |
| (int) (height / aspectRatio), MeasureSpec.EXACTLY), |
| heightMeasureSpec); |
| } else { |
| super.onMeasure( |
| widthMeasureSpec, |
| MeasureSpec.makeMeasureSpec( |
| (int) (width * aspectRatio), MeasureSpec.EXACTLY)); |
| } |
| } else { |
| final float viewRatio = (float) height / width; |
| if (aspectRatio > viewRatio) { |
| int padding = (int) ((width - height / aspectRatio) / 2); |
| setPadding(padding, 0, padding, 0); |
| } else { |
| int padding = (int) ((height - width * aspectRatio) / 2); |
| setPadding(0, padding, 0, padding); |
| } |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| return; |
| } |
| } |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| if (mTimeoutHandler != null) { |
| mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS); |
| mTimeoutHandler = null; |
| } |
| super.onDetachedFromWindow(); |
| } |
| |
| /** |
| * Sets the listener to monitor movie events. |
| * |
| * @param movieListener The listener to be set. |
| */ |
| public void setMovieListener(@Nullable MovieListener movieListener) { |
| mMovieListener = movieListener; |
| } |
| |
| /** |
| * The raw resource id of the video to play. |
| * |
| * @return ID of the video resource. |
| */ |
| public int getVideoResourceId() { |
| return mVideoResourceId; |
| } |
| |
| /** |
| * Sets the raw resource ID of video to play. |
| * |
| * @param id The raw resource ID. |
| */ |
| public void setVideoResourceId(@RawRes int id) { |
| if (id == mVideoResourceId) { |
| return; |
| } |
| mVideoResourceId = id; |
| Surface surface = mSurfaceView.getHolder().getSurface(); |
| if (surface != null && surface.isValid()) { |
| closeVideo(); |
| openVideo(surface); |
| } |
| } |
| |
| public void setAdjustViewBounds(boolean adjustViewBounds) { |
| if (mAdjustViewBounds == adjustViewBounds) { |
| return; |
| } |
| mAdjustViewBounds = adjustViewBounds; |
| if (adjustViewBounds) { |
| setBackground(null); |
| } else { |
| setBackgroundColor(Color.BLACK); |
| } |
| requestLayout(); |
| } |
| |
| /** Shows all the controls. */ |
| public void showControls() { |
| TransitionManager.beginDelayedTransition(this); |
| mShade.setVisibility(View.VISIBLE); |
| mToggle.setVisibility(View.VISIBLE); |
| mFastForward.setVisibility(View.VISIBLE); |
| mFastRewind.setVisibility(View.VISIBLE); |
| mMinimize.setVisibility(View.VISIBLE); |
| } |
| |
| /** Hides all the controls. */ |
| public void hideControls() { |
| TransitionManager.beginDelayedTransition(this); |
| mShade.setVisibility(View.INVISIBLE); |
| mToggle.setVisibility(View.INVISIBLE); |
| mFastForward.setVisibility(View.INVISIBLE); |
| mFastRewind.setVisibility(View.INVISIBLE); |
| mMinimize.setVisibility(View.INVISIBLE); |
| } |
| |
| /** Fast-forward the video. */ |
| public void fastForward() { |
| if (mMediaPlayer == null) { |
| return; |
| } |
| mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() + FAST_FORWARD_REWIND_INTERVAL); |
| } |
| |
| /** Fast-rewind the video. */ |
| public void fastRewind() { |
| if (mMediaPlayer == null) { |
| return; |
| } |
| mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() - FAST_FORWARD_REWIND_INTERVAL); |
| } |
| |
| /** |
| * Returns the current position of the video. If the the player has not been created, then |
| * assumes the beginning of the video. |
| * |
| * @return The current position of the video. |
| */ |
| public int getCurrentPosition() { |
| if (mMediaPlayer == null) { |
| return 0; |
| } |
| return mMediaPlayer.getCurrentPosition(); |
| } |
| |
| public boolean isPlaying() { |
| return mMediaPlayer != null && mMediaPlayer.isPlaying(); |
| } |
| |
| public void play() { |
| if (mMediaPlayer == null) { |
| return; |
| } |
| mMediaPlayer.start(); |
| adjustToggleState(); |
| setKeepScreenOn(true); |
| if (mMovieListener != null) { |
| mMovieListener.onMovieStarted(); |
| } |
| } |
| |
| public void pause() { |
| if (mMediaPlayer == null) { |
| adjustToggleState(); |
| return; |
| } |
| mMediaPlayer.pause(); |
| adjustToggleState(); |
| setKeepScreenOn(false); |
| if (mMovieListener != null) { |
| mMovieListener.onMovieStopped(); |
| } |
| } |
| |
| void openVideo(Surface surface) { |
| if (mVideoResourceId == 0) { |
| return; |
| } |
| mMediaPlayer = new MediaPlayer(); |
| mMediaPlayer.setSurface(surface); |
| startVideo(); |
| } |
| |
| /** Restarts playback of the video. */ |
| public void startVideo() { |
| mMediaPlayer.reset(); |
| try (AssetFileDescriptor fd = getResources().openRawResourceFd(mVideoResourceId)) { |
| mMediaPlayer.setDataSource(fd); |
| mMediaPlayer.setOnPreparedListener( |
| new MediaPlayer.OnPreparedListener() { |
| @Override |
| public void onPrepared(MediaPlayer mediaPlayer) { |
| // Adjust the aspect ratio of this view |
| requestLayout(); |
| if (mSavedCurrentPosition > 0) { |
| mediaPlayer.seekTo(mSavedCurrentPosition); |
| mSavedCurrentPosition = 0; |
| } else { |
| // Start automatically |
| play(); |
| } |
| } |
| }); |
| mMediaPlayer.setOnCompletionListener( |
| new MediaPlayer.OnCompletionListener() { |
| @Override |
| public void onCompletion(MediaPlayer mediaPlayer) { |
| adjustToggleState(); |
| setKeepScreenOn(false); |
| if (mMovieListener != null) { |
| mMovieListener.onMovieStopped(); |
| } |
| } |
| }); |
| mMediaPlayer.prepare(); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to open video", e); |
| } |
| } |
| |
| void closeVideo() { |
| if (mMediaPlayer != null) { |
| mMediaPlayer.release(); |
| mMediaPlayer = null; |
| } |
| } |
| |
| void toggle() { |
| if (mMediaPlayer == null) { |
| return; |
| } |
| if (mMediaPlayer.isPlaying()) { |
| pause(); |
| } else { |
| play(); |
| } |
| } |
| |
| void toggleControls() { |
| if (mShade.getVisibility() == View.VISIBLE) { |
| hideControls(); |
| } else { |
| showControls(); |
| } |
| } |
| |
| void adjustToggleState() { |
| if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { |
| mToggle.setContentDescription(getResources().getString(R.string.pause)); |
| mToggle.setImageResource(R.drawable.ic_pause_64dp); |
| } else { |
| mToggle.setContentDescription(getResources().getString(R.string.play)); |
| mToggle.setImageResource(R.drawable.ic_play_arrow_64dp); |
| } |
| } |
| |
| private static class TimeoutHandler extends Handler { |
| |
| static final int MESSAGE_HIDE_CONTROLS = 1; |
| |
| private final WeakReference<MovieView> mMovieViewRef; |
| |
| TimeoutHandler(MovieView view) { |
| mMovieViewRef = new WeakReference<>(view); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_HIDE_CONTROLS: |
| MovieView movieView = mMovieViewRef.get(); |
| if (movieView != null) { |
| movieView.hideControls(); |
| } |
| break; |
| default: |
| super.handleMessage(msg); |
| } |
| } |
| } |
| } |