| /* |
| * 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 android.support.v4.view.animation.FastOutLinearInInterpolator; |
| import android.support.v4.view.animation.FastOutSlowInInterpolator; |
| import android.support.v4.view.animation.LinearOutSlowInInterpolator; |
| import android.util.Log; |
| import android.util.Property; |
| import android.view.View; |
| import android.view.ViewGroup.MarginLayoutParams; |
| import android.widget.TextView; |
| |
| 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 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 = -1; |
| |
| 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(-1); |
| if (firstVisiblePosition != -1) { |
| 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(); |
| } |
| } |
| } |
| |
| 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 -1; |
| } |
| |
| 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 " + 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; |
| 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 " + 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(); |
| } |
| final int oldPosition = mSelectedPosition; |
| mSelectedPosition = position; |
| if (DEBUG) dumpChildren("startRowAnimation()"); |
| |
| MenuRowView currentView = mMenuRowViews.get(position); |
| // Show the children of the next row. |
| currentView.getTitleView().setVisibility(View.VISIBLE); |
| currentView.getContentsView().setVisibility(View.VISIBLE); |
| // 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); |
| 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)); |
| TextView currentTitleView = currentView.getTitleView(); |
| View currentContentsView = currentView.getContentsView(); |
| 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 != -1) { |
| 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 != -1) { |
| 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); |
| } |
| oldTitleView.setVisibility(View.VISIBLE); |
| mMenuRowViews.get(oldPosition).onDeselected(); |
| mMenuRowViews.get(position).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; |
| } |
| } |
| } |