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