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