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