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