• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
16 package android.support.v17.leanback.app;
17 
18 import android.animation.PropertyValuesHolder;
19 import android.app.Fragment;
20 import android.graphics.Bitmap;
21 import android.graphics.Color;
22 import android.graphics.drawable.ColorDrawable;
23 import android.graphics.drawable.Drawable;
24 import android.support.annotation.ColorInt;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.v17.leanback.R;
28 import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
29 import android.support.v17.leanback.media.PlaybackGlue;
30 import android.support.v17.leanback.media.PlaybackGlueHost;
31 import android.support.v17.leanback.widget.DetailsParallaxDrawable;
32 import android.support.v17.leanback.widget.ParallaxTarget;
33 
34 /**
35  * Controller for DetailsFragment 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  * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(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 #onCreateVideoFragment()} and
104  * {@link #onCreateGlueHost()}.
105  * </p>
106  *
107  */
108 public class DetailsFragmentBackgroundController {
109 
110     final DetailsFragment 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 mLastVideoFragmentForGlueHost;
121 
122     /**
123      * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that
124      * each DetailsFragment can only associate with one DetailsFragmentBackgroundController.
125      *
126      * @param fragment The DetailsFragment to control background and embedded video playing.
127      * @throws IllegalStateException If fragment was already associated with another controller.
128      */
DetailsFragmentBackgroundController(DetailsFragment fragment)129     public DetailsFragmentBackgroundController(DetailsFragment fragment) {
130         if (fragment.mDetailsBackgroundController != null) {
131             throw new IllegalStateException("Each DetailsFragment is allowed to initialize "
132                     + "DetailsFragmentBackgroundController 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      */
enableParallax()149     public void enableParallax() {
150         int offset = mParallaxDrawableMaxOffset;
151         if (offset == 0) {
152             offset = FragmentUtil.getContext(mFragment).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      */
enableParallax(@onNull Drawable coverDrawable, @NonNull Drawable bottomDrawable, @Nullable ParallaxTarget.PropertyValuesHolderTarget coverDrawableParallaxTarget)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                 FragmentUtil.getContext(mFragment),
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 VideoFragment and VideoFragmentGlueHost 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 #onCreateVideoFragment()
227      * @see #onCreateGlueHost().
228      */
229     @SuppressWarnings("ReferenceEquality")
setupVideoPlayback(@onNull PlaybackGlue playbackGlue)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                     || mLastVideoFragmentForGlueHost != findOrCreateVideoFragment()) {
246                 mPlaybackGlue.setHost(createGlueHost());
247                 mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
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      */
getPlaybackGlue()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      */
canNavigateToVideoFragment()271     public boolean canNavigateToVideoFragment() {
272         return mPlaybackGlue != null;
273     }
274 
switchToVideoBeforeCreate()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 #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()
283      * it will make video fragment to be initially focused once it is created.
284      * <p>
285      * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter
286      * transition and shared element transition.
287      * </p>
288      * <p>
289      * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and
290      * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.
291      * </p>
292      * <p>
293      * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an
294      * IllegalStateException will be thrown.
295      * </p>
296      */
switchToVideo()297     public final void switchToVideo() {
298         mFragment.switchToVideo();
299     }
300 
301     /**
302      * Switch to rows fragment.
303      */
switchToRows()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      */
onStart()312     void onStart() {
313         if (!mCanUseHost) {
314             mCanUseHost = true;
315             if (mPlaybackGlue != null) {
316                 mPlaybackGlue.setHost(createGlueHost());
317                 mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
318             }
319         }
320         if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
321             mPlaybackGlue.play();
322         }
323     }
324 
onStop()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      */
disableVideoParallax()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      */
getCoverDrawable()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      */
getBottomDrawable()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 VideoFragment} 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      */
onCreateVideoFragment()378     public Fragment onCreateVideoFragment() {
379         return new VideoFragment();
380     }
381 
382     /**
383      * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
384      * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment
385      * super.onCreate(). When override this method, app may call
386      * {@link #findOrCreateVideoFragment()} to get or create a fragment.
387      *
388      * @return A new PlaybackGlueHost to host PlaybackGlue.
389      * @see #onCreateVideoFragment()
390      * @see #findOrCreateVideoFragment()
391      * @see #setupVideoPlayback(PlaybackGlue)
392      */
onCreateGlueHost()393     public PlaybackGlueHost onCreateGlueHost() {
394         return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());
395     }
396 
createGlueHost()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 DetailsFragment. 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      */
findOrCreateVideoFragment()415     public final Fragment findOrCreateVideoFragment() {
416         return mFragment.findOrCreateVideoFragment();
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      */
setCoverBitmap(Bitmap bitmap)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      */
getCoverBitmap()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      */
getSolidColor()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      */
setSolidColor(@olorInt int color)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      */
setParallaxDrawableMaxOffset(int offset)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      */
getParallaxDrawableMaxOffset()488     public final int getParallaxDrawableMaxOffset() {
489         return mParallaxDrawableMaxOffset;
490     }
491 
492 }
493