Java tutorial
/* * 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 android.support.v17.leanback.app; import android.animation.PropertyValuesHolder; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.graphics.FitWidthBitmapDrawable; import android.support.v17.leanback.media.PlaybackGlue; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.widget.DetailsParallaxDrawable; import android.support.v17.leanback.widget.ParallaxTarget; import android.support.v4.app.Fragment; /** * Controller for DetailsSupportFragment parallax background and embedded video play. * <p> * The parallax background drawable is made of two parts: cover drawable (by default * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size * of cover drawable and bottom drawable will be updated and the cover drawable will by default * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}. * </p> * <pre> * *************************** * * Cover Drawable * * * (FitWidthBitmapDrawable)* * * * * *************************** * * DetailsOverviewRow * * * * * *************************** * * Bottom Drawable * * * (ColorDrawable) * * * Related * * * Content * * *************************** * </pre> * Both parallax background drawable and embedded video play are optional. App must call * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly. * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable * will be faded out. * Example: * <pre> * DetailsSupportFragmentBackgroundController mController = new DetailsSupportFragmentBackgroundController(this); * * public void onCreate(Bundle savedInstance) { * super.onCreate(savedInstance); * MediaPlayerGlue player = new MediaPlayerGlue(..); * player.setUrl(...); * mController.enableParallax(); * mController.setupVideoPlayback(player); * } * * static class MyLoadBitmapTask extends ... { * WeakReference<MyFragment> mFragmentRef; * MyLoadBitmapTask(MyFragment fragment) { * mFragmentRef = new WeakReference(fragment); * } * protected void onPostExecute(Bitmap bitmap) { * MyFragment fragment = mFragmentRef.get(); * if (fragment != null) { * fragment.mController.setCoverBitmap(bitmap); * } * } * } * * public void onStart() { * new MyLoadBitmapTask(this).execute(url); * } * * public void onStop() { * mController.setCoverBitmap(null); * } * </pre> * <p> * To customize cover drawable and/or bottom drawable, app should call * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}. * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}. * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}. * </p> * <p> * To customize playback fragment, app should override {@link #onCreateVideoSupportFragment()} and * {@link #onCreateGlueHost()}. * </p> * */ public class DetailsSupportFragmentBackgroundController { final DetailsSupportFragment mFragment; DetailsParallaxDrawable mParallaxDrawable; int mParallaxDrawableMaxOffset; PlaybackGlue mPlaybackGlue; DetailsBackgroundVideoHelper mVideoHelper; Bitmap mCoverBitmap; int mSolidColor; boolean mCanUseHost = false; boolean mInitialControlVisible = false; private Fragment mLastVideoSupportFragmentForGlueHost; /** * Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that * each DetailsSupportFragment can only associate with one DetailsSupportFragmentBackgroundController. * * @param fragment The DetailsSupportFragment to control background and embedded video playing. * @throws IllegalStateException If fragment was already associated with another controller. */ public DetailsSupportFragmentBackgroundController(DetailsSupportFragment fragment) { if (fragment.mDetailsBackgroundController != null) { throw new IllegalStateException("Each DetailsSupportFragment is allowed to initialize " + "DetailsSupportFragmentBackgroundController once"); } fragment.mDetailsBackgroundController = this; mFragment = fragment; } /** * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable. * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}. * * @see #setCoverBitmap(Bitmap) * @see #setSolidColor(int) * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called. */ public void enableParallax() { int offset = mParallaxDrawableMaxOffset; if (offset == 0) { offset = mFragment.getContext().getResources() .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement); } Drawable coverDrawable = new FitWidthBitmapDrawable(); ColorDrawable colorDrawable = new ColorDrawable(); enableParallax(coverDrawable, colorDrawable, new ParallaxTarget.PropertyValuesHolderTarget(coverDrawable, PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET, 0, -offset))); } /** * Enables parallax background using a custom cover drawable at top and a custom bottom * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}. * * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)} * will not work if coverDrawable is not {@link FitWidthBitmapDrawable}; * in that case it's app's responsibility to set content into * coverDrawable. * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work * if bottomDrawable is not {@link ColorDrawable}; in that case it's app's * responsibility to set content of bottomDrawable. * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable. * Use null for no parallax movement effect. * Example to move bitmap within FitWidthBitmapDrawable: * new ParallaxTarget.PropertyValuesHolderTarget( * coverDrawable, PropertyValuesHolder.ofInt( * FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET, * 0, -120)) * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called. */ public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable, @Nullable ParallaxTarget.PropertyValuesHolderTarget coverDrawableParallaxTarget) { if (mParallaxDrawable != null) { return; } // if bitmap is set before enableParallax, use it as initial value. if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) { ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap); } // if solid color is set before enableParallax, use it as initial value. if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) { ((ColorDrawable) bottomDrawable).setColor(mSolidColor); } if (mPlaybackGlue != null) { throw new IllegalStateException( "enableParallaxDrawable must be called before " + "enableVideoPlayback"); } mParallaxDrawable = new DetailsParallaxDrawable(mFragment.getContext(), mFragment.getParallax(), coverDrawable, bottomDrawable, coverDrawableParallaxTarget); mFragment.setBackgroundDrawable(mParallaxDrawable); // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility // before PlaybackGlue is ready. mVideoHelper = new DetailsBackgroundVideoHelper(null, mFragment.getParallax(), mParallaxDrawable.getCoverDrawable()); } /** * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default * creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue. * This method must be called after calling details Fragment super.onCreate(). This method * can be called multiple times to replace existing PlaybackGlue or calling * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue} * subclass is not doing that, it's app's responsibility to release the resources. * * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one. * @see #onCreateVideoSupportFragment() * @see #onCreateGlueHost(). */ @SuppressWarnings("ReferenceEquality") public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) { if (mPlaybackGlue == playbackGlue) { return; } PlaybackGlueHost playbackGlueHost = null; if (mPlaybackGlue != null) { playbackGlueHost = mPlaybackGlue.getHost(); mPlaybackGlue.setHost(null); } mPlaybackGlue = playbackGlue; mVideoHelper.setPlaybackGlue(mPlaybackGlue); if (mCanUseHost && mPlaybackGlue != null) { if (playbackGlueHost == null || mLastVideoSupportFragmentForGlueHost != findOrCreateVideoSupportFragment()) { mPlaybackGlue.setHost(createGlueHost()); mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment(); } else { mPlaybackGlue.setHost(playbackGlueHost); } } } /** * Returns current PlaybackGlue or null if not set or cleared. * * @return Current PlaybackGlue or null */ public final PlaybackGlue getPlaybackGlue() { return mPlaybackGlue; } /** * Precondition allows user navigate to video fragment using DPAD. Default implementation * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block * app calls {@link #switchToVideo}. * * @return True allow to navigate to video fragment. */ public boolean canNavigateToVideoSupportFragment() { return mPlaybackGlue != null; } void switchToVideoBeforeCreate() { mVideoHelper.crossFadeBackgroundToVideo(true, true); mInitialControlVisible = true; } /** * Switch to video fragment, note that this method is not affected by result of * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate() * it will make video fragment to be initially focused once it is created. * <p> * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter * transition and shared element transition. * </p> * <p> * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored. * </p> * <p> * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an * IllegalStateException will be thrown. * </p> */ public final void switchToVideo() { mFragment.switchToVideo(); } /** * Switch to rows fragment. */ public final void switchToRows() { mFragment.switchToRows(); } /** * When fragment is started and no running transition. First set host if not yet set, second * start playing if it was paused before. */ void onStart() { if (!mCanUseHost) { mCanUseHost = true; if (mPlaybackGlue != null) { mPlaybackGlue.setHost(createGlueHost()); mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment(); } } if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) { mPlaybackGlue.play(); } } void onStop() { if (mPlaybackGlue != null) { mPlaybackGlue.pause(); } } /** * Disable parallax that would auto-start video playback * @return true if video fragment is visible or false otherwise. */ boolean disableVideoParallax() { if (mVideoHelper != null) { mVideoHelper.stopParallax(); return mVideoHelper.isVideoVisible(); } return false; } /** * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called. * By default it's a {@link FitWidthBitmapDrawable}. * * @return The cover drawable at top. */ public final Drawable getCoverDrawable() { if (mParallaxDrawable == null) { return null; } return mParallaxDrawable.getCoverDrawable(); } /** * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called. * By default it's a {@link ColorDrawable}. * * @return The bottom drawable. */ public final Drawable getBottomDrawable() { if (mParallaxDrawable == null) { return null; } return mParallaxDrawable.getBottomDrawable(); } /** * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoSupportFragment} by * default. App may override and return a different fragment and it also must override * {@link #onCreateGlueHost()}. * * @return A new fragment used in {@link #onCreateGlueHost()}. * @see #onCreateGlueHost() * @see #setupVideoPlayback(PlaybackGlue) */ public Fragment onCreateVideoSupportFragment() { return new VideoSupportFragment(); } /** * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides * {@link #onCreateVideoSupportFragment()}. This method must be called after calling Fragment * super.onCreate(). When override this method, app may call * {@link #findOrCreateVideoSupportFragment()} to get or create a fragment. * * @return A new PlaybackGlueHost to host PlaybackGlue. * @see #onCreateVideoSupportFragment() * @see #findOrCreateVideoSupportFragment() * @see #setupVideoPlayback(PlaybackGlue) */ public PlaybackGlueHost onCreateGlueHost() { return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment()); } PlaybackGlueHost createGlueHost() { PlaybackGlueHost host = onCreateGlueHost(); if (mInitialControlVisible) { host.showControlsOverlay(false); } else { host.hideControlsOverlay(false); } return host; } /** * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating * a {@link PlaybackGlueHost}. * * @return Fragment the added or restored fragment responsible for rendering video. * @see #onCreateGlueHost() */ public final Fragment findOrCreateVideoSupportFragment() { return mFragment.findOrCreateVideoSupportFragment(); } /** * Convenient method to set Bitmap in cover drawable. If app is not using default * {@link FitWidthBitmapDrawable}, app should not use this method It's safe to call * setCoverBitmap() before calling {@link #enableParallax()}. * * @param bitmap bitmap to set as cover. */ public final void setCoverBitmap(Bitmap bitmap) { mCoverBitmap = bitmap; Drawable drawable = getCoverDrawable(); if (drawable instanceof FitWidthBitmapDrawable) { ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap); } } /** * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}. * * @return Bitmap for cover drawable. */ public final Bitmap getCoverBitmap() { return mCoverBitmap; } /** * Returns color set by {@link #setSolidColor(int)}. * * @return Solid color used for bottom drawable. */ public final @ColorInt int getSolidColor() { return mSolidColor; } /** * Convenient method to set color in bottom drawable. If app is not using default * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor() * before calling {@link #enableParallax()}. * * @param color color for bottom drawable. */ public final void setSolidColor(@ColorInt int color) { mSolidColor = color; Drawable bottomDrawable = getBottomDrawable(); if (bottomDrawable instanceof ColorDrawable) { ((ColorDrawable) bottomDrawable).setColor(color); } } /** * Sets default parallax offset in pixels for bitmap moving vertically. This method must * be called before {@link #enableParallax()}. * * @param offset Offset in pixels (e.g. 120). * @see #enableParallax() */ public final void setParallaxDrawableMaxOffset(int offset) { if (mParallaxDrawable != null) { throw new IllegalStateException("enableParallax already called"); } mParallaxDrawableMaxOffset = offset; } /** * Returns Default parallax offset in pixels for bitmap moving vertically. * When 0, a default value would be used. * * @return Default parallax offset in pixels for bitmap moving vertically. * @see #enableParallax() */ public final int getParallaxDrawableMaxOffset() { return mParallaxDrawableMaxOffset; } }