| /* |
| * 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.app.Fragment; |
| 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; |
| |
| /** |
| * Controller for DetailsFragment 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> |
| * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(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 #onCreateVideoFragment()} and |
| * {@link #onCreateGlueHost()}. |
| * </p> |
| * |
| */ |
| public class DetailsFragmentBackgroundController { |
| |
| final DetailsFragment mFragment; |
| DetailsParallaxDrawable mParallaxDrawable; |
| int mParallaxDrawableMaxOffset; |
| PlaybackGlue mPlaybackGlue; |
| DetailsBackgroundVideoHelper mVideoHelper; |
| Bitmap mCoverBitmap; |
| int mSolidColor; |
| boolean mCanUseHost = false; |
| |
| /** |
| * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that |
| * each DetailsFragment can only associate with one DetailsFragmentBackgroundController. |
| * |
| * @param fragment The DetailsFragment to control background and embedded video playing. |
| * @throws IllegalStateException If fragment was already associated with another controller. |
| */ |
| public DetailsFragmentBackgroundController(DetailsFragment fragment) { |
| if (fragment.mDetailsBackgroundController != null) { |
| throw new IllegalStateException("Each DetailsFragment is allowed to initialize " |
| + "DetailsFragmentBackgroundController 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 = FragmentUtil.getContext(mFragment).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( |
| FragmentUtil.getContext(mFragment), |
| 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 VideoFragment and VideoFragmentGlueHost 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 #onCreateVideoFragment() |
| * @see #onCreateGlueHost(). |
| */ |
| public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) { |
| if (mPlaybackGlue == playbackGlue) { |
| return; |
| } |
| if (mPlaybackGlue != null) { |
| mPlaybackGlue.setHost(null); |
| } |
| mPlaybackGlue = playbackGlue; |
| mVideoHelper.setPlaybackGlue(mPlaybackGlue); |
| if (mCanUseHost && mPlaybackGlue != null) { |
| mPlaybackGlue.setHost(onCreateGlueHost()); |
| } |
| } |
| |
| /** |
| * 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#isReadyForPlayback()} is true. Note this method does not block |
| * app calls {@link #switchToVideo}. |
| * |
| * @return True allow to navigate to video fragment. |
| */ |
| public boolean canNavigateToVideoFragment() { |
| return mPlaybackGlue != null; |
| } |
| |
| void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) { |
| mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate); |
| } |
| |
| /** |
| * Switch to video fragment, note that this method is not affected by result of |
| * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate() |
| * it will make video fragment to be initially focused once it is created. |
| * <p> |
| * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter |
| * transition and shared element transition. |
| * </p> |
| * <p> |
| * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and |
| * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored. |
| * </p> |
| * <p> |
| * If {@link DetailsFragment#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(onCreateGlueHost()); |
| } |
| } |
| if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) { |
| 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 VideoFragment} 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 onCreateVideoFragment() { |
| return new VideoFragment(); |
| } |
| |
| /** |
| * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides |
| * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment |
| * super.onCreate(). When override this method, app may call |
| * {@link #findOrCreateVideoFragment()} to get or create a fragment. |
| * |
| * @return A new PlaybackGlueHost to host PlaybackGlue. |
| * @see #onCreateVideoFragment() |
| * @see #findOrCreateVideoFragment() |
| * @see #setupVideoPlayback(PlaybackGlue) |
| */ |
| public PlaybackGlueHost onCreateGlueHost() { |
| return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment()); |
| } |
| |
| /** |
| * Adds or gets fragment for rendering video in DetailsFragment. 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 findOrCreateVideoFragment() { |
| return mFragment.findOrCreateVideoFragment(); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| } |