blob: c8be13b25d0c5af29e12fc5030dc93fef902e227 [file] [log] [blame]
package com.android.clockwork.globalactions;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.BaseInterpolator;
import android.view.animation.PathInterpolator;
/**
* This class is ported from
* {@link com.google.android.clockwork.common.wearable.wearmaterial.list.ViewGroupFader} with minor
* modifications set the opacity of the views during animation (uses setAlpha on the view instead of
* setLayerType as the latter doesn't play nicely with a dialog. See - b/193583546)
*
* Fades of the children of a {@link ViewGroup} in and out, based on the position of the child.
*
* <p>Children are "faded" when they lie entirely in a region on the top and bottom of a {@link
* ViewGroup}. This region is sized as a percentage of the {@link ViewGroup}'s height, based on the
* height of the child. When not in the top or bottom regions, children have their default alpha and
* scale.
*
*/
public class ViewGroupFader {
public static final float SCALE_LOWER_BOUND = 0.7f;
private float scaleLowerBound = SCALE_LOWER_BOUND;
public static final float ALPHA_LOWER_BOUND = 0.5f;
private float alphaLowerBound = ALPHA_LOWER_BOUND;
private static final float CHAINED_BOUNDS_TOP_PERCENT = 0.6f;
private static final float CHAINED_BOUNDS_BOTTOM_PERCENT = 0.2f;
private static final float CHAINED_LOWER_REGION_PERCENT = 0.35f;
private static final float CHAINED_UPPER_REGION_PERCENT = 0.55f;
public float chainedBoundsTop = CHAINED_BOUNDS_TOP_PERCENT;
public float chainedBoundsBottom = CHAINED_BOUNDS_BOTTOM_PERCENT;
public float chainedLowerRegion = CHAINED_LOWER_REGION_PERCENT;
public float chainedUpperRegion = CHAINED_UPPER_REGION_PERCENT;
protected final ViewGroup parent;
private final Rect containerBounds = new Rect();
private final Rect offsetViewBounds = new Rect();
private final AnimationCallback callback;
private final ChildViewBoundsProvider childViewBoundsProvider;
private ContainerBoundsProvider containerBoundsProvider;
private float topBoundPixels;
private float bottomBoundPixels;
private BaseInterpolator topInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f);
private BaseInterpolator bottomInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f);
/** Callback which is called when attempting to fade a view. */
public interface AnimationCallback {
boolean shouldFadeFromTop(View view);
boolean shouldFadeFromBottom(View view);
void viewHasBecomeFullSize(View view);
}
/**
* Interface for providing the bounds of the child views. This is needed because for
* RecyclerViews, we might need to use bounds that represents the post-layout position, instead of
* the current position.
*/
// TODO(b/182846214): Clean up the interface design to avoid exposing too much details to users.
public interface ChildViewBoundsProvider {
void provideBounds(ViewGroup parent, View child, Rect bounds);
}
/** Interface for providing the bounds of the container for use in calculating item fades. */
public interface ContainerBoundsProvider {
void provideBounds(ViewGroup parent, Rect bounds);
}
/**
* Implementation of {@link ContainerBoundsProvider} that returns the screen bounds as the
* container that is used for calculating the animation of the child elements in the ViewGroup.
*/
public static final class ScreenContainerBoundsProvider implements ContainerBoundsProvider {
@Override
public void provideBounds(ViewGroup parent, Rect bounds) {
bounds.set(
0,
0,
parent.getResources().getDisplayMetrics().widthPixels,
parent.getResources().getDisplayMetrics().heightPixels);
}
}
/**
* Implementation of {@link ContainerBoundsProvider} that returns the parent ViewGroup bounds as
* the container that is used for calculating the animation of the child elements in the
* ViewGroup.
*/
public static final class ParentContainerBoundsProvider implements ContainerBoundsProvider {
@Override
public void provideBounds(ViewGroup parent, Rect bounds) {
parent.getGlobalVisibleRect(bounds);
}
}
/**
* Default implementation of {@link ChildViewBoundsProvider} that returns the post-layout bounds
* of the child view. This should be used when the {@link ViewGroupFader} is used together with a
* RecyclerView.
*/
public static final class DefaultViewBoundsProvider implements ChildViewBoundsProvider {
@Override
public void provideBounds(ViewGroup parent, View child, Rect bounds) {
child.getDrawingRect(bounds);
bounds.offset(0, (int) child.getTranslationY());
parent.offsetDescendantRectToMyCoords(child, bounds);
// Additionally offset the bounds based on parent container's absolute position.
Rect parentGlobalVisibleBounds = new Rect();
parent.getGlobalVisibleRect(parentGlobalVisibleBounds);
bounds.offset(parentGlobalVisibleBounds.left, parentGlobalVisibleBounds.top);
}
}
/**
* Implementation of {@link ChildViewBoundsProvider} that returns the global visible bounds of the
* child view. This should be used when the {@link ViewGroupFader} is not used together with a
* RecyclerView.
*/
public static final class GlobalVisibleViewBoundsProvider implements ChildViewBoundsProvider {
@Override
public void provideBounds(ViewGroup parent, View child, Rect bounds) {
// Get the absolute position of the child. Normally we'd need to also reset the transformation
// matrix before computing this, but the transformations we apply set a pivot that preserves
// the coordinate of the top/bottom boundary used to compute the scaling factor in the first
// place.
child.getGlobalVisibleRect(bounds);
}
}
public ViewGroupFader(
ViewGroup parent,
AnimationCallback callback,
ChildViewBoundsProvider childViewBoundsProvider) {
this.parent = parent;
this.callback = callback;
this.childViewBoundsProvider = childViewBoundsProvider;
this.containerBoundsProvider = new ScreenContainerBoundsProvider();
}
public AnimationCallback getAnimationCallback() {
return callback;
}
public void setScaleLowerBound(float scale) {
scaleLowerBound = scale;
}
public void setAlphaLowerBound(float alpha) {
alphaLowerBound = alpha;
}
public void setTopInterpolator(BaseInterpolator interpolator) {
this.topInterpolator = interpolator;
}
public void setBottomInterpolator(BaseInterpolator interpolator) {
this.bottomInterpolator = interpolator;
}
public void setContainerBoundsProvider(ContainerBoundsProvider boundsProvider) {
this.containerBoundsProvider = boundsProvider;
}
public void updateFade() {
containerBoundsProvider.provideBounds(parent, containerBounds);
topBoundPixels = containerBounds.height() * chainedBoundsTop;
bottomBoundPixels = containerBounds.height() * chainedBoundsBottom;
updateListElementFades();
}
/** For each list element, calculate and adjust the scale and alpha based on its position */
private void updateListElementFades() {
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (child.getVisibility() != View.VISIBLE) {
continue;
}
childViewBoundsProvider.provideBounds(parent, child, offsetViewBounds);
animateViewByPosition(child, offsetViewBounds, topBoundPixels, bottomBoundPixels);
}
}
/** Set the bounds and change the view's scale and alpha accordingly */
private void animateViewByPosition(
View view, Rect bounds, float topBoundPixels, float bottomBoundPixels) {
float fadeOutRegionPercent;
if (view.getHeight() < topBoundPixels && view.getHeight() > bottomBoundPixels) {
// Scale from LOWER_REGION_PERCENT to UPPER_REGION_PERCENT based on the ratio of view height
// to chain region height
fadeOutRegionPercent = lerp(
chainedLowerRegion,
chainedUpperRegion,
(view.getHeight() - bottomBoundPixels) / (topBoundPixels - bottomBoundPixels));
} else if (view.getHeight() < bottomBoundPixels) {
fadeOutRegionPercent = chainedLowerRegion;
} else {
fadeOutRegionPercent = chainedUpperRegion;
}
int fadeOutRegionHeight = (int) (containerBounds.height() * fadeOutRegionPercent);
int topFadeBoundary = fadeOutRegionHeight + containerBounds.top;
int bottomFadeBoundary = containerBounds.bottom - fadeOutRegionHeight;
boolean wasFullSize = (view.getScaleX() == 1);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
view.setPivotX(view.getWidth() * 0.5f);
if (bounds.top > bottomFadeBoundary && callback.shouldFadeFromBottom(view)) {
view.setPivotY((float) -lp.topMargin);
scaleAndFadeByRelativeOffset(
view,
bottomInterpolator.getInterpolation(
(float) (containerBounds.bottom - bounds.top) / fadeOutRegionHeight));
} else if (bounds.bottom < topFadeBoundary && callback.shouldFadeFromTop(view)) {
view.setPivotY(view.getMeasuredHeight() + (float) lp.bottomMargin);
scaleAndFadeByRelativeOffset(
view,
topInterpolator.getInterpolation(
(float) (bounds.bottom - containerBounds.top) / fadeOutRegionHeight));
} else {
if (!wasFullSize) {
callback.viewHasBecomeFullSize(view);
}
setDefaultSizeAndAlphaForView(view);
}
}
/** Change the scale and opacity of the view based on its offset to the determining bound */
private void scaleAndFadeByRelativeOffset(View view, float offset) {
float alpha = lerp(alphaLowerBound, 1, offset);
view.setAlpha(alpha);
float scale = lerp(scaleLowerBound, 1, offset);
view.setScaleX(scale);
view.setScaleY(scale);
}
/** Set the scale and alpha of the view to the full default */
private void setDefaultSizeAndAlphaForView(View view) {
view.setAlpha(1f);
view.setScaleX(1f);
view.setScaleY(1f);
}
/**
* Linear interpolation between [start, end] using value as fraction.
*
* @param min the starting point of the interpolation range.
* @param max the ending point of the interpolation range.
* @param value the proportion of the range to linearly interpolate for.
* @return the interpolated value.
*/
private static float lerp(float min, float max, float value) {
return min + (max - min) * value;
}
}