blob: 8f95db7701a831f760c699db4296c71878de4ab6 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.menu;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.annotation.UiThread;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.util.Property;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.TextView;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
import com.android.tv.R;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
/** A view that represents TV main menu. */
@UiThread
public class MenuLayoutManager {
static final String TAG = "MenuLayoutManager";
static final boolean DEBUG = false;
// The visible duration of the title before it is hidden.
private static final long TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS = TimeUnit.SECONDS.toMillis(2);
private static final int INVALID_POSITION = -1;
private final MenuView mMenuView;
private final List<MenuRow> mMenuRows = new ArrayList<>();
private final List<MenuRowView> mMenuRowViews = new ArrayList<>();
private final List<Integer> mRemovingRowViews = new ArrayList<>();
private int mSelectedPosition = INVALID_POSITION;
private int mPendingSelectedPosition = INVALID_POSITION;
private final int mRowAlignFromBottom;
private final int mRowContentsPaddingTop;
private final int mRowContentsPaddingBottomMax;
private final int mRowTitleTextDescenderHeight;
private final int mMenuMarginBottomMin;
private final int mRowTitleHeight;
private final int mRowScrollUpAnimationOffset;
private final long mRowAnimationDuration;
private final long mOldContentsFadeOutDuration;
private final long mCurrentContentsFadeInDuration;
private final TimeInterpolator mFastOutSlowIn = new FastOutSlowInInterpolator();
private final TimeInterpolator mFastOutLinearIn = new FastOutLinearInInterpolator();
private final TimeInterpolator mLinearOutSlowIn = new LinearOutSlowInInterpolator();
private AnimatorSet mAnimatorSet;
private ObjectAnimator mTitleFadeOutAnimator;
private final List<ViewPropertyValueHolder> mPropertyValuesAfterAnimation = new ArrayList<>();
private TextView mTempTitleViewForOld;
private TextView mTempTitleViewForCurrent;
public MenuLayoutManager(Context context, MenuView menuView) {
mMenuView = menuView;
// Load dimensions
Resources res = context.getResources();
mRowAlignFromBottom = res.getDimensionPixelOffset(R.dimen.menu_row_align_from_bottom);
mRowContentsPaddingTop = res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_top);
mRowContentsPaddingBottomMax =
res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_bottom_max);
mRowTitleTextDescenderHeight =
res.getDimensionPixelOffset(R.dimen.menu_row_title_text_descender_height);
mMenuMarginBottomMin = res.getDimensionPixelOffset(R.dimen.menu_margin_bottom_min);
mRowTitleHeight = res.getDimensionPixelSize(R.dimen.menu_row_title_height);
mRowScrollUpAnimationOffset =
res.getDimensionPixelOffset(R.dimen.menu_row_scroll_up_anim_offset);
mRowAnimationDuration = res.getInteger(R.integer.menu_row_selection_anim_duration);
mOldContentsFadeOutDuration =
res.getInteger(R.integer.menu_previous_contents_fade_out_duration);
mCurrentContentsFadeInDuration =
res.getInteger(R.integer.menu_current_contents_fade_in_duration);
}
/** Sets the menu rows and views. */
public void setMenuRowsAndViews(List<MenuRow> menuRows, List<MenuRowView> menuRowViews) {
mMenuRows.clear();
mMenuRows.addAll(menuRows);
mMenuRowViews.clear();
mMenuRowViews.addAll(menuRowViews);
}
/**
* Layouts main menu view.
*
* <p>Do not call this method directly. It's supposed to be called only by View.onLayout().
*/
public void layout(int left, int top, int right, int bottom) {
if (mAnimatorSet != null) {
// Layout will be done after the animation ends.
return;
}
int count = mMenuRowViews.size();
MenuRowView currentView = mMenuRowViews.get(mSelectedPosition);
if (currentView.getVisibility() == View.GONE) {
// If the selected row is not visible, select the first visible row.
int firstVisiblePosition = findNextVisiblePosition(INVALID_POSITION);
if (firstVisiblePosition != INVALID_POSITION) {
mSelectedPosition = firstVisiblePosition;
} else {
// No rows are visible.
return;
}
}
List<Rect> layouts = getViewLayouts(left, top, right, bottom);
for (int i = 0; i < count; ++i) {
Rect rect = layouts.get(i);
if (rect != null) {
currentView = mMenuRowViews.get(i);
currentView.layout(rect.left, rect.top, rect.right, rect.bottom);
if (DEBUG) dumpChildren("layout()");
}
}
// If the contents view is INVISIBLE initially, it should be changed to GONE after layout.
// See MenuRowView.onFinishInflate() for more information
// TODO: Find a better way to resolve this issue..
for (MenuRowView view : mMenuRowViews) {
if (view.getVisibility() == View.VISIBLE
&& view.getContentsView().getVisibility() == View.INVISIBLE) {
view.onDeselected();
}
}
if (mPendingSelectedPosition != INVALID_POSITION) {
setSelectedPositionSmooth(mPendingSelectedPosition);
}
}
private int findNextVisiblePosition(int start) {
int count = mMenuRowViews.size();
for (int i = start + 1; i < count; ++i) {
if (mMenuRowViews.get(i).getVisibility() != View.GONE) {
return i;
}
}
return INVALID_POSITION;
}
private void dumpChildren(String prefix) {
int position = 0;
for (MenuRowView view : mMenuRowViews) {
View title = view.getChildAt(0);
View contents = view.getChildAt(1);
Log.d(
TAG,
prefix
+ " position="
+ position++
+ " rowView={visiblility="
+ view.getVisibility()
+ ", alpha="
+ view.getAlpha()
+ ", translationY="
+ view.getTranslationY()
+ ", left="
+ view.getLeft()
+ ", top="
+ view.getTop()
+ ", right="
+ view.getRight()
+ ", bottom="
+ view.getBottom()
+ "}, title={visiblility="
+ title.getVisibility()
+ ", alpha="
+ title.getAlpha()
+ ", translationY="
+ title.getTranslationY()
+ ", left="
+ title.getLeft()
+ ", top="
+ title.getTop()
+ ", right="
+ title.getRight()
+ ", bottom="
+ title.getBottom()
+ "}, contents={visiblility="
+ contents.getVisibility()
+ ", alpha="
+ contents.getAlpha()
+ ", translationY="
+ contents.getTranslationY()
+ ", left="
+ contents.getLeft()
+ ", top="
+ contents.getTop()
+ ", right="
+ contents.getRight()
+ ", bottom="
+ contents.getBottom()
+ "}");
}
}
/**
* Checks if the view will take up space for the layout not.
*
* @param position The index of the menu row view in the list. This is not the index of the view
* in the screen.
* @param view The menu row view.
* @param rowsToAdd The menu row views to be added in the next layout process.
* @param rowsToRemove The menu row views to be removed in the next layout process.
* @return {@code true} if the view will take up space for the layout, otherwise {@code false}.
*/
private boolean isVisibleInLayout(
int position, MenuRowView view, List<Integer> rowsToAdd, List<Integer> rowsToRemove) {
// Checks if the view will be visible or not.
return (view.getVisibility() != View.GONE && !rowsToRemove.contains(position))
|| rowsToAdd.contains(position);
}
/**
* Calculates and returns a list of the layout bounds of the menu row views for the layout.
*
* @param left The left coordinate of the menu view.
* @param top The top coordinate of the menu view.
* @param right The right coordinate of the menu view.
* @param bottom The bottom coordinate of the menu view.
*/
private List<Rect> getViewLayouts(int left, int top, int right, int bottom) {
return getViewLayouts(
left, top, right, bottom, Collections.emptyList(), Collections.emptyList());
}
/**
* Calculates and returns a list of the layout bounds of the menu row views for the layout. The
* order of the bounds is the same as that of the menu row views. e.g. the second rectangle in
* the list is for the second menu row view in the view list (not the second view in the
* screen).
*
* <p>It predicts the layout bounds for the next layout process. Some views will be added or
* removed in the layout, so they need to be considered here.
*
* @param left The left coordinate of the menu view.
* @param top The top coordinate of the menu view.
* @param right The right coordinate of the menu view.
* @param bottom The bottom coordinate of the menu view.
* @param rowsToAdd The menu row views to be added in the next layout process.
* @param rowsToRemove The menu row views to be removed in the next layout process.
* @return the layout bounds of the menu row views.
*/
private List<Rect> getViewLayouts(
int left,
int top,
int right,
int bottom,
List<Integer> rowsToAdd,
List<Integer> rowsToRemove) {
// The coordinates should be relative to the parent.
int relativeLeft = 0;
int relateiveRight = right - left;
int relativeBottom = bottom - top;
List<Rect> layouts = new ArrayList<>();
int count = mMenuRowViews.size();
MenuRowView selectedView = mMenuRowViews.get(mSelectedPosition);
int rowTitleHeight = selectedView.getTitleView().getMeasuredHeight();
int rowContentsHeight = selectedView.getPreferredContentsHeight();
// Calculate for the selected row first.
// The distance between the bottom of the screen and the vertical center of the contents
// should be kept fixed. For more information, please see the redlines.
int childTop =
relativeBottom
- mRowAlignFromBottom
- rowContentsHeight / 2
- mRowContentsPaddingTop
- rowTitleHeight;
int childBottom = relativeBottom;
int position = mSelectedPosition + 1;
for (; position < count; ++position) {
// Find and layout the next row to calculate the bottom line of the selected row.
MenuRowView nextView = mMenuRowViews.get(position);
if (isVisibleInLayout(position, nextView, rowsToAdd, rowsToRemove)) {
int nextTitleTopMax =
relativeBottom
- mMenuMarginBottomMin
- rowTitleHeight
+ mRowTitleTextDescenderHeight;
int childBottomMax =
relativeBottom
- mRowAlignFromBottom
+ rowContentsHeight / 2
+ mRowContentsPaddingBottomMax
- rowTitleHeight;
childBottom = Math.min(nextTitleTopMax, childBottomMax);
layouts.add(new Rect(relativeLeft, childBottom, relateiveRight, relativeBottom));
break;
} else {
// null means that the row is GONE.
layouts.add(null);
}
}
layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom));
// Layout the previous rows.
for (int i = mSelectedPosition - 1; i >= 0; --i) {
MenuRowView view = mMenuRowViews.get(i);
if (isVisibleInLayout(i, view, rowsToAdd, rowsToRemove)) {
childTop -= mRowTitleHeight;
childBottom = childTop + rowTitleHeight;
layouts.add(0, new Rect(relativeLeft, childTop, relateiveRight, childBottom));
} else {
layouts.add(0, null);
}
}
// Move all the next rows to the below of the screen.
childTop = relativeBottom;
for (++position; position < count; ++position) {
MenuRowView view = mMenuRowViews.get(position);
if (isVisibleInLayout(position, view, rowsToAdd, rowsToRemove)) {
childBottom = childTop + rowTitleHeight;
layouts.add(new Rect(relativeLeft, childTop, relateiveRight, childBottom));
childTop += mRowTitleHeight;
} else {
layouts.add(null);
}
}
return layouts;
}
/** Move the current selection to the given {@code position}. */
public void setSelectedPosition(int position) {
if (DEBUG) {
Log.d(
TAG,
"setSelectedPosition(position="
+ position
+ ") {previousPosition="
+ mSelectedPosition
+ "}");
}
if (mSelectedPosition == position) {
return;
}
boolean indexValid = Utils.isIndexValid(mMenuRowViews, position);
SoftPreconditions.checkArgument(indexValid, TAG, "position %s ", position);
if (!indexValid) {
return;
}
MenuRow row = mMenuRows.get(position);
if (!row.isVisible()) {
Log.e(TAG, "Selecting invisible row: " + position);
return;
}
if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onDeselected();
}
mSelectedPosition = position;
mPendingSelectedPosition = INVALID_POSITION;
if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onSelected(false);
}
if (mMenuView.getVisibility() == View.VISIBLE) {
// Request focus after the new contents view shows up.
mMenuView.requestFocus();
// Adjust the position of the selected row.
mMenuView.requestLayout();
}
}
/**
* Move the current selection to the given {@code position} with animation. The animation
* specification is included in http://b/21069476
*/
public void setSelectedPositionSmooth(final int position) {
if (DEBUG) {
Log.d(
TAG,
"setSelectedPositionSmooth(position="
+ position
+ ") {previousPosition="
+ mSelectedPosition
+ "}");
}
if (mMenuView.getVisibility() != View.VISIBLE) {
setSelectedPosition(position);
return;
}
if (mSelectedPosition == position) {
return;
}
boolean oldIndexValid = Utils.isIndexValid(mMenuRowViews, mSelectedPosition);
SoftPreconditions.checkState(
oldIndexValid, TAG, "No previous selection: " + mSelectedPosition);
if (!oldIndexValid) {
return;
}
boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position);
SoftPreconditions.checkArgument(newIndexValid, TAG, "position %s", position);
if (!newIndexValid) {
return;
}
MenuRow row = mMenuRows.get(position);
if (!row.isVisible()) {
Log.e(TAG, "Moving to the invisible row: " + position);
return;
}
if (mAnimatorSet != null) {
// Do not cancel the animation here. The property values should be set to the end values
// when the animation finishes.
mAnimatorSet.end();
}
if (mTitleFadeOutAnimator != null) {
// Cancel the animation instead of ending it in order that the title animation starts
// again from the intermediate state.
mTitleFadeOutAnimator.cancel();
}
if (DEBUG) dumpChildren("startRowAnimation()");
// Show the children of the next row.
final MenuRowView currentView = mMenuRowViews.get(position);
TextView currentTitleView = currentView.getTitleView();
View currentContentsView = currentView.getContentsView();
currentTitleView.setVisibility(View.VISIBLE);
currentContentsView.setVisibility(View.VISIBLE);
if (currentView instanceof PlayControlsRowView) {
((PlayControlsRowView) currentView).onPreselected();
}
// When contents view's visibility is gone, layouting might be delayed until it's shown and
// thus cause onBindViewHolder() and menu action updating occurs in front of users' sight.
// Therefore we call requestLayout() here if there are pending adapter updates.
if (currentContentsView instanceof RecyclerView
&& ((RecyclerView) currentContentsView).hasPendingAdapterUpdates()) {
currentContentsView.requestLayout();
mPendingSelectedPosition = position;
return;
}
final int oldPosition = mSelectedPosition;
mSelectedPosition = position;
mPendingSelectedPosition = INVALID_POSITION;
// Request focus after the new contents view shows up.
mMenuView.requestFocus();
if (mTempTitleViewForOld == null) {
// Initialize here because we don't know when the views are inflated.
mTempTitleViewForOld = (TextView) mMenuView.findViewById(R.id.temp_title_for_old);
mTempTitleViewForCurrent =
(TextView) mMenuView.findViewById(R.id.temp_title_for_current);
}
// Animations.
mPropertyValuesAfterAnimation.clear();
List<Animator> animators = new ArrayList<>();
boolean scrollDown = position > oldPosition;
List<Rect> layouts =
getViewLayouts(
mMenuView.getLeft(),
mMenuView.getTop(),
mMenuView.getRight(),
mMenuView.getBottom());
// Old row.
MenuRow oldRow = mMenuRows.get(oldPosition);
final MenuRowView oldView = mMenuRowViews.get(oldPosition);
View oldContentsView = oldView.getContentsView();
// Old contents view.
animators.add(
createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn)
.setDuration(mOldContentsFadeOutDuration));
final TextView oldTitleView = oldView.getTitleView();
setTempTitleView(mTempTitleViewForOld, oldTitleView);
Rect oldLayoutRect = layouts.get(oldPosition);
if (scrollDown) {
// Old title view.
if (oldRow.hideTitleWhenSelected() && oldTitleView.getVisibility() != View.VISIBLE) {
// This case is not included in the animation specification.
mTempTitleViewForOld.setScaleX(1.0f);
mTempTitleViewForOld.setScaleY(1.0f);
animators.add(
createAlphaAnimator(
mTempTitleViewForOld,
0.0f,
oldView.getTitleViewAlphaDeselected(),
mFastOutLinearIn));
int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop();
animators.add(
createTranslationYAnimator(
mTempTitleViewForOld,
offset + mRowScrollUpAnimationOffset,
offset));
} else {
animators.add(
createScaleXAnimator(
mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f));
animators.add(
createScaleYAnimator(
mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f));
animators.add(
createAlphaAnimator(
mTempTitleViewForOld,
oldTitleView.getAlpha(),
oldView.getTitleViewAlphaDeselected(),
mLinearOutSlowIn));
animators.add(
createTranslationYAnimator(
mTempTitleViewForOld,
0,
oldLayoutRect.top - mTempTitleViewForOld.getTop()));
}
oldTitleView.setAlpha(oldView.getTitleViewAlphaDeselected());
oldTitleView.setVisibility(View.INVISIBLE);
} else {
Rect currentLayoutRect = new Rect(layouts.get(position));
// Old title view.
// The move distance in the specification is 32dp(mRowScrollUpAnimationOffset).
// But if the height of the upper row is small, the upper row will move down a lot. In
// this case, this row needs to move more than the specification to avoid the overlap of
// the two titles.
// The maximum is to the top of the start position of mTempTitleViewForOld.
int distanceCurrentTitle = currentLayoutRect.top - currentView.getTop();
int distance = Math.max(mRowScrollUpAnimationOffset, distanceCurrentTitle);
int distanceToTopOfSecondTitle =
oldLayoutRect.top - mRowScrollUpAnimationOffset - oldView.getTop();
animators.add(
createTranslationYAnimator(
oldTitleView, 0.0f, Math.min(distance, distanceToTopOfSecondTitle)));
animators.add(
createAlphaAnimator(oldTitleView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn)
.setDuration(mOldContentsFadeOutDuration));
animators.add(
createScaleXAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f));
animators.add(
createScaleYAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f));
mTempTitleViewForOld.setScaleX(1.0f);
mTempTitleViewForOld.setScaleY(1.0f);
animators.add(
createAlphaAnimator(
mTempTitleViewForOld,
0.0f,
oldView.getTitleViewAlphaDeselected(),
mFastOutLinearIn));
int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop();
animators.add(
createTranslationYAnimator(
mTempTitleViewForOld, offset - mRowScrollUpAnimationOffset, offset));
}
// Current row.
Rect currentLayoutRect = new Rect(layouts.get(position));
currentContentsView.setAlpha(0.0f);
if (scrollDown) {
// Current title view.
setTempTitleView(mTempTitleViewForCurrent, currentTitleView);
// The move distance in the specification is 32dp(mRowScrollUpAnimationOffset).
// But if the height of the upper row is small, the upper row will move up a lot. In
// this case, this row needs to start the move from more than the specification to avoid
// the overlap of the two titles.
// The maximum is to the top of the end position of mTempTitleViewForCurrent.
int distanceOldTitle = oldView.getTop() - oldLayoutRect.top;
int distance = Math.max(mRowScrollUpAnimationOffset, distanceOldTitle);
int distanceTopOfSecondTitle =
currentView.getTop() - mRowScrollUpAnimationOffset - currentLayoutRect.top;
animators.add(
createTranslationYAnimator(
currentTitleView, Math.min(distance, distanceTopOfSecondTitle), 0.0f));
currentView.setTop(currentLayoutRect.top);
ObjectAnimator animator =
createAlphaAnimator(currentTitleView, 0.0f, 1.0f, mFastOutLinearIn)
.setDuration(mCurrentContentsFadeInDuration);
animator.setStartDelay(mOldContentsFadeOutDuration);
currentTitleView.setAlpha(0.0f);
animators.add(animator);
animators.add(
createScaleXAnimator(
currentTitleView, 1.0f, currentView.getTitleViewScaleSelected()));
animators.add(
createScaleYAnimator(
currentTitleView, 1.0f, currentView.getTitleViewScaleSelected()));
animators.add(
createTranslationYAnimator(
mTempTitleViewForCurrent, 0.0f, -mRowScrollUpAnimationOffset));
animators.add(
createAlphaAnimator(
mTempTitleViewForCurrent,
currentView.getTitleViewAlphaDeselected(),
0,
mLinearOutSlowIn));
// Current contents view.
animators.add(
createTranslationYAnimator(
currentContentsView, mRowScrollUpAnimationOffset, 0.0f));
animator =
createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn)
.setDuration(mCurrentContentsFadeInDuration);
animator.setStartDelay(mOldContentsFadeOutDuration);
animators.add(animator);
} else {
currentView.setBottom(currentLayoutRect.bottom);
// Current title view.
int currentViewOffset = currentLayoutRect.top - currentView.getTop();
animators.add(createTranslationYAnimator(currentTitleView, 0, currentViewOffset));
animators.add(
createAlphaAnimator(
currentTitleView,
currentView.getTitleViewAlphaDeselected(),
1.0f,
mFastOutSlowIn));
animators.add(
createScaleXAnimator(
currentTitleView, 1.0f, currentView.getTitleViewScaleSelected()));
animators.add(
createScaleYAnimator(
currentTitleView, 1.0f, currentView.getTitleViewScaleSelected()));
// Current contents view.
animators.add(
createTranslationYAnimator(
currentContentsView,
currentViewOffset - mRowScrollUpAnimationOffset,
currentViewOffset));
ObjectAnimator animator =
createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn)
.setDuration(mCurrentContentsFadeInDuration);
animator.setStartDelay(mOldContentsFadeOutDuration);
animators.add(animator);
}
// Next row.
int nextPosition;
if (scrollDown) {
nextPosition = findNextVisiblePosition(position);
if (nextPosition != INVALID_POSITION) {
MenuRowView nextView = mMenuRowViews.get(nextPosition);
Rect nextLayoutRect = layouts.get(nextPosition);
animators.add(
createTranslationYAnimator(
nextView,
nextLayoutRect.top
+ mRowScrollUpAnimationOffset
- nextView.getTop(),
nextLayoutRect.top - nextView.getTop()));
animators.add(createAlphaAnimator(nextView, 0.0f, 1.0f, mFastOutLinearIn));
}
} else {
nextPosition = findNextVisiblePosition(oldPosition);
if (nextPosition != INVALID_POSITION) {
MenuRowView nextView = mMenuRowViews.get(nextPosition);
animators.add(createTranslationYAnimator(nextView, 0, mRowScrollUpAnimationOffset));
animators.add(
createAlphaAnimator(
nextView,
nextView.getTitleViewAlphaDeselected(),
0.0f,
1.0f,
mLinearOutSlowIn));
}
}
// Other rows.
int count = mMenuRowViews.size();
for (int i = 0; i < count; ++i) {
MenuRowView view = mMenuRowViews.get(i);
if (view.getVisibility() == View.VISIBLE
&& i != oldPosition
&& i != position
&& i != nextPosition) {
Rect rect = layouts.get(i);
animators.add(createTranslationYAnimator(view, 0, rect.top - view.getTop()));
}
}
// Run animation.
final List<ViewPropertyValueHolder> propertyValuesAfterAnimation = new ArrayList<>();
propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation);
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playTogether(animators);
mAnimatorSet.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
if (DEBUG) dumpChildren("onRowAnimationEndBefore");
mAnimatorSet = null;
// The property values which are different from the end values and need to
// be
// changed after the animation are set here.
// e.g. setting translationY to 0, alpha of the contents view to 1.
for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) {
holder.property.set(holder.view, holder.value);
}
oldView.onDeselected();
currentView.onSelected(true);
mTempTitleViewForOld.setVisibility(View.GONE);
mTempTitleViewForCurrent.setVisibility(View.GONE);
layout(
mMenuView.getLeft(),
mMenuView.getTop(),
mMenuView.getRight(),
mMenuView.getBottom());
if (DEBUG) dumpChildren("onRowAnimationEndAfter");
MenuRow currentRow = mMenuRows.get(position);
if (currentRow.hideTitleWhenSelected()) {
View titleView = mMenuRowViews.get(position).getTitleView();
mTitleFadeOutAnimator =
createAlphaAnimator(
titleView,
titleView.getAlpha(),
0.0f,
mLinearOutSlowIn);
mTitleFadeOutAnimator.setStartDelay(
TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS);
mTitleFadeOutAnimator.addListener(
new AnimatorListenerAdapter() {
private boolean mCanceled;
@Override
public void onAnimationCancel(Animator animator) {
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator animator) {
mTitleFadeOutAnimator = null;
if (!mCanceled) {
mMenuRowViews.get(position).onSelected(false);
}
}
});
mTitleFadeOutAnimator.start();
}
}
});
mAnimatorSet.start();
if (DEBUG) dumpChildren("startedRowAnimation()");
}
private void setTempTitleView(TextView dest, TextView src) {
dest.setVisibility(View.VISIBLE);
dest.setText(src.getText());
dest.setTranslationY(0.0f);
if (src.getVisibility() == View.VISIBLE) {
dest.setAlpha(src.getAlpha());
dest.setScaleX(src.getScaleX());
dest.setScaleY(src.getScaleY());
} else {
dest.setAlpha(0.0f);
dest.setScaleX(1.0f);
dest.setScaleY(1.0f);
}
View parent = (View) src.getParent();
dest.setLeft(src.getLeft() + parent.getLeft());
dest.setRight(src.getRight() + parent.getLeft());
dest.setTop(src.getTop() + parent.getTop());
dest.setBottom(src.getBottom() + parent.getTop());
}
/**
* Called when the menu row information is updated. The add/remove animation of the row views
* will be started.
*
* <p>Note that the current row should not be removed.
*/
public void onMenuRowUpdated() {
if (mMenuView.getVisibility() != View.VISIBLE) {
int count = mMenuRowViews.size();
for (int i = 0; i < count; ++i) {
mMenuRowViews
.get(i)
.setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE : View.GONE);
}
return;
}
List<Integer> addedRowViews = new ArrayList<>();
List<Integer> removedRowViews = new ArrayList<>();
Map<Integer, Integer> offsetsToMove = new HashMap<>();
int added = 0;
for (int i = mSelectedPosition - 1; i >= 0; --i) {
MenuRow row = mMenuRows.get(i);
MenuRowView view = mMenuRowViews.get(i);
if (row.isVisible()
&& (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) {
// Removing rows are still VISIBLE.
addedRowViews.add(i);
++added;
} else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) {
removedRowViews.add(i);
--added;
} else if (added != 0) {
offsetsToMove.put(i, -added);
}
}
added = 0;
int count = mMenuRowViews.size();
for (int i = mSelectedPosition + 1; i < count; ++i) {
MenuRow row = mMenuRows.get(i);
MenuRowView view = mMenuRowViews.get(i);
if (row.isVisible()
&& (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) {
// Removing rows are still VISIBLE.
addedRowViews.add(i);
++added;
} else if (!row.isVisible() && view.getVisibility() == View.VISIBLE) {
removedRowViews.add(i);
--added;
} else if (added != 0) {
offsetsToMove.put(i, added);
}
}
if (addedRowViews.size() == 0 && removedRowViews.size() == 0) {
return;
}
if (mAnimatorSet != null) {
// Do not cancel the animation here. The property values should be set to the end values
// when the animation finishes.
mAnimatorSet.end();
}
if (mTitleFadeOutAnimator != null) {
mTitleFadeOutAnimator.end();
}
mPropertyValuesAfterAnimation.clear();
List<Animator> animators = new ArrayList<>();
List<Rect> layouts =
getViewLayouts(
mMenuView.getLeft(),
mMenuView.getTop(),
mMenuView.getRight(),
mMenuView.getBottom(),
addedRowViews,
removedRowViews);
for (int position : addedRowViews) {
MenuRowView view = mMenuRowViews.get(position);
view.setVisibility(View.VISIBLE);
Rect rect = layouts.get(position);
// TODO: The animation is not visible when it is shown for the first time. Need to find
// a better way to resolve this issue.
view.layout(rect.left, rect.top, rect.right, rect.bottom);
View titleView = view.getTitleView();
MarginLayoutParams params = (MarginLayoutParams) titleView.getLayoutParams();
titleView.layout(
view.getPaddingLeft() + params.leftMargin,
view.getPaddingTop() + params.topMargin,
rect.right - rect.left - view.getPaddingRight() - params.rightMargin,
rect.bottom - rect.top - view.getPaddingBottom() - params.bottomMargin);
animators.add(createAlphaAnimator(view, 0.0f, 1.0f, mFastOutLinearIn));
}
for (int position : removedRowViews) {
MenuRowView view = mMenuRowViews.get(position);
animators.add(createAlphaAnimator(view, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn));
}
for (Entry<Integer, Integer> entry : offsetsToMove.entrySet()) {
MenuRowView view = mMenuRowViews.get(entry.getKey());
animators.add(createTranslationYAnimator(view, 0, entry.getValue() * mRowTitleHeight));
}
// Run animation.
final List<ViewPropertyValueHolder> propertyValuesAfterAnimation = new ArrayList<>();
propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation);
mRemovingRowViews.clear();
mRemovingRowViews.addAll(removedRowViews);
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playTogether(animators);
mAnimatorSet.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimatorSet = null;
// The property values which are different from the end values and need to
// be
// changed after the animation are set here.
// e.g. setting translationY to 0, alpha of the contents view to 1.
for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) {
holder.property.set(holder.view, holder.value);
}
for (int position : mRemovingRowViews) {
mMenuRowViews.get(position).setVisibility(View.GONE);
}
layout(
mMenuView.getLeft(),
mMenuView.getTop(),
mMenuView.getRight(),
mMenuView.getBottom());
}
});
mAnimatorSet.start();
if (DEBUG) dumpChildren("onMenuRowUpdated()");
}
private ObjectAnimator createTranslationYAnimator(View view, float from, float to) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, from, to);
animator.setDuration(mRowAnimationDuration);
animator.setInterpolator(mFastOutSlowIn);
mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.TRANSLATION_Y, view, 0));
return animator;
}
private ObjectAnimator createAlphaAnimator(
View view, float from, float to, TimeInterpolator interpolator) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to);
animator.setDuration(mRowAnimationDuration);
animator.setInterpolator(interpolator);
return animator;
}
private ObjectAnimator createAlphaAnimator(
View view, float from, float to, float end, TimeInterpolator interpolator) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to);
animator.setDuration(mRowAnimationDuration);
animator.setInterpolator(interpolator);
mPropertyValuesAfterAnimation.add(new ViewPropertyValueHolder(View.ALPHA, view, end));
return animator;
}
private ObjectAnimator createScaleXAnimator(View view, float from, float to) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_X, from, to);
animator.setDuration(mRowAnimationDuration);
animator.setInterpolator(mFastOutSlowIn);
return animator;
}
private ObjectAnimator createScaleYAnimator(View view, float from, float to) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.SCALE_Y, from, to);
animator.setDuration(mRowAnimationDuration);
animator.setInterpolator(mFastOutSlowIn);
return animator;
}
/** Returns the current position. */
public int getSelectedPosition() {
return mSelectedPosition;
}
private static final class ViewPropertyValueHolder {
public final Property<View, Float> property;
public final View view;
public final float value;
public ViewPropertyValueHolder(Property<View, Float> property, View view, float value) {
this.property = property;
this.view = view;
this.value = value;
}
}
/** Called when the menu becomes visible. */
public void onMenuShow() {}
/** Called when the menu becomes hidden. */
public void onMenuHide() {
if (mAnimatorSet != null) {
mAnimatorSet.end();
mAnimatorSet = null;
}
// Should be finished after the animator set.
if (mTitleFadeOutAnimator != null) {
mTitleFadeOutAnimator.end();
mTitleFadeOutAnimator = null;
}
}
}