// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.widget.animation;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import java.util.ArrayList;
import javax.annotation.Nullable;
/** Animates children of a vertical {@link LinearLayout} expanding/collapsing when focused. */
public class FocusAnimator {
private static final int ANIMATION_LENGTH_MS = 225;
/** Contains all of the Views that may be focused. */
private final LinearLayout mLayout;
/** Child that is being focused. */
private final View mFocusedChild;
/** Number of children initially set when the {@link FocusAnimator} was created. */
private final int mInitialNumberOfChildren;
/** Values of {@link View#getTop} for each child View. See {@link #calculateChildTops}. */
private final ArrayList<Integer> mInitialTops;
/**
* Constructs the {@link FocusAnimator}.
*
* To get the correct values to animate between, this should be called immediately before the
* children of the layout are remeasured.
*
* @param layout Layout being animated.
* @param focusedChild Child being focused, or null if none is being focused.
* @param callback Callback to run when children are in the correct places.
*/
public FocusAnimator(
LinearLayout layout, @Nullable View focusedChild, final Runnable callback) {
mLayout = layout;
mFocusedChild = focusedChild;
mInitialNumberOfChildren = mLayout.getChildCount();
mInitialTops = calculateChildTops();
// Add a listener to know when Android has done another measurement pass. The listener
// automatically removes itself to prevent triggering the animation multiple times.
mLayout.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
mLayout.removeOnLayoutChangeListener(this);
startAnimator(callback);
}
});
}
private void startAnimator(final Runnable callback) {
// Don't animate anything if the number of children changed.
if (mInitialNumberOfChildren != mLayout.getChildCount()) {
finishAnimation(callback);
return;
}
// Don't animate if children are already all in the correct places.
boolean isAnimationNecessary = false;
ArrayList<Integer> finalChildTops = calculateChildTops();
for (int i = 0; i < finalChildTops.size() && !isAnimationNecessary; i++) {
isAnimationNecessary |= finalChildTops.get(i).compareTo(mInitialTops.get(i)) != 0;
}
if (!isAnimationNecessary) {
finishAnimation(callback);
return;
}
// Animate each child moving and changing size to match their final locations.
ArrayList<Animator> animators = new ArrayList<Animator>();
ValueAnimator childAnimator = ValueAnimator.ofFloat(0f, 1f);
animators.add(childAnimator);
for (int i = 0; i < mLayout.getChildCount(); i++) {
// The child is already where it should be.
if (mInitialTops.get(i).compareTo(finalChildTops.get(i)) == 0
&& mInitialTops.get(i + 1).compareTo(finalChildTops.get(i + 1)) == 0) {
continue;
}
final View child = mLayout.getChildAt(i);
final int translationDifference = mInitialTops.get(i) - finalChildTops.get(i);
final int oldHeight = mInitialTops.get(i + 1) - mInitialTops.get(i);
final int newHeight = finalChildTops.get(i + 1) - finalChildTops.get(i);
// Translate the child to its new place while changing where its bottom is drawn to
// animate the child changing height without causing another layout.
childAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (Float) animation.getAnimatedValue();
child.setTranslationY(translationDifference * (1f - progress));
if (oldHeight != newHeight) {
float animatedHeight = oldHeight * (1f - progress) + newHeight * progress;
child.setBottom(child.getTop() + (int) animatedHeight);
}
}
});
// Explicitly place the child in its final position in the end.
childAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
child.setTranslationY(0);
child.setBottom(child.getTop() + newHeight);
}
});
}
// Animate the height of the container itself changing.
int oldContainerHeight = mInitialTops.get(mInitialTops.size() - 1);
int newContainerHeight = finalChildTops.get(finalChildTops.size() - 1);
ValueAnimator layoutAnimator = ValueAnimator.ofInt(oldContainerHeight, newContainerHeight);
layoutAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLayout.setBottom(((Integer) animation.getAnimatedValue()));
requestChildFocus();
}
});
animators.add(layoutAnimator);
// Set up and kick off the animation.
AnimatorSet animator = new AnimatorSet();
animator.setDuration(ANIMATION_LENGTH_MS);
animator.setInterpolator(new LinearOutSlowInInterpolator());
animator.playTogether(animators);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
finishAnimation(callback);
// Request a layout to put everything in the right final place.
mLayout.requestLayout();
}
});
animator.start();
}
/** Cleans up the animation and notifies the owner that it is done via the runnable. */
private void finishAnimation(Runnable callback) {
requestChildFocus();
callback.run();
}
/** Scroll the layout so that the focused child is on screen. */
private void requestChildFocus() {
ViewGroup parent = (ViewGroup) mLayout.getParent();
if (mLayout.getParent() == null) return;
// Scroll the parent to make the focused child visible.
if (mFocusedChild != null) parent.requestChildFocus(mLayout, mFocusedChild);
// {@link View#requestChildFocus} fails to account for children changing their height, so
// the scroll value may be past the actual maximum.
int viewportHeight = parent.getBottom() - parent.getTop();
int scrollMax = Math.max(0, mLayout.getMeasuredHeight() - viewportHeight);
if (parent.getScrollY() > scrollMax) parent.setScrollY(scrollMax);
}
/**
* Calculates where the top of each child view should be.
*
* @return Array containing the values of {@link View#getTop} for each child of the layout.
* An additional value at the end indicates the total height of the layout and points at
* the bottom of the last child.
*/
private ArrayList<Integer> calculateChildTops() {
ArrayList<Integer> tops = new ArrayList<Integer>();
int runningTotal = 0;
for (int i = 0; i < mLayout.getChildCount(); i++) {
tops.add(runningTotal);
runningTotal += mLayout.getChildAt(i).getMeasuredHeight();
}
tops.add(runningTotal);
return tops;
}
}