/* * Copyright (C) 2014 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.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.view.InputEvent; import android.view.animation.AccelerateInterpolator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v7.widget.RecyclerView; import android.support.v17.leanback.R; import android.support.v17.leanback.animation.LogAccelerateInterpolator; import android.support.v17.leanback.animation.LogDecelerateInterpolator; import android.support.v17.leanback.widget.ItemBridgeAdapter; import android.support.v17.leanback.widget.ObjectAdapter; import android.support.v17.leanback.widget.ObjectAdapter.DataObserver; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.VerticalGridView; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; /** * A fragment for displaying playback controls and related content. *
* A PlaybackOverlayFragment renders the elements of its {@link ObjectAdapter} as a set * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses * of {@link RowPresenter}. *
** An instance of {@link android.support.v17.leanback.widget.PlaybackControlsRow} is expected to be * at position 0 in the adapter. *
*/ public class PlaybackOverlayFragment extends DetailsFragment { /** * No background. */ public static final int BG_NONE = 0; /** * A dark translucent background. */ public static final int BG_DARK = 1; /** * A light translucent background. */ public static final int BG_LIGHT = 2; /** * Listener allowing the application to receive notification of fade in and/or fade out * completion events. */ public static class OnFadeCompleteListener { public void onFadeInComplete() { } public void onFadeOutComplete() { } } /** * Interface allowing the application to handle input events. */ public interface InputEventHandler { /** * Called when an {@link InputEvent} is received. * * @return If the event should be consumed, return true. To allow the event to * continue on to the next handler, return false. */ public boolean handleInputEvent(InputEvent event); } private static final String TAG = "PlaybackOverlayFragment"; private static final boolean DEBUG = false; private static final int ANIMATION_MULTIPLIER = 1; private static int START_FADE_OUT = 1; // Fading status private static final int IDLE = 0; private static final int IN = 1; private static final int OUT = 2; private int mPaddingTop; private int mPaddingBottom; private View mRootView; private int mBackgroundType = BG_DARK; private int mBgDarkColor; private int mBgLightColor; private int mShowTimeMs; private int mMajorFadeTranslateY, mMinorFadeTranslateY; private int mAnimationTranslateY; private OnFadeCompleteListener mFadeCompleteListener; private InputEventHandler mInputEventHandler; private boolean mFadingEnabled = true; private int mFadingStatus = IDLE; private int mBgAlpha; private ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; private ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; private ValueAnimator mDescriptionFadeInAnimator, mDescriptionFadeOutAnimator; private ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; private boolean mTranslateAnimationEnabled; private boolean mResetControlsToPrimaryActionsPending; private RecyclerView.ItemAnimator mItemAnimator; private final Animator.AnimatorListener mFadeListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { enableVerticalGridAnimations(false); } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); if (mBgAlpha > 0) { enableVerticalGridAnimations(true); startFadeTimer(); if (mFadeCompleteListener != null) { mFadeCompleteListener.onFadeInComplete(); } } else { VerticalGridView verticalView = getVerticalGridView(); // reset focus to the primary actions only if the selected row was the controls row if (verticalView != null && verticalView.getSelectedPosition() == 0) { resetControlsToPrimaryActions(null); } if (mFadeCompleteListener != null) { mFadeCompleteListener.onFadeOutComplete(); } } mFadingStatus = IDLE; } }; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message message) { if (message.what == START_FADE_OUT && mFadingEnabled) { fade(false); } } }; private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = new VerticalGridView.OnTouchInterceptListener() { public boolean onInterceptTouchEvent(MotionEvent event) { return onInterceptInputEvent(event); } }; private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = new VerticalGridView.OnKeyInterceptListener() { public boolean onInterceptKeyEvent(KeyEvent event) { return onInterceptInputEvent(event); } }; private void setBgAlpha(int alpha) { mBgAlpha = alpha; if (mRootView != null) { mRootView.getBackground().setAlpha(alpha); } } private void enableVerticalGridAnimations(boolean enable) { if (getVerticalGridView() != null) { getVerticalGridView().setAnimateChildLayout(enable); } } private void resetControlsToPrimaryActions(ItemBridgeAdapter.ViewHolder vh) { if (vh == null && getVerticalGridView() != null) { vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView().findViewHolderForPosition(0); } if (vh == null) { mResetControlsToPrimaryActionsPending = true; } else if (vh.getPresenter() instanceof PlaybackControlsRowPresenter) { mResetControlsToPrimaryActionsPending = false; ((PlaybackControlsRowPresenter) vh.getPresenter()).showPrimaryActions( (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder()); } } /** * Enables or disables view fading. If enabled, * the view will be faded in when the fragment starts, * and will fade out after a time period. The timeout * period is reset each time {@link #tickle} is called. * */ public void setFadingEnabled(boolean enabled) { if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled); if (enabled != mFadingEnabled) { mFadingEnabled = enabled; if (mFadingEnabled) { if (isResumed() && mFadingStatus == IDLE && !mHandler.hasMessages(START_FADE_OUT)) { startFadeTimer(); } } else { // Ensure fully opaque mHandler.removeMessages(START_FADE_OUT); fade(true); } } } /** * Returns true if view fading is enabled. */ public boolean isFadingEnabled() { return mFadingEnabled; } /** * Sets the listener to be called when fade in or out has completed. */ public void setFadeCompleteListener(OnFadeCompleteListener listener) { mFadeCompleteListener = listener; } /** * Returns the listener to be called when fade in or out has completed. */ public OnFadeCompleteListener getFadeCompleteListener() { return mFadeCompleteListener; } /** * Sets the input event handler. */ public final void setInputEventHandler(InputEventHandler handler) { mInputEventHandler = handler; } /** * Returns the input event handler. */ public final InputEventHandler getInputEventHandler() { return mInputEventHandler; } /** * Tickles the playback controls. Fades in the view if it was faded out, * otherwise resets the fade out timer. Tickling on input events is handled * by the fragment. */ public void tickle() { if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); if (!mFadingEnabled || !isResumed()) { return; } if (mHandler.hasMessages(START_FADE_OUT)) { // Restart the timer startFadeTimer(); } else { fade(true); } } /** * Fades out the playback overlay immediately. */ public void fadeOut() { mHandler.removeMessages(START_FADE_OUT); fade(false); } private boolean areControlsHidden() { return mFadingStatus == IDLE && mBgAlpha == 0; } private boolean onInterceptInputEvent(InputEvent event) { final boolean controlsHidden = areControlsHidden(); if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event); boolean consumeEvent = false; int keyCode = KeyEvent.KEYCODE_UNKNOWN; if (mInputEventHandler != null) { consumeEvent = mInputEventHandler.handleInputEvent(event); } if (event instanceof KeyEvent) { keyCode = ((KeyEvent) event).getKeyCode(); } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: // Event may be consumed; regardless, if controls are hidden then these keys will // bring up the controls. if (controlsHidden) { consumeEvent = true; } tickle(); break; case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_ESCAPE: // If fading enabled and controls are not hidden, back will be consumed to fade // them out (even if the key was consumed by the handler). if (mFadingEnabled && !controlsHidden) { consumeEvent = true; mHandler.removeMessages(START_FADE_OUT); fade(false); } else if (consumeEvent) { tickle(); } break; default: if (consumeEvent) { tickle(); } } return consumeEvent; } @Override public void onResume() { super.onResume(); if (mFadingEnabled) { setBgAlpha(0); fade(true); } getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); } private void startFadeTimer() { if (mHandler != null) { mHandler.removeMessages(START_FADE_OUT); mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs); } } private static ValueAnimator loadAnimator(Context context, int resId) { ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); return animator; } private void loadBgAnimator() { AnimatorUpdateListener listener = new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator arg0) { setBgAlpha((Integer) arg0.getAnimatedValue()); } }; mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in); mBgFadeInAnimator.addUpdateListener(listener); mBgFadeInAnimator.addListener(mFadeListener); mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out); mBgFadeOutAnimator.addUpdateListener(listener); mBgFadeOutAnimator.addListener(mFadeListener); } private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100,0); private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100,0); private View getControlRowView() { if (getVerticalGridView() == null) { return null; } RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0); if (vh == null) { return null; } return vh.itemView; } private void loadControlRowAnimator() { final AnimatorListener listener = new AnimatorListener() { @Override void getViews(ArrayList