Home | History | Annotate | Download | only in menu
      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