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 androidx.recyclerview.widget.RecyclerView;
29 import android.util.Log;
30 import android.util.Property;
31 import android.view.View;
32 import android.view.ViewGroup.MarginLayoutParams;
33 import android.widget.TextView;
34 import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
35 import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
36 import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
37 import com.android.tv.R;
38 import com.android.tv.common.SoftPreconditions;
39 import com.android.tv.util.Utils;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Map.Entry;
46 import java.util.concurrent.TimeUnit;
47 
48 /** A view that represents TV main menu. */
49 @UiThread
50 public 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 
MenuLayoutManager(Context context, MenuView menuView)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. */
setMenuRowsAndViews(List<MenuRow> menuRows, List<MenuRowView> menuRowViews)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      */
layout(int left, int top, int right, int bottom)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 
findNextVisiblePosition(int start)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 
dumpChildren(String prefix)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      */
isVisibleInLayout( int position, MenuRowView view, List<Integer> rowsToAdd, List<Integer> rowsToRemove)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      */
getViewLayouts(int left, int top, int right, int bottom)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      */
getViewLayouts( int left, int top, int right, int bottom, List<Integer> rowsToAdd, List<Integer> rowsToRemove)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}. */
setSelectedPosition(int 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      */
setSelectedPositionSmooth(final int position)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 
setTempTitleView(TextView dest, TextView src)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      */
onMenuRowUpdated()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 
createTranslationYAnimator(View view, float from, float to)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 
createAlphaAnimator( View view, float from, float to, TimeInterpolator interpolator)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 
createAlphaAnimator( View view, float from, float to, float end, TimeInterpolator interpolator)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 
createScaleXAnimator(View view, float from, float to)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 
createScaleYAnimator(View view, float from, float to)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. */
getSelectedPosition()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 
ViewPropertyValueHolder(Property<View, Float> property, View view, float value)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. */
onMenuShow()963     public void onMenuShow() {}
964 
965     /** Called when the menu becomes hidden. */
onMenuHide()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