PlaybackOverlayFragment.java revision d20507e0f5ac7ad021f42ca87c294787246f0591
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.graphics.Color;
17import android.graphics.drawable.ColorDrawable;
18import android.animation.Animator;
19import android.animation.AnimatorInflater;
20import android.animation.TimeInterpolator;
21import android.animation.ValueAnimator;
22import android.view.animation.AccelerateInterpolator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.Message;
28import android.support.v7.widget.RecyclerView;
29import android.support.v17.leanback.R;
30import android.support.v17.leanback.animation.LogAccelerateInterpolator;
31import android.support.v17.leanback.animation.LogDecelerateInterpolator;
32import android.support.v17.leanback.widget.Presenter;
33import android.support.v17.leanback.widget.ItemBridgeAdapter;
34import android.support.v17.leanback.widget.ObjectAdapter;
35import android.support.v17.leanback.widget.ObjectAdapter.DataObserver;
36import android.support.v17.leanback.widget.VerticalGridView;
37import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
38import android.util.Log;
39import android.view.KeyEvent;
40import android.view.LayoutInflater;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.ViewGroup.MarginLayoutParams;
45import android.view.animation.Interpolator;
46import android.view.animation.LinearInterpolator;
47
48
49/**
50 * A fragment for displaying playback controls and related content.
51 * The {@link android.support.v17.leanback.widget.PlaybackControlsRow} is expected to be
52 * at position 0 in the adapter.
53 */
54public class PlaybackOverlayFragment extends DetailsFragment {
55
56    /**
57     * No background.
58     */
59    public static final int BG_NONE = 0;
60
61    /**
62     * A dark translucent background.
63     */
64    public static final int BG_DARK = 1;
65
66    /**
67     * A light translucent background.
68     */
69    public static final int BG_LIGHT = 2;
70
71    public static class OnFadeCompleteListener {
72        public void onFadeInComplete() {
73        }
74        public void onFadeOutComplete() {
75        }
76    }
77
78    private static final String TAG = "PlaybackOverlayFragment";
79    private static final boolean DEBUG = false;
80    private static final int ANIMATION_MULTIPLIER = 1;
81
82    private static int START_FADE_OUT = 1;
83
84    // Fading status
85    private static final int IDLE = 0;
86    private static final int IN = 1;
87    private static final int OUT = 2;
88
89    private int mAlignPosition;
90    private int mPaddingBottom;
91    private View mRootView;
92    private int mBackgroundType = BG_DARK;
93    private int mBgDarkColor;
94    private int mBgLightColor;
95    private int mShowTimeMs;
96    private int mMajorFadeTranslateY, mMinorFadeTranslateY;
97    private int mAnimationTranslateY;
98    private OnFadeCompleteListener mFadeCompleteListener;
99    private boolean mFadingEnabled = true;
100    private int mFadingStatus = IDLE;
101    private int mBgAlpha;
102    private ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
103    private ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
104    private ValueAnimator mDescriptionFadeInAnimator, mDescriptionFadeOutAnimator;
105    private ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator;
106    private boolean mTranslateAnimationEnabled;
107    private RecyclerView.ItemAnimator mItemAnimator;
108
109    private final Animator.AnimatorListener mFadeListener =
110            new Animator.AnimatorListener() {
111        @Override
112        public void onAnimationStart(Animator animation) {
113            enableVerticalGridAnimations(false);
114        }
115        @Override
116        public void onAnimationRepeat(Animator animation) {
117        }
118        @Override
119        public void onAnimationCancel(Animator animation) {
120        }
121        @Override
122        public void onAnimationEnd(Animator animation) {
123            if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
124            if (mBgAlpha > 0) {
125                enableVerticalGridAnimations(true);
126                startFadeTimer();
127                if (mFadeCompleteListener != null) {
128                    mFadeCompleteListener.onFadeInComplete();
129                }
130            } else {
131                if (getVerticalGridView() != null) {
132                    // Reset focus to the controls row
133                    getVerticalGridView().setSelectedPosition(0);
134                }
135                if (mFadeCompleteListener != null) {
136                    mFadeCompleteListener.onFadeOutComplete();
137                }
138            }
139            mFadingStatus = IDLE;
140        }
141    };
142
143    private final Handler mHandler = new Handler() {
144        @Override
145        public void handleMessage(Message message) {
146            if (message.what == START_FADE_OUT && mFadingEnabled) {
147                fade(false);
148            }
149        }
150    };
151
152    private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener =
153            new VerticalGridView.OnTouchInterceptListener() {
154        public boolean onInterceptTouchEvent(MotionEvent event) {
155            return onInterceptInputEvent();
156        }
157    };
158
159    private final VerticalGridView.OnMotionInterceptListener mOnMotionInterceptListener =
160            new VerticalGridView.OnMotionInterceptListener() {
161        public boolean onInterceptMotionEvent(MotionEvent event) {
162            return onInterceptInputEvent();
163        }
164    };
165
166    private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener =
167            new VerticalGridView.OnKeyInterceptListener() {
168        public boolean onInterceptKeyEvent(KeyEvent event) {
169            return onInterceptInputEvent();
170        }
171    };
172
173    private void setBgAlpha(int alpha) {
174        mBgAlpha = alpha;
175        if (mRootView != null) {
176            mRootView.getBackground().setAlpha(alpha);
177        }
178    }
179
180    private void enableVerticalGridAnimations(boolean enable) {
181        if (getVerticalGridView() == null) {
182            return;
183        }
184        if (enable && mItemAnimator != null) {
185            getVerticalGridView().setItemAnimator(mItemAnimator);
186        } else if (!enable) {
187            mItemAnimator = getVerticalGridView().getItemAnimator();
188            getVerticalGridView().setItemAnimator(null);
189        }
190    }
191
192    /**
193     * Enables or disables view fading.  If enabled,
194     * the view will be faded in when the fragment starts,
195     * and will fade out after a time period.  The timeout
196     * period is reset each time {@link #tickle} is called.
197     *
198     */
199    public void setFadingEnabled(boolean enabled) {
200        if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled);
201        if (enabled != mFadingEnabled) {
202            mFadingEnabled = enabled;
203            if (isResumed()) {
204                if (mFadingEnabled) {
205                    if (mFadingStatus == IDLE && !mHandler.hasMessages(START_FADE_OUT)) {
206                        startFadeTimer();
207                    }
208                } else {
209                    // Ensure fully opaque
210                    mHandler.removeMessages(START_FADE_OUT);
211                    fade(true);
212                }
213            }
214        }
215    }
216
217    /**
218     * Returns true if view fading is enabled.
219     */
220    public boolean isFadingEnabled() {
221        return mFadingEnabled;
222    }
223
224    /**
225     * Sets the listener to be called when fade in or out has completed.
226     */
227    public void setFadeCompleteListener(OnFadeCompleteListener listener) {
228        mFadeCompleteListener = listener;
229    }
230
231    /**
232     * Returns the listener to be called when fade in or out has completed.
233     */
234    public OnFadeCompleteListener getFadeCompleteListener() {
235        return mFadeCompleteListener;
236    }
237
238    /**
239     * Tickles the playback controls.  Fades in the view if it was faded out,
240     * otherwise resets the fade out timer.  Tickling on input events is handled
241     * by the fragment.
242     */
243    public void tickle() {
244        if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
245        if (!mFadingEnabled || !isResumed()) {
246            return;
247        }
248        if (mHandler.hasMessages(START_FADE_OUT)) {
249            // Restart the timer
250            startFadeTimer();
251        } else {
252            fade(true);
253        }
254    }
255
256    private boolean onInterceptInputEvent() {
257        if (DEBUG) Log.v(TAG, "onInterceptInputEvent status " + mFadingStatus);
258        boolean consumeEvent = (mFadingStatus == IDLE && mBgAlpha == 0);
259        tickle();
260        return consumeEvent;
261    }
262
263    @Override
264    public void onResume() {
265        super.onResume();
266        if (mFadingEnabled) {
267            setBgAlpha(0);
268            fade(true);
269        }
270        getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
271        getVerticalGridView().setOnMotionInterceptListener(mOnMotionInterceptListener);
272        getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
273    }
274
275    private void startFadeTimer() {
276        if (mHandler != null) {
277            mHandler.removeMessages(START_FADE_OUT);
278            mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs);
279        }
280    }
281
282    private static ValueAnimator loadAnimator(Context context, int resId) {
283        ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId);
284        animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER);
285        return animator;
286    }
287
288    private void loadBgAnimator() {
289        AnimatorUpdateListener listener = new AnimatorUpdateListener() {
290            @Override
291            public void onAnimationUpdate(ValueAnimator arg0) {
292                setBgAlpha((Integer) arg0.getAnimatedValue());
293            }
294        };
295
296        mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in);
297        mBgFadeInAnimator.addUpdateListener(listener);
298        mBgFadeInAnimator.addListener(mFadeListener);
299
300        mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out);
301        mBgFadeOutAnimator.addUpdateListener(listener);
302        mBgFadeOutAnimator.addListener(mFadeListener);
303    }
304
305    private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100,0);
306    private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100,0);
307
308    private void loadControlRowAnimator() {
309        AnimatorUpdateListener listener = new AnimatorUpdateListener() {
310            @Override
311            public void onAnimationUpdate(ValueAnimator arg0) {
312                if (getVerticalGridView() == null) {
313                    return;
314                }
315                RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0);
316                if (vh != null) {
317                    final float fraction = (Float) arg0.getAnimatedValue();
318                    if (DEBUG) Log.v(TAG, "fraction " + fraction);
319                    vh.itemView.setAlpha(fraction);
320                    vh.itemView.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
321                }
322            }
323        };
324
325        mControlRowFadeInAnimator = loadAnimator(
326                getActivity(), R.animator.lb_playback_controls_fade_in);
327        mControlRowFadeInAnimator.addUpdateListener(listener);
328        mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
329
330        mControlRowFadeOutAnimator = loadAnimator(
331                getActivity(), R.animator.lb_playback_controls_fade_out);
332        mControlRowFadeOutAnimator.addUpdateListener(listener);
333        mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
334    }
335
336    private void loadOtherRowAnimator() {
337        AnimatorUpdateListener listener = new AnimatorUpdateListener() {
338            @Override
339            public void onAnimationUpdate(ValueAnimator arg0) {
340                if (getVerticalGridView() == null) {
341                    return;
342                }
343                final float fraction = (Float) arg0.getAnimatedValue();
344                final int count = getVerticalGridView().getChildCount();
345                for (int i = 0; i < count; i++) {
346                    View view = getVerticalGridView().getChildAt(i);
347                    if (getVerticalGridView().getChildPosition(view) > 0) {
348                        view.setAlpha(fraction);
349                        view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
350                    }
351                }
352            }
353        };
354
355        mOtherRowFadeInAnimator = loadAnimator(
356                getActivity(), R.animator.lb_playback_controls_fade_in);
357        mOtherRowFadeInAnimator.addUpdateListener(listener);
358        mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
359
360        mOtherRowFadeOutAnimator = loadAnimator(
361                getActivity(), R.animator.lb_playback_controls_fade_out);
362        mOtherRowFadeOutAnimator.addUpdateListener(listener);
363        mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
364    }
365
366    private void loadDescriptionAnimator() {
367        AnimatorUpdateListener listener = new AnimatorUpdateListener() {
368            @Override
369            public void onAnimationUpdate(ValueAnimator arg0) {
370                if (getVerticalGridView() == null) {
371                    return;
372                }
373                ItemBridgeAdapter.ViewHolder adapterVh = (ItemBridgeAdapter.ViewHolder)
374                        getVerticalGridView().findViewHolderForPosition(0);
375                if (adapterVh != null && adapterVh.getViewHolder()
376                        instanceof PlaybackControlsRowPresenter.ViewHolder) {
377                    final Presenter.ViewHolder vh = ((PlaybackControlsRowPresenter.ViewHolder)
378                            adapterVh.getViewHolder()).mDescriptionViewHolder;
379                    vh.view.setAlpha((Float) arg0.getAnimatedValue());
380                }
381            }
382        };
383
384        mDescriptionFadeInAnimator = loadAnimator(
385                getActivity(), R.animator.lb_playback_description_fade_in);
386        mDescriptionFadeInAnimator.addUpdateListener(listener);
387        mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
388
389        mDescriptionFadeOutAnimator = loadAnimator(
390                getActivity(), R.animator.lb_playback_description_fade_out);
391        mDescriptionFadeOutAnimator.addUpdateListener(listener);
392    }
393
394    private void fade(boolean fadeIn) {
395        if (DEBUG) Log.v(TAG, "fade " + fadeIn);
396        if (getView() == null) {
397            return;
398        }
399        if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) {
400            if (DEBUG) Log.v(TAG, "requested fade in progress");
401            return;
402        }
403        if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) {
404            if (DEBUG) Log.v(TAG, "fade is no-op");
405            return;
406        }
407
408        mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0 ?
409                mMajorFadeTranslateY : mMinorFadeTranslateY;
410
411        if (mFadingStatus == IDLE) {
412            if (fadeIn) {
413                mBgFadeInAnimator.start();
414                mControlRowFadeInAnimator.start();
415                mOtherRowFadeInAnimator.start();
416                mDescriptionFadeInAnimator.start();
417            } else {
418                mBgFadeOutAnimator.start();
419                mControlRowFadeOutAnimator.start();
420                mOtherRowFadeOutAnimator.start();
421                mDescriptionFadeOutAnimator.start();
422            }
423        } else {
424            if (fadeIn) {
425                mBgFadeOutAnimator.reverse();
426                mControlRowFadeOutAnimator.reverse();
427                mOtherRowFadeOutAnimator.reverse();
428                mDescriptionFadeOutAnimator.reverse();
429            } else {
430                mBgFadeInAnimator.reverse();
431                mControlRowFadeInAnimator.reverse();
432                mOtherRowFadeInAnimator.reverse();
433                mDescriptionFadeInAnimator.reverse();
434            }
435        }
436
437        // If fading in while control row is focused, set initial translationY so
438        // views slide in from below.
439        if (fadeIn && mFadingStatus == IDLE) {
440            final int count = getVerticalGridView().getChildCount();
441            for (int i = 0; i < count; i++) {
442                getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY);
443            }
444        }
445
446        mFadingStatus = fadeIn ? IN : OUT;
447    }
448
449    /**
450     * Sets the list of rows for the fragment.
451     */
452    @Override
453    public void setAdapter(ObjectAdapter adapter) {
454        if (getAdapter() != null) {
455            getAdapter().unregisterObserver(mObserver);
456        }
457        super.setAdapter(adapter);
458        if (adapter != null) {
459            adapter.registerObserver(mObserver);
460        }
461    }
462
463    @Override
464    void setVerticalGridViewLayout(VerticalGridView listview) {
465        if (listview == null) {
466            return;
467        }
468        // Padding affects alignment when last row is focused
469        // (last is first when there's only one row).
470        setBottomPadding(listview, mPaddingBottom);
471
472        // Item alignment affects focused row that isn't the last.
473        listview.setItemAlignmentOffset(mAlignPosition);
474        listview.setItemAlignmentOffsetPercent(100);
475
476        // Push rows to the bottom.
477        listview.setWindowAlignmentOffset(0);
478        listview.setWindowAlignmentOffsetPercent(100);
479        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);
480    }
481
482    private static void setBottomPadding(View view, int padding) {
483        view.setPadding(view.getPaddingLeft(), view.getPaddingTop(),
484                view.getPaddingRight(), padding);
485    }
486
487    @Override
488    public void onCreate(Bundle savedInstanceState) {
489        super.onCreate(savedInstanceState);
490
491        mAlignPosition =
492                getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_align_bottom);
493        mPaddingBottom =
494                getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom);
495        mBgDarkColor =
496                getResources().getColor(R.color.lb_playback_controls_background_dark);
497        mBgLightColor =
498                getResources().getColor(R.color.lb_playback_controls_background_light);
499        mShowTimeMs =
500                getResources().getInteger(R.integer.lb_playback_controls_show_time_ms);
501        mMajorFadeTranslateY =
502                getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y);
503        mMinorFadeTranslateY =
504                getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y);
505
506        loadBgAnimator();
507        loadControlRowAnimator();
508        loadOtherRowAnimator();
509        loadDescriptionAnimator();
510    }
511
512    /**
513     * Sets the background type.
514     *
515     * @param type One of BG_LIGHT, BG_DARK, or BG_NONE.
516     */
517    public void setBackgroundType(int type) {
518        switch (type) {
519        case BG_LIGHT:
520        case BG_DARK:
521        case BG_NONE:
522            if (type != mBackgroundType) {
523                mBackgroundType = type;
524                updateBackground();
525            }
526            break;
527        default:
528            throw new IllegalArgumentException("Invalid background type");
529        }
530    }
531
532    /**
533     * Returns the background type.
534     */
535    public int getBackgroundType() {
536        return mBackgroundType;
537    }
538
539    private void updateBackground() {
540        if (mRootView != null) {
541            int color = mBgDarkColor;
542            switch (mBackgroundType) {
543                case BG_DARK: break;
544                case BG_LIGHT: color = mBgLightColor; break;
545                case BG_NONE: color = Color.TRANSPARENT; break;
546            }
547            mRootView.setBackground(new ColorDrawable(color));
548        }
549    }
550
551    private void updateControlsBottomSpace(ItemBridgeAdapter.ViewHolder vh) {
552        // Add extra space between rows 0 and 1
553        if (vh == null && getVerticalGridView() != null) {
554            vh = (ItemBridgeAdapter.ViewHolder)
555                    getVerticalGridView().findViewHolderForPosition(0);
556        }
557        if (vh != null && vh.getPresenter() instanceof PlaybackControlsRowPresenter) {
558            final int adapterSize = getAdapter() == null ? 0 : getAdapter().size();
559            ((PlaybackControlsRowPresenter) vh.getPresenter()).showBottomSpace(
560                    (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder(),
561                    adapterSize > 1);
562        }
563    }
564
565    private final ItemBridgeAdapter.AdapterListener mAdapterListener =
566            new ItemBridgeAdapter.AdapterListener() {
567        @Override
568        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
569            if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
570            if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) {
571                if (DEBUG) Log.v(TAG, "setting alpha to 0");
572                vh.getViewHolder().view.setAlpha(0);
573            }
574        }
575        @Override
576        public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
577            if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
578            // Reset animation state
579            vh.getViewHolder().view.setAlpha(1f);
580            vh.getViewHolder().view.setTranslationY(0);
581            if (vh.getViewHolder() instanceof PlaybackControlsRowPresenter.ViewHolder) {
582                ((PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder())
583                        .mDescriptionViewHolder.view.setAlpha(1f);
584            }
585        }
586        @Override
587        public void onBind(ItemBridgeAdapter.ViewHolder vh) {
588            if (vh.getPosition() == 0) {
589                updateControlsBottomSpace(vh);
590            }
591        }
592    };
593
594    @Override
595    public View onCreateView(LayoutInflater inflater, ViewGroup container,
596            Bundle savedInstanceState) {
597        mRootView = super.onCreateView(inflater, container, savedInstanceState);
598        mBgAlpha = 255;
599        updateBackground();
600        getRowsFragment().setExternalAdapterListener(mAdapterListener);
601        return mRootView;
602    }
603
604    @Override
605    public void onDestroyView() {
606        mRootView = null;
607        super.onDestroyView();
608    }
609
610    private final DataObserver mObserver = new DataObserver() {
611        public void onChanged() {
612            updateControlsBottomSpace(null);
613        }
614    };
615}
616