1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.support.v17.leanback.app; 17 18import android.animation.PropertyValuesHolder; 19import android.graphics.Bitmap; 20import android.graphics.Color; 21import android.graphics.drawable.ColorDrawable; 22import android.graphics.drawable.Drawable; 23import android.support.annotation.ColorInt; 24import android.support.annotation.NonNull; 25import android.support.annotation.Nullable; 26import android.support.v17.leanback.R; 27import android.support.v17.leanback.graphics.FitWidthBitmapDrawable; 28import android.support.v17.leanback.media.PlaybackGlue; 29import android.support.v17.leanback.media.PlaybackGlueHost; 30import android.support.v17.leanback.widget.DetailsParallaxDrawable; 31import android.support.v17.leanback.widget.ParallaxTarget; 32import android.support.v4.app.Fragment; 33 34/** 35 * Controller for DetailsSupportFragment parallax background and embedded video play. 36 * <p> 37 * The parallax background drawable is made of two parts: cover drawable (by default 38 * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default 39 * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size 40 * of cover drawable and bottom drawable will be updated and the cover drawable will by default 41 * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}. 42 * </p> 43 * <pre> 44 * *************************** 45 * * Cover Drawable * 46 * * (FitWidthBitmapDrawable)* 47 * * * 48 * *************************** 49 * * DetailsOverviewRow * 50 * * * 51 * *************************** 52 * * Bottom Drawable * 53 * * (ColorDrawable) * 54 * * Related * 55 * * Content * 56 * *************************** 57 * </pre> 58 * Both parallax background drawable and embedded video play are optional. App must call 59 * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly. 60 * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and 61 * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable 62 * will be faded out. 63 * Example: 64 * <pre> 65 * DetailsSupportFragmentBackgroundController mController = new DetailsSupportFragmentBackgroundController(this); 66 * 67 * public void onCreate(Bundle savedInstance) { 68 * super.onCreate(savedInstance); 69 * MediaPlayerGlue player = new MediaPlayerGlue(..); 70 * player.setUrl(...); 71 * mController.enableParallax(); 72 * mController.setupVideoPlayback(player); 73 * } 74 * 75 * static class MyLoadBitmapTask extends ... { 76 * WeakReference<MyFragment> mFragmentRef; 77 * MyLoadBitmapTask(MyFragment fragment) { 78 * mFragmentRef = new WeakReference(fragment); 79 * } 80 * protected void onPostExecute(Bitmap bitmap) { 81 * MyFragment fragment = mFragmentRef.get(); 82 * if (fragment != null) { 83 * fragment.mController.setCoverBitmap(bitmap); 84 * } 85 * } 86 * } 87 * 88 * public void onStart() { 89 * new MyLoadBitmapTask(this).execute(url); 90 * } 91 * 92 * public void onStop() { 93 * mController.setCoverBitmap(null); 94 * } 95 * </pre> 96 * <p> 97 * To customize cover drawable and/or bottom drawable, app should call 98 * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}. 99 * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}. 100 * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}. 101 * </p> 102 * <p> 103 * To customize playback fragment, app should override {@link #onCreateVideoSupportFragment()} and 104 * {@link #onCreateGlueHost()}. 105 * </p> 106 * 107 */ 108public class DetailsSupportFragmentBackgroundController { 109 110 final DetailsSupportFragment mFragment; 111 DetailsParallaxDrawable mParallaxDrawable; 112 int mParallaxDrawableMaxOffset; 113 PlaybackGlue mPlaybackGlue; 114 DetailsBackgroundVideoHelper mVideoHelper; 115 Bitmap mCoverBitmap; 116 int mSolidColor; 117 boolean mCanUseHost = false; 118 boolean mInitialControlVisible = false; 119 120 private Fragment mLastVideoSupportFragmentForGlueHost; 121 122 /** 123 * Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that 124 * each DetailsSupportFragment can only associate with one DetailsSupportFragmentBackgroundController. 125 * 126 * @param fragment The DetailsSupportFragment to control background and embedded video playing. 127 * @throws IllegalStateException If fragment was already associated with another controller. 128 */ 129 public DetailsSupportFragmentBackgroundController(DetailsSupportFragment fragment) { 130 if (fragment.mDetailsBackgroundController != null) { 131 throw new IllegalStateException("Each DetailsSupportFragment is allowed to initialize " 132 + "DetailsSupportFragmentBackgroundController once"); 133 } 134 fragment.mDetailsBackgroundController = this; 135 mFragment = fragment; 136 } 137 138 /** 139 * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable 140 * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied 141 * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and 142 * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable. 143 * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}. 144 * 145 * @see #setCoverBitmap(Bitmap) 146 * @see #setSolidColor(int) 147 * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called. 148 */ 149 public void enableParallax() { 150 int offset = mParallaxDrawableMaxOffset; 151 if (offset == 0) { 152 offset = mFragment.getContext().getResources() 153 .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement); 154 } 155 Drawable coverDrawable = new FitWidthBitmapDrawable(); 156 ColorDrawable colorDrawable = new ColorDrawable(); 157 enableParallax(coverDrawable, colorDrawable, 158 new ParallaxTarget.PropertyValuesHolderTarget( 159 coverDrawable, 160 PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET, 161 0, -offset) 162 )); 163 } 164 165 /** 166 * Enables parallax background using a custom cover drawable at top and a custom bottom 167 * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}. 168 * 169 * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)} 170 * will not work if coverDrawable is not {@link FitWidthBitmapDrawable}; 171 * in that case it's app's responsibility to set content into 172 * coverDrawable. 173 * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work 174 * if bottomDrawable is not {@link ColorDrawable}; in that case it's app's 175 * responsibility to set content of bottomDrawable. 176 * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable. 177 * Use null for no parallax movement effect. 178 * Example to move bitmap within FitWidthBitmapDrawable: 179 * new ParallaxTarget.PropertyValuesHolderTarget( 180 * coverDrawable, PropertyValuesHolder.ofInt( 181 * FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET, 182 * 0, -120)) 183 * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called. 184 */ 185 public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable, 186 @Nullable ParallaxTarget.PropertyValuesHolderTarget 187 coverDrawableParallaxTarget) { 188 if (mParallaxDrawable != null) { 189 return; 190 } 191 // if bitmap is set before enableParallax, use it as initial value. 192 if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) { 193 ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap); 194 } 195 // if solid color is set before enableParallax, use it as initial value. 196 if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) { 197 ((ColorDrawable) bottomDrawable).setColor(mSolidColor); 198 } 199 if (mPlaybackGlue != null) { 200 throw new IllegalStateException("enableParallaxDrawable must be called before " 201 + "enableVideoPlayback"); 202 } 203 mParallaxDrawable = new DetailsParallaxDrawable( 204 mFragment.getContext(), 205 mFragment.getParallax(), 206 coverDrawable, 207 bottomDrawable, 208 coverDrawableParallaxTarget); 209 mFragment.setBackgroundDrawable(mParallaxDrawable); 210 // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility 211 // before PlaybackGlue is ready. 212 mVideoHelper = new DetailsBackgroundVideoHelper(null, 213 mFragment.getParallax(), mParallaxDrawable.getCoverDrawable()); 214 } 215 216 /** 217 * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default 218 * creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue. 219 * This method must be called after calling details Fragment super.onCreate(). This method 220 * can be called multiple times to replace existing PlaybackGlue or calling 221 * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases 222 * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue} 223 * subclass is not doing that, it's app's responsibility to release the resources. 224 * 225 * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one. 226 * @see #onCreateVideoSupportFragment() 227 * @see #onCreateGlueHost(). 228 */ 229 @SuppressWarnings("ReferenceEquality") 230 public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) { 231 if (mPlaybackGlue == playbackGlue) { 232 return; 233 } 234 235 PlaybackGlueHost playbackGlueHost = null; 236 if (mPlaybackGlue != null) { 237 playbackGlueHost = mPlaybackGlue.getHost(); 238 mPlaybackGlue.setHost(null); 239 } 240 241 mPlaybackGlue = playbackGlue; 242 mVideoHelper.setPlaybackGlue(mPlaybackGlue); 243 if (mCanUseHost && mPlaybackGlue != null) { 244 if (playbackGlueHost == null 245 || mLastVideoSupportFragmentForGlueHost != findOrCreateVideoSupportFragment()) { 246 mPlaybackGlue.setHost(createGlueHost()); 247 mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment(); 248 } else { 249 mPlaybackGlue.setHost(playbackGlueHost); 250 } 251 } 252 } 253 254 /** 255 * Returns current PlaybackGlue or null if not set or cleared. 256 * 257 * @return Current PlaybackGlue or null 258 */ 259 public final PlaybackGlue getPlaybackGlue() { 260 return mPlaybackGlue; 261 } 262 263 /** 264 * Precondition allows user navigate to video fragment using DPAD. Default implementation 265 * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation 266 * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block 267 * app calls {@link #switchToVideo}. 268 * 269 * @return True allow to navigate to video fragment. 270 */ 271 public boolean canNavigateToVideoSupportFragment() { 272 return mPlaybackGlue != null; 273 } 274 275 void switchToVideoBeforeCreate() { 276 mVideoHelper.crossFadeBackgroundToVideo(true, true); 277 mInitialControlVisible = true; 278 } 279 280 /** 281 * Switch to video fragment, note that this method is not affected by result of 282 * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate() 283 * it will make video fragment to be initially focused once it is created. 284 * <p> 285 * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter 286 * transition and shared element transition. 287 * </p> 288 * <p> 289 * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and 290 * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored. 291 * </p> 292 * <p> 293 * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an 294 * IllegalStateException will be thrown. 295 * </p> 296 */ 297 public final void switchToVideo() { 298 mFragment.switchToVideo(); 299 } 300 301 /** 302 * Switch to rows fragment. 303 */ 304 public final void switchToRows() { 305 mFragment.switchToRows(); 306 } 307 308 /** 309 * When fragment is started and no running transition. First set host if not yet set, second 310 * start playing if it was paused before. 311 */ 312 void onStart() { 313 if (!mCanUseHost) { 314 mCanUseHost = true; 315 if (mPlaybackGlue != null) { 316 mPlaybackGlue.setHost(createGlueHost()); 317 mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment(); 318 } 319 } 320 if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) { 321 mPlaybackGlue.play(); 322 } 323 } 324 325 void onStop() { 326 if (mPlaybackGlue != null) { 327 mPlaybackGlue.pause(); 328 } 329 } 330 331 /** 332 * Disable parallax that would auto-start video playback 333 * @return true if video fragment is visible or false otherwise. 334 */ 335 boolean disableVideoParallax() { 336 if (mVideoHelper != null) { 337 mVideoHelper.stopParallax(); 338 return mVideoHelper.isVideoVisible(); 339 } 340 return false; 341 } 342 343 /** 344 * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called. 345 * By default it's a {@link FitWidthBitmapDrawable}. 346 * 347 * @return The cover drawable at top. 348 */ 349 public final Drawable getCoverDrawable() { 350 if (mParallaxDrawable == null) { 351 return null; 352 } 353 return mParallaxDrawable.getCoverDrawable(); 354 } 355 356 /** 357 * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called. 358 * By default it's a {@link ColorDrawable}. 359 * 360 * @return The bottom drawable. 361 */ 362 public final Drawable getBottomDrawable() { 363 if (mParallaxDrawable == null) { 364 return null; 365 } 366 return mParallaxDrawable.getBottomDrawable(); 367 } 368 369 /** 370 * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoSupportFragment} by 371 * default. App may override and return a different fragment and it also must override 372 * {@link #onCreateGlueHost()}. 373 * 374 * @return A new fragment used in {@link #onCreateGlueHost()}. 375 * @see #onCreateGlueHost() 376 * @see #setupVideoPlayback(PlaybackGlue) 377 */ 378 public Fragment onCreateVideoSupportFragment() { 379 return new VideoSupportFragment(); 380 } 381 382 /** 383 * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides 384 * {@link #onCreateVideoSupportFragment()}. This method must be called after calling Fragment 385 * super.onCreate(). When override this method, app may call 386 * {@link #findOrCreateVideoSupportFragment()} to get or create a fragment. 387 * 388 * @return A new PlaybackGlueHost to host PlaybackGlue. 389 * @see #onCreateVideoSupportFragment() 390 * @see #findOrCreateVideoSupportFragment() 391 * @see #setupVideoPlayback(PlaybackGlue) 392 */ 393 public PlaybackGlueHost onCreateGlueHost() { 394 return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment()); 395 } 396 397 PlaybackGlueHost createGlueHost() { 398 PlaybackGlueHost host = onCreateGlueHost(); 399 if (mInitialControlVisible) { 400 host.showControlsOverlay(false); 401 } else { 402 host.hideControlsOverlay(false); 403 } 404 return host; 405 } 406 407 /** 408 * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that 409 * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating 410 * a {@link PlaybackGlueHost}. 411 * 412 * @return Fragment the added or restored fragment responsible for rendering video. 413 * @see #onCreateGlueHost() 414 */ 415 public final Fragment findOrCreateVideoSupportFragment() { 416 return mFragment.findOrCreateVideoSupportFragment(); 417 } 418 419 /** 420 * Convenient method to set Bitmap in cover drawable. If app is not using default 421 * {@link FitWidthBitmapDrawable}, app should not use this method It's safe to call 422 * setCoverBitmap() before calling {@link #enableParallax()}. 423 * 424 * @param bitmap bitmap to set as cover. 425 */ 426 public final void setCoverBitmap(Bitmap bitmap) { 427 mCoverBitmap = bitmap; 428 Drawable drawable = getCoverDrawable(); 429 if (drawable instanceof FitWidthBitmapDrawable) { 430 ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap); 431 } 432 } 433 434 /** 435 * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}. 436 * 437 * @return Bitmap for cover drawable. 438 */ 439 public final Bitmap getCoverBitmap() { 440 return mCoverBitmap; 441 } 442 443 /** 444 * Returns color set by {@link #setSolidColor(int)}. 445 * 446 * @return Solid color used for bottom drawable. 447 */ 448 public final @ColorInt int getSolidColor() { 449 return mSolidColor; 450 } 451 452 /** 453 * Convenient method to set color in bottom drawable. If app is not using default 454 * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor() 455 * before calling {@link #enableParallax()}. 456 * 457 * @param color color for bottom drawable. 458 */ 459 public final void setSolidColor(@ColorInt int color) { 460 mSolidColor = color; 461 Drawable bottomDrawable = getBottomDrawable(); 462 if (bottomDrawable instanceof ColorDrawable) { 463 ((ColorDrawable) bottomDrawable).setColor(color); 464 } 465 } 466 467 /** 468 * Sets default parallax offset in pixels for bitmap moving vertically. This method must 469 * be called before {@link #enableParallax()}. 470 * 471 * @param offset Offset in pixels (e.g. 120). 472 * @see #enableParallax() 473 */ 474 public final void setParallaxDrawableMaxOffset(int offset) { 475 if (mParallaxDrawable != null) { 476 throw new IllegalStateException("enableParallax already called"); 477 } 478 mParallaxDrawableMaxOffset = offset; 479 } 480 481 /** 482 * Returns Default parallax offset in pixels for bitmap moving vertically. 483 * When 0, a default value would be used. 484 * 485 * @return Default parallax offset in pixels for bitmap moving vertically. 486 * @see #enableParallax() 487 */ 488 public final int getParallaxDrawableMaxOffset() { 489 return mParallaxDrawableMaxOffset; 490 } 491 492} 493