1/* 2 * Copyright (C) 2015 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 17package com.android.tv.menu; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.animation.TimeInterpolator; 24import android.content.Context; 25import android.content.res.Resources; 26import android.graphics.Rect; 27import android.support.annotation.UiThread; 28import android.support.v4.view.animation.FastOutLinearInInterpolator; 29import android.support.v4.view.animation.FastOutSlowInInterpolator; 30import android.support.v4.view.animation.LinearOutSlowInInterpolator; 31import android.support.v7.widget.RecyclerView; 32import android.util.Log; 33import android.util.Property; 34import android.view.View; 35import android.view.ViewGroup.MarginLayoutParams; 36import android.widget.TextView; 37import com.android.tv.R; 38import com.android.tv.common.SoftPreconditions; 39import com.android.tv.util.Utils; 40import java.util.ArrayList; 41import java.util.Collections; 42import java.util.HashMap; 43import java.util.List; 44import java.util.Map; 45import java.util.Map.Entry; 46import java.util.concurrent.TimeUnit; 47 48/** A view that represents TV main menu. */ 49@UiThread 50public class MenuLayoutManager { 51 static final String TAG = "MenuLayoutManager"; 52 static final boolean DEBUG = false; 53 54 // The visible duration of the title before it is hidden. 55 private static final long TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS = TimeUnit.SECONDS.toMillis(2); 56 private static final int INVALID_POSITION = -1; 57 58 private final MenuView mMenuView; 59 private final List<MenuRow> mMenuRows = new ArrayList<>(); 60 private final List<MenuRowView> mMenuRowViews = new ArrayList<>(); 61 private final List<Integer> mRemovingRowViews = new ArrayList<>(); 62 private int mSelectedPosition = INVALID_POSITION; 63 private int mPendingSelectedPosition = INVALID_POSITION; 64 65 private final int mRowAlignFromBottom; 66 private final int mRowContentsPaddingTop; 67 private final int mRowContentsPaddingBottomMax; 68 private final int mRowTitleTextDescenderHeight; 69 private final int mMenuMarginBottomMin; 70 private final int mRowTitleHeight; 71 private final int mRowScrollUpAnimationOffset; 72 73 private final long mRowAnimationDuration; 74 private final long mOldContentsFadeOutDuration; 75 private final long mCurrentContentsFadeInDuration; 76 private final TimeInterpolator mFastOutSlowIn = new FastOutSlowInInterpolator(); 77 private final TimeInterpolator mFastOutLinearIn = new FastOutLinearInInterpolator(); 78 private final TimeInterpolator mLinearOutSlowIn = new LinearOutSlowInInterpolator(); 79 private AnimatorSet mAnimatorSet; 80 private ObjectAnimator mTitleFadeOutAnimator; 81 private final List<ViewPropertyValueHolder> mPropertyValuesAfterAnimation = new ArrayList<>(); 82 83 private TextView mTempTitleViewForOld; 84 private TextView mTempTitleViewForCurrent; 85 86 public MenuLayoutManager(Context context, MenuView menuView) { 87 mMenuView = menuView; 88 // Load dimensions 89 Resources res = context.getResources(); 90 mRowAlignFromBottom = res.getDimensionPixelOffset(R.dimen.menu_row_align_from_bottom); 91 mRowContentsPaddingTop = res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_top); 92 mRowContentsPaddingBottomMax = 93 res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_bottom_max); 94 mRowTitleTextDescenderHeight = 95 res.getDimensionPixelOffset(R.dimen.menu_row_title_text_descender_height); 96 mMenuMarginBottomMin = res.getDimensionPixelOffset(R.dimen.menu_margin_bottom_min); 97 mRowTitleHeight = res.getDimensionPixelSize(R.dimen.menu_row_title_height); 98 mRowScrollUpAnimationOffset = 99 res.getDimensionPixelOffset(R.dimen.menu_row_scroll_up_anim_offset); 100 mRowAnimationDuration = res.getInteger(R.integer.menu_row_selection_anim_duration); 101 mOldContentsFadeOutDuration = 102 res.getInteger(R.integer.menu_previous_contents_fade_out_duration); 103 mCurrentContentsFadeInDuration = 104 res.getInteger(R.integer.menu_current_contents_fade_in_duration); 105 } 106 107 /** Sets the menu rows and views. */ 108 public void setMenuRowsAndViews(List<MenuRow> menuRows, List<MenuRowView> menuRowViews) { 109 mMenuRows.clear(); 110 mMenuRows.addAll(menuRows); 111 mMenuRowViews.clear(); 112 mMenuRowViews.addAll(menuRowViews); 113 } 114 115 /** 116 * Layouts main menu view. 117 * 118 * <p>Do not call this method directly. It's supposed to be called only by View.onLayout(). 119 */ 120 public void layout(int left, int top, int right, int bottom) { 121 if (mAnimatorSet != null) { 122 // Layout will be done after the animation ends. 123 return; 124 } 125 126 int count = mMenuRowViews.size(); 127 MenuRowView currentView = mMenuRowViews.get(mSelectedPosition); 128 if (currentView.getVisibility() == View.GONE) { 129 // If the selected row is not visible, select the first visible row. 130 int firstVisiblePosition = findNextVisiblePosition(INVALID_POSITION); 131 if (firstVisiblePosition != INVALID_POSITION) { 132 mSelectedPosition = firstVisiblePosition; 133 } else { 134 // No rows are visible. 135 return; 136 } 137 } 138 List<Rect> layouts = getViewLayouts(left, top, right, bottom); 139 for (int i = 0; i < count; ++i) { 140 Rect rect = layouts.get(i); 141 if (rect != null) { 142 currentView = mMenuRowViews.get(i); 143 currentView.layout(rect.left, rect.top, rect.right, rect.bottom); 144 if (DEBUG) dumpChildren("layout()"); 145 } 146 } 147 148 // If the contents view is INVISIBLE initially, it should be changed to GONE after layout. 149 // See MenuRowView.onFinishInflate() for more information 150 // TODO: Find a better way to resolve this issue.. 151 for (MenuRowView view : mMenuRowViews) { 152 if (view.getVisibility() == View.VISIBLE 153 && view.getContentsView().getVisibility() == View.INVISIBLE) { 154 view.onDeselected(); 155 } 156 } 157 158 if (mPendingSelectedPosition != INVALID_POSITION) { 159 setSelectedPositionSmooth(mPendingSelectedPosition); 160 } 161 } 162 163 private int findNextVisiblePosition(int start) { 164 int count = mMenuRowViews.size(); 165 for (int i = start + 1; i < count; ++i) { 166 if (mMenuRowViews.get(i).getVisibility() != View.GONE) { 167 return i; 168 } 169 } 170 return INVALID_POSITION; 171 } 172 173 private void dumpChildren(String prefix) { 174 int position = 0; 175 for (MenuRowView view : mMenuRowViews) { 176 View title = view.getChildAt(0); 177 View contents = view.getChildAt(1); 178 Log.d( 179 TAG, 180 prefix 181 + " position=" 182 + position++ 183 + " rowView={visiblility=" 184 + view.getVisibility() 185 + ", alpha=" 186 + view.getAlpha() 187 + ", translationY=" 188 + view.getTranslationY() 189 + ", left=" 190 + view.getLeft() 191 + ", top=" 192 + view.getTop() 193 + ", right=" 194 + view.getRight() 195 + ", bottom=" 196 + view.getBottom() 197 + "}, title={visiblility=" 198 + title.getVisibility() 199 + ", alpha=" 200 + title.getAlpha() 201 + ", translationY=" 202 + title.getTranslationY() 203 + ", left=" 204 + title.getLeft() 205 + ", top=" 206 + title.getTop() 207 + ", right=" 208 + title.getRight() 209 + ", bottom=" 210 + title.getBottom() 211 + "}, contents={visiblility=" 212 + contents.getVisibility() 213 + ", alpha=" 214 + contents.getAlpha() 215 + ", translationY=" 216 + contents.getTranslationY() 217 + ", left=" 218 + contents.getLeft() 219 + ", top=" 220 + contents.getTop() 221 + ", right=" 222 + contents.getRight() 223 + ", bottom=" 224 + contents.getBottom() 225 + "}"); 226 } 227 } 228 229 /** 230 * Checks if the view will take up space for the layout not. 231 * 232 * @param position The index of the menu row view in the list. This is not the index of the view 233 * in the screen. 234 * @param view The menu row view. 235 * @param rowsToAdd The menu row views to be added in the next layout process. 236 * @param rowsToRemove The menu row views to be removed in the next layout process. 237 * @return {@code true} if the view will take up space for the layout, otherwise {@code false}. 238 */ 239 private boolean isVisibleInLayout( 240 int position, MenuRowView view, List<Integer> rowsToAdd, List<Integer> rowsToRemove) { 241 // Checks if the view will be visible or not. 242 return (view.getVisibility() != View.GONE && !rowsToRemove.contains(position)) 243 || rowsToAdd.contains(position); 244 } 245 246 /** 247 * Calculates and returns a list of the layout bounds of the menu row views for the layout. 248 * 249 * @param left The left coordinate of the menu view. 250 * @param top The top coordinate of the menu view. 251 * @param right The right coordinate of the menu view. 252 * @param bottom The bottom coordinate of the menu view. 253 */ 254 private List<Rect> getViewLayouts(int left, int top, int right, int bottom) { 255 return getViewLayouts( 256 left, top, right, bottom, Collections.emptyList(), Collections.emptyList()); 257 } 258 259 /** 260 * Calculates and returns a list of the layout bounds of the menu row views for the layout. The 261 * order of the bounds is the same as that of the menu row views. e.g. the second rectangle in 262 * the list is for the second menu row view in the view list (not the second view in the 263 * screen). 264 * 265 * <p>It predicts the layout bounds for the next layout process. Some views will be added or 266 * removed in the layout, so they need to be considered here. 267 * 268 * @param left The left coordinate of the menu view. 269 * @param top The top coordinate of the menu view. 270 * @param right The right coordinate of the menu view. 271 * @param bottom The bottom coordinate of the menu view. 272 * @param rowsToAdd The menu row views to be added in the next layout process. 273 * @param rowsToRemove The menu row views to be removed in the next layout process. 274 * @return the layout bounds of the menu row views. 275 */ 276 private List<Rect> getViewLayouts( 277 int left, 278 int top, 279 int right, 280 int bottom, 281 List<Integer> rowsToAdd, 282 List<Integer> rowsToRemove) { 283 // The coordinates should be relative to the parent. 284 int relativeLeft = 0; 285 int relateiveRight = right - left; 286 int relativeBottom = bottom - top; 287 288 List<Rect> layouts = new ArrayList<>(); 289 int count = mMenuRowViews.size(); 290 MenuRowView selectedView = mMenuRowViews.get(mSelectedPosition); 291 int rowTitleHeight = selectedView.getTitleView().getMeasuredHeight(); 292 int rowContentsHeight = selectedView.getPreferredContentsHeight(); 293 // Calculate for the selected row first. 294 // The distance between the bottom of the screen and the vertical center of the contents 295 // should be kept fixed. For more information, please see the redlines. 296 int childTop = 297 relativeBottom 298 - mRowAlignFromBottom 299 - rowContentsHeight / 2 300 - mRowContentsPaddingTop 301 - rowTitleHeight; 302 int childBottom = relativeBottom; 303 int position = mSelectedPosition + 1; 304 for (; position < count; ++position) { 305 // Find and layout the next row to calculate the bottom line of the selected row. 306 MenuRowView nextView = mMenuRowViews.get(position); 307 if (isVisibleInLayout(position, nextView, rowsToAdd, rowsToRemove)) { 308 int nextTitleTopMax = 309 relativeBottom 310 - mMenuMarginBottomMin 311 - rowTitleHeight 312 + mRowTitleTextDescenderHeight; 313 int childBottomMax = 314 relativeBottom 315 - mRowAlignFromBottom 316 + rowContentsHeight / 2 317 + mRowContentsPaddingBottomMax 318 - rowTitleHeight; 319 childBottom = Math.min(nextTitleTopMax, childBottomMax); 320 layouts.add(new Rect(relativeLeft, childBottom, relateiveRight, relativeBottom)); 321 break; 322 } else { 323 // null means that the row is GONE. 324 layouts.add(null); 325 } 326 } 327 layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom)); 328 // Layout the previous rows. 329 for (int i = mSelectedPosition - 1; i >= 0; --i) { 330 MenuRowView view = mMenuRowViews.get(i); 331 if (isVisibleInLayout(i, view, rowsToAdd, rowsToRemove)) { 332 childTop -= mRowTitleHeight; 333 childBottom = childTop + rowTitleHeight; 334 layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom)); 335 } else { 336 layouts.add(0, null); 337 } 338 } 339 // Move all the next rows to the below of the screen. 340 childTop = relativeBottom; 341 for (++position; position < count; ++position) { 342 MenuRowView view = mMenuRowViews.get(position); 343 if (isVisibleInLayout(position, view, rowsToAdd, rowsToRemove)) { 344 childBottom = childTop + rowTitleHeight; 345 layouts.add(new Rect(relativeLeft, childTop, relateiveRight, childBottom)); 346 childTop += mRowTitleHeight; 347 } else { 348 layouts.add(null); 349 } 350 } 351 return layouts; 352 } 353 354 /** Move the current selection to the given {@code position}. */ 355 public void setSelectedPosition(int position) { 356 if (DEBUG) { 357 Log.d( 358 TAG, 359 "setSelectedPosition(position=" 360 + position 361 + ") {previousPosition=" 362 + mSelectedPosition 363 + "}"); 364 } 365 if (mSelectedPosition == position) { 366 return; 367 } 368 boolean indexValid = Utils.isIndexValid(mMenuRowViews, position); 369 SoftPreconditions.checkArgument(indexValid, TAG, "position %s ", position); 370 if (!indexValid) { 371 return; 372 } 373 MenuRow row = mMenuRows.get(position); 374 if (!row.isVisible()) { 375 Log.e(TAG, "Selecting invisible row: " + position); 376 return; 377 } 378 if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) { 379 mMenuRowViews.get(mSelectedPosition).onDeselected(); 380 } 381 mSelectedPosition = position; 382 mPendingSelectedPosition = INVALID_POSITION; 383 if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) { 384 mMenuRowViews.get(mSelectedPosition).onSelected(false); 385 } 386 if (mMenuView.getVisibility() == View.VISIBLE) { 387 // Request focus after the new contents view shows up. 388 mMenuView.requestFocus(); 389 // Adjust the position of the selected row. 390 mMenuView.requestLayout(); 391 } 392 } 393 394 /** 395 * Move the current selection to the given {@code position} with animation. The animation 396 * specification is included in http://b/21069476 397 */ 398 public void setSelectedPositionSmooth(final int position) { 399 if (DEBUG) { 400 Log.d( 401 TAG, 402 "setSelectedPositionSmooth(position=" 403 + position 404 + ") {previousPosition=" 405 + mSelectedPosition 406 + "}"); 407 } 408 if (mMenuView.getVisibility() != View.VISIBLE) { 409 setSelectedPosition(position); 410 return; 411 } 412 if (mSelectedPosition == position) { 413 return; 414 } 415 boolean oldIndexValid = Utils.isIndexValid(mMenuRowViews, mSelectedPosition); 416 SoftPreconditions.checkState( 417 oldIndexValid, TAG, "No previous selection: " + mSelectedPosition); 418 if (!oldIndexValid) { 419 return; 420 } 421 boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position); 422 SoftPreconditions.checkArgument(newIndexValid, TAG, "position %s", position); 423 if (!newIndexValid) { 424 return; 425 } 426 MenuRow row = mMenuRows.get(position); 427 if (!row.isVisible()) { 428 Log.e(TAG, "Moving to the invisible row: " + position); 429 return; 430 } 431 if (mAnimatorSet != null) { 432 // Do not cancel the animation here. The property values should be set to the end values 433 // when the animation finishes. 434 mAnimatorSet.end(); 435 } 436 if (mTitleFadeOutAnimator != null) { 437 // Cancel the animation instead of ending it in order that the title animation starts 438 // again from the intermediate state. 439 mTitleFadeOutAnimator.cancel(); 440 } 441 if (DEBUG) dumpChildren("startRowAnimation()"); 442 443 // Show the children of the next row. 444 final MenuRowView currentView = mMenuRowViews.get(position); 445 TextView currentTitleView = currentView.getTitleView(); 446 View currentContentsView = currentView.getContentsView(); 447 currentTitleView.setVisibility(View.VISIBLE); 448 currentContentsView.setVisibility(View.VISIBLE); 449 if (currentView instanceof PlayControlsRowView) { 450 ((PlayControlsRowView) currentView).onPreselected(); 451 } 452 // When contents view's visibility is gone, layouting might be delayed until it's shown and 453 // thus cause onBindViewHolder() and menu action updating occurs in front of users' sight. 454 // Therefore we call requestLayout() here if there are pending adapter updates. 455 if (currentContentsView instanceof RecyclerView 456 && ((RecyclerView) currentContentsView).hasPendingAdapterUpdates()) { 457 currentContentsView.requestLayout(); 458 mPendingSelectedPosition = position; 459 return; 460 } 461 final int oldPosition = mSelectedPosition; 462 mSelectedPosition = position; 463 mPendingSelectedPosition = INVALID_POSITION; 464 // Request focus after the new contents view shows up. 465 mMenuView.requestFocus(); 466 if (mTempTitleViewForOld == null) { 467 // Initialize here because we don't know when the views are inflated. 468 mTempTitleViewForOld = (TextView) mMenuView.findViewById(R.id.temp_title_for_old); 469 mTempTitleViewForCurrent = 470 (TextView) mMenuView.findViewById(R.id.temp_title_for_current); 471 } 472 473 // Animations. 474 mPropertyValuesAfterAnimation.clear(); 475 List<Animator> animators = new ArrayList<>(); 476 boolean scrollDown = position > oldPosition; 477 List<Rect> layouts = 478 getViewLayouts( 479 mMenuView.getLeft(), 480 mMenuView.getTop(), 481 mMenuView.getRight(), 482 mMenuView.getBottom()); 483 484 // Old row. 485 MenuRow oldRow = mMenuRows.get(oldPosition); 486 final MenuRowView oldView = mMenuRowViews.get(oldPosition); 487 View oldContentsView = oldView.getContentsView(); 488 // Old contents view. 489 animators.add( 490 createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) 491 .setDuration(mOldContentsFadeOutDuration)); 492 final TextView oldTitleView = oldView.getTitleView(); 493 setTempTitleView(mTempTitleViewForOld, oldTitleView); 494 Rect oldLayoutRect = layouts.get(oldPosition); 495 if (scrollDown) { 496 // Old title view. 497 if (oldRow.hideTitleWhenSelected() && oldTitleView.getVisibility() != View.VISIBLE) { 498 // This case is not included in the animation specification. 499 mTempTitleViewForOld.setScaleX(1.0f); 500 mTempTitleViewForOld.setScaleY(1.0f); 501 animators.add( 502 createAlphaAnimator( 503 mTempTitleViewForOld, 504 0.0f, 505 oldView.getTitleViewAlphaDeselected(), 506 mFastOutLinearIn)); 507 int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); 508 animators.add( 509 createTranslationYAnimator( 510 mTempTitleViewForOld, 511 offset + mRowScrollUpAnimationOffset, 512 offset)); 513 } else { 514 animators.add( 515 createScaleXAnimator( 516 mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f)); 517 animators.add( 518 createScaleYAnimator( 519 mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f)); 520 animators.add( 521 createAlphaAnimator( 522 mTempTitleViewForOld, 523 oldTitleView.getAlpha(), 524 oldView.getTitleViewAlphaDeselected(), 525 mLinearOutSlowIn)); 526 animators.add( 527 createTranslationYAnimator( 528 mTempTitleViewForOld, 529 0, 530 oldLayoutRect.top - mTempTitleViewForOld.getTop())); 531 } 532 oldTitleView.setAlpha(oldView.getTitleViewAlphaDeselected()); 533 oldTitleView.setVisibility(View.INVISIBLE); 534 } else { 535 Rect currentLayoutRect = new Rect(layouts.get(position)); 536 // Old title view. 537 // The move distance in the specification is 32dp(mRowScrollUpAnimationOffset). 538 // But if the height of the upper row is small, the upper row will move down a lot. In 539 // this case, this row needs to move more than the specification to avoid the overlap of 540 // the two titles. 541 // The maximum is to the top of the start position of mTempTitleViewForOld. 542 int distanceCurrentTitle = currentLayoutRect.top - currentView.getTop(); 543 int distance = Math.max(mRowScrollUpAnimationOffset, distanceCurrentTitle); 544 int distanceToTopOfSecondTitle = 545 oldLayoutRect.top - mRowScrollUpAnimationOffset - oldView.getTop(); 546 animators.add( 547 createTranslationYAnimator( 548 oldTitleView, 0.0f, Math.min(distance, distanceToTopOfSecondTitle))); 549 animators.add( 550 createAlphaAnimator(oldTitleView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) 551 .setDuration(mOldContentsFadeOutDuration)); 552 animators.add( 553 createScaleXAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f)); 554 animators.add( 555 createScaleYAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f)); 556 mTempTitleViewForOld.setScaleX(1.0f); 557 mTempTitleViewForOld.setScaleY(1.0f); 558 animators.add( 559 createAlphaAnimator( 560 mTempTitleViewForOld, 561 0.0f, 562 oldView.getTitleViewAlphaDeselected(), 563 mFastOutLinearIn)); 564 int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); 565 animators.add( 566 createTranslationYAnimator( 567 mTempTitleViewForOld, offset - mRowScrollUpAnimationOffset, offset)); 568 } 569 // Current row. 570 Rect currentLayoutRect = new Rect(layouts.get(position)); 571 currentContentsView.setAlpha(0.0f); 572 if (scrollDown) { 573 // Current title view. 574 setTempTitleView(mTempTitleViewForCurrent, currentTitleView); 575 // The move distance in the specification is 32dp(mRowScrollUpAnimationOffset). 576 // But if the height of the upper row is small, the upper row will move up a lot. In 577 // this case, this row needs to start the move from more than the specification to avoid 578 // the overlap of the two titles. 579 // The maximum is to the top of the end position of mTempTitleViewForCurrent. 580 int distanceOldTitle = oldView.getTop() - oldLayoutRect.top; 581 int distance = Math.max(mRowScrollUpAnimationOffset, distanceOldTitle); 582 int distanceTopOfSecondTitle = 583 currentView.getTop() - mRowScrollUpAnimationOffset - currentLayoutRect.top; 584 animators.add( 585 createTranslationYAnimator( 586 currentTitleView, Math.min(distance, distanceTopOfSecondTitle), 0.0f)); 587 currentView.setTop(currentLayoutRect.top); 588 ObjectAnimator animator = 589 createAlphaAnimator(currentTitleView, 0.0f, 1.0f, mFastOutLinearIn) 590 .setDuration(mCurrentContentsFadeInDuration); 591 animator.setStartDelay(mOldContentsFadeOutDuration); 592 currentTitleView.setAlpha(0.0f); 593 animators.add(animator); 594 animators.add( 595 createScaleXAnimator( 596 currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); 597 animators.add( 598 createScaleYAnimator( 599 currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); 600 animators.add( 601 createTranslationYAnimator( 602 mTempTitleViewForCurrent, 0.0f, -mRowScrollUpAnimationOffset)); 603 animators.add( 604 createAlphaAnimator( 605 mTempTitleViewForCurrent, 606 currentView.getTitleViewAlphaDeselected(), 607 0, 608 mLinearOutSlowIn)); 609 // Current contents view. 610 animators.add( 611 createTranslationYAnimator( 612 currentContentsView, mRowScrollUpAnimationOffset, 0.0f)); 613 animator = 614 createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) 615 .setDuration(mCurrentContentsFadeInDuration); 616 animator.setStartDelay(mOldContentsFadeOutDuration); 617 animators.add(animator); 618 } else { 619 currentView.setBottom(currentLayoutRect.bottom); 620 // Current title view. 621 int currentViewOffset = currentLayoutRect.top - currentView.getTop(); 622 animators.add(createTranslationYAnimator(currentTitleView, 0, currentViewOffset)); 623 animators.add( 624 createAlphaAnimator( 625 currentTitleView, 626 currentView.getTitleViewAlphaDeselected(), 627 1.0f, 628 mFastOutSlowIn)); 629 animators.add( 630 createScaleXAnimator( 631 currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); 632 animators.add( 633 createScaleYAnimator( 634 currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); 635 // Current contents view. 636 animators.add( 637 createTranslationYAnimator( 638 currentContentsView, 639 currentViewOffset - mRowScrollUpAnimationOffset, 640 currentViewOffset)); 641 ObjectAnimator animator = 642 createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) 643 .setDuration(mCurrentContentsFadeInDuration); 644 animator.setStartDelay(mOldContentsFadeOutDuration); 645 animators.add(animator); 646 } 647 // Next row. 648 int nextPosition; 649 if (scrollDown) { 650 nextPosition = findNextVisiblePosition(position); 651 if (nextPosition != INVALID_POSITION) { 652 MenuRowView nextView = mMenuRowViews.get(nextPosition); 653 Rect nextLayoutRect = layouts.get(nextPosition); 654 animators.add( 655 createTranslationYAnimator( 656 nextView, 657 nextLayoutRect.top 658 + mRowScrollUpAnimationOffset 659 - nextView.getTop(), 660 nextLayoutRect.top - nextView.getTop())); 661 animators.add(createAlphaAnimator(nextView, 0.0f, 1.0f, mFastOutLinearIn)); 662 } 663 } else { 664 nextPosition = findNextVisiblePosition(oldPosition); 665 if (nextPosition != INVALID_POSITION) { 666 MenuRowView nextView = mMenuRowViews.get(nextPosition); 667 animators.add(createTranslationYAnimator(nextView, 0, mRowScrollUpAnimationOffset)); 668 animators.add( 669 createAlphaAnimator( 670 nextView, 671 nextView.getTitleViewAlphaDeselected(), 672 0.0f, 673 1.0f, 674 mLinearOutSlowIn)); 675 } 676 } 677 // Other rows. 678 int count = mMenuRowViews.size(); 679 for (int i = 0; i < count; ++i) { 680 MenuRowView view = mMenuRowViews.get(i); 681 if (view.getVisibility() == View.VISIBLE 682 && i != oldPosition 683 && i != position 684 && i != nextPosition) { 685 Rect rect = layouts.get(i); 686 animators.add(createTranslationYAnimator(view, 0, rect.top - view.getTop())); 687 } 688 } 689 // Run animation. 690 final List<ViewPropertyValueHolder> propertyValuesAfterAnimation = new ArrayList<>(); 691 propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation); 692 mAnimatorSet = new AnimatorSet(); 693 mAnimatorSet.playTogether(animators); 694 mAnimatorSet.addListener( 695 new AnimatorListenerAdapter() { 696 @Override 697 public void onAnimationEnd(Animator animator) { 698 if (DEBUG) dumpChildren("onRowAnimationEndBefore"); 699 mAnimatorSet = null; 700 // The property values which are different from the end values and need to 701 // be 702 // changed after the animation are set here. 703 // e.g. setting translationY to 0, alpha of the contents view to 1. 704 for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { 705 holder.property.set(holder.view, holder.value); 706 } 707 oldView.onDeselected(); 708 currentView.onSelected(true); 709 mTempTitleViewForOld.setVisibility(View.GONE); 710 mTempTitleViewForCurrent.setVisibility(View.GONE); 711 layout( 712 mMenuView.getLeft(), 713 mMenuView.getTop(), 714 mMenuView.getRight(), 715 mMenuView.getBottom()); 716 if (DEBUG) dumpChildren("onRowAnimationEndAfter"); 717 718 MenuRow currentRow = mMenuRows.get(position); 719 if (currentRow.hideTitleWhenSelected()) { 720 View titleView = mMenuRowViews.get(position).getTitleView(); 721 mTitleFadeOutAnimator = 722 createAlphaAnimator( 723 titleView, 724 titleView.getAlpha(), 725 0.0f, 726 mLinearOutSlowIn); 727 mTitleFadeOutAnimator.setStartDelay( 728 TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS); 729 mTitleFadeOutAnimator.addListener( 730 new AnimatorListenerAdapter() { 731 private boolean mCanceled; 732 733 @Override 734 public void onAnimationCancel(Animator animator) { 735 mCanceled = true; 736 } 737 738 @Override 739 public void onAnimationEnd(Animator animator) { 740 mTitleFadeOutAnimator = null; 741 if (!mCanceled) { 742 mMenuRowViews.get(position).onSelected(false); 743 } 744 } 745 }); 746 mTitleFadeOutAnimator.start(); 747 } 748 } 749 }); 750 mAnimatorSet.start(); 751 if (DEBUG) dumpChildren("startedRowAnimation()"); 752 } 753 754 private void setTempTitleView(TextView dest, TextView src) { 755 dest.setVisibility(View.VISIBLE); 756 dest.setText(src.getText()); 757 dest.setTranslationY(0.0f); 758 if (src.getVisibility() == View.VISIBLE) { 759 dest.setAlpha(src.getAlpha()); 760 dest.setScaleX(src.getScaleX()); 761 dest.setScaleY(src.getScaleY()); 762 } else { 763 dest.setAlpha(0.0f); 764 dest.setScaleX(1.0f); 765 dest.setScaleY(1.0f); 766 } 767 View parent = (View) src.getParent(); 768 dest.setLeft(src.getLeft() + parent.getLeft()); 769 dest.setRight(src.getRight() + parent.getLeft()); 770 dest.setTop(src.getTop() + parent.getTop()); 771 dest.setBottom(src.getBottom() + parent.getTop()); 772 } 773 774 /** 775 * Called when the menu row information is updated. The add/remove animation of the row views 776 * will be started. 777 * 778 * <p>Note that the current row should not be removed. 779 */ 780 public void onMenuRowUpdated() { 781 if (mMenuView.getVisibility() != View.VISIBLE) { 782 int count = mMenuRowViews.size(); 783 for (int i = 0; i < count; ++i) { 784 mMenuRowViews 785 .get(i) 786 .setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE : View.GONE); 787 } 788 return; 789 } 790 791 List<Integer> addedRowViews = new ArrayList<>(); 792 List<Integer> removedRowViews = new ArrayList<>(); 793 Map<Integer, Integer> offsetsToMove = new HashMap<>(); 794 int added = 0; 795 for (int i = mSelectedPosition - 1; i >= 0; --i) { 796 MenuRow row = mMenuRows.get(i); 797 MenuRowView view = mMenuRowViews.get(i); 798 if (row.isVisible() 799 && (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) { 800 // Removing rows are still VISIBLE. 801 addedRowViews.add(i); 802 ++added; 803 } else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) { 804 removedRowViews.add(i); 805 --added; 806 } else if (added != 0) { 807 offsetsToMove.put(i, -added); 808 } 809 } 810 added = 0; 811 int count = mMenuRowViews.size(); 812 for (int i = mSelectedPosition + 1; i < count; ++i) { 813 MenuRow row = mMenuRows.get(i); 814 MenuRowView view = mMenuRowViews.get(i); 815 if (row.isVisible() 816 && (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) { 817 // Removing rows are still VISIBLE. 818 addedRowViews.add(i); 819 ++added; 820 } else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) { 821 removedRowViews.add(i); 822 --added; 823 } else if (added != 0) { 824 offsetsToMove.put(i, added); 825 } 826 } 827 if (addedRowViews.size() == 0 && removedRowViews.size() == 0) { 828 return; 829 } 830 831 if (mAnimatorSet != null) { 832 // Do not cancel the animation here. The property values should be set to the end values 833 // when the animation finishes. 834 mAnimatorSet.end(); 835 } 836 if (mTitleFadeOutAnimator != null) { 837 mTitleFadeOutAnimator.end(); 838 } 839 mPropertyValuesAfterAnimation.clear(); 840 List<Animator> animators = new ArrayList<>(); 841 List<Rect> layouts = 842 getViewLayouts( 843 mMenuView.getLeft(), 844 mMenuView.getTop(), 845 mMenuView.getRight(), 846 mMenuView.getBottom(), 847 addedRowViews, 848 removedRowViews); 849 for (int position : addedRowViews) { 850 MenuRowView view = mMenuRowViews.get(position); 851 view.setVisibility(View.VISIBLE); 852 Rect rect = layouts.get(position); 853 // TODO: The animation is not visible when it is shown for the first time. Need to find 854 // a better way to resolve this issue. 855 view.layout(rect.left, rect.top, rect.right, rect.bottom); 856 View titleView = view.getTitleView(); 857 MarginLayoutParams params = (MarginLayoutParams) titleView.getLayoutParams(); 858 titleView.layout( 859 view.getPaddingLeft() + params.leftMargin, 860 view.getPaddingTop() + params.topMargin, 861 rect.right - rect.left - view.getPaddingRight() - params.rightMargin, 862 rect.bottom - rect.top - view.getPaddingBottom() - params.bottomMargin); 863 animators.add(createAlphaAnimator(view, 0.0f, 1.0f, mFastOutLinearIn)); 864 } 865 for (int position : removedRowViews) { 866 MenuRowView view = mMenuRowViews.get(position); 867 animators.add(createAlphaAnimator(view, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn)); 868 } 869 for (Entry<Integer, Integer> entry : offsetsToMove.entrySet()) { 870 MenuRowView view = mMenuRowViews.get(entry.getKey()); 871 animators.add(createTranslationYAnimator(view, 0, entry.getValue() * mRowTitleHeight)); 872 } 873 // Run animation. 874 final List<ViewPropertyValueHolder> propertyValuesAfterAnimation = new ArrayList<>(); 875 propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation); 876 mRemovingRowViews.clear(); 877 mRemovingRowViews.addAll(removedRowViews); 878 mAnimatorSet = new AnimatorSet(); 879 mAnimatorSet.playTogether(animators); 880 mAnimatorSet.addListener( 881 new AnimatorListenerAdapter() { 882 @Override 883 public void onAnimationEnd(Animator animation) { 884 mAnimatorSet = null; 885 // The property values which are different from the end values and need to 886 // be 887 // changed after the animation are set here. 888 // e.g. setting translationY to 0, alpha of the contents view to 1. 889 for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { 890 holder.property.set(holder.view, holder.value); 891 } 892 for (int position : mRemovingRowViews) { 893 mMenuRowViews.get(position).setVisibility(View.GONE); 894 } 895 layout( 896 mMenuView.getLeft(), 897 mMenuView.getTop(), 898 mMenuView.getRight(), 899 mMenuView.getBottom()); 900 } 901 }); 902 mAnimatorSet.start(); 903 if (DEBUG) dumpChildren("onMenuRowUpdated()"); 904 } 905 906 private ObjectAnimator createTranslationYAnimator(View view, float from, float to) { 907 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, from, to); 908 animator.setDuration(mRowAnimationDuration); 909 animator.setInterpolator(mFastOutSlowIn); 910 mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.TRANSLATION_Y, view, 0)); 911 return animator; 912 } 913 914 private ObjectAnimator createAlphaAnimator( 915 View view, float from, float to, TimeInterpolator interpolator) { 916 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); 917 animator.setDuration(mRowAnimationDuration); 918 animator.setInterpolator(interpolator); 919 return animator; 920 } 921 922 private ObjectAnimator createAlphaAnimator( 923 View view, float from, float to, float end, TimeInterpolator interpolator) { 924 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); 925 animator.setDuration(mRowAnimationDuration); 926 animator.setInterpolator(interpolator); 927 mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.ALPHA, view, end)); 928 return animator; 929 } 930 931 private ObjectAnimator createScaleXAnimator(View view, float from, float to) { 932 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_X, from, to); 933 animator.setDuration(mRowAnimationDuration); 934 animator.setInterpolator(mFastOutSlowIn); 935 return animator; 936 } 937 938 private ObjectAnimator createScaleYAnimator(View view, float from, float to) { 939 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_Y, from, to); 940 animator.setDuration(mRowAnimationDuration); 941 animator.setInterpolator(mFastOutSlowIn); 942 return animator; 943 } 944 945 /** Returns the current position. */ 946 public int getSelectedPosition() { 947 return mSelectedPosition; 948 } 949 950 private static final class ViewPropertyValueHolder { 951 public final Property<View, Float> property; 952 public final View view; 953 public final float value; 954 955 public ViewPropertyValueHolder(Property<View, Float> property, View view, float value) { 956 this.property = property; 957 this.view = view; 958 this.value = value; 959 } 960 } 961 962 /** Called when the menu becomes visible. */ 963 public void onMenuShow() {} 964 965 /** Called when the menu becomes hidden. */ 966 public void onMenuHide() { 967 if (mAnimatorSet != null) { 968 mAnimatorSet.end(); 969 mAnimatorSet = null; 970 } 971 // Should be finished after the animator set. 972 if (mTitleFadeOutAnimator != null) { 973 mTitleFadeOutAnimator.end(); 974 mTitleFadeOutAnimator = null; 975 } 976 } 977} 978