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