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