| /* |
| * Copyright (C) 2022 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.internal.widget; |
| |
| 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 |
| * com.google.android.clockwork.common.wearable.wearmaterial.list.ViewGroupFader with minor |
| * modifications set the opacity of the views during animation (uses setTransitionAlpha 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 fraction 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. |
| */ |
| class ViewGroupFader { |
| |
| private static final float SCALE_LOWER_BOUND = 0.7f; |
| private float mScaleLowerBound = SCALE_LOWER_BOUND; |
| |
| private static final float ALPHA_LOWER_BOUND = 0.5f; |
| private float mAlphaLowerBound = ALPHA_LOWER_BOUND; |
| |
| private static final float CHAINED_BOUNDS_TOP_FRACTION = 0.6f; |
| private static final float CHAINED_BOUNDS_BOTTOM_FRACTION = 0.2f; |
| private static final float CHAINED_LOWER_REGION_FRACTION = 0.35f; |
| private static final float CHAINED_UPPER_REGION_FRACTION = 0.55f; |
| |
| private float mChainedBoundsTop = CHAINED_BOUNDS_TOP_FRACTION; |
| private float mChainedBoundsBottom = CHAINED_BOUNDS_BOTTOM_FRACTION; |
| private float mChainedLowerRegion = CHAINED_LOWER_REGION_FRACTION; |
| private float mChainedUpperRegion = CHAINED_UPPER_REGION_FRACTION; |
| |
| protected final ViewGroup mParent; |
| |
| private final Rect mContainerBounds = new Rect(); |
| private final Rect mOffsetViewBounds = new Rect(); |
| private final AnimationCallback mCallback; |
| private final ChildViewBoundsProvider mChildViewBoundsProvider; |
| |
| private ContainerBoundsProvider mContainerBoundsProvider; |
| private float mTopBoundPixels; |
| private float mBottomBoundPixels; |
| private BaseInterpolator mTopInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f); |
| private BaseInterpolator mBottomInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f); |
| |
| /** Callback which is called when attempting to fade a view. */ |
| 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. |
| interface ChildViewBoundsProvider { |
| /** |
| * Provide the bounds of the child view. |
| * |
| * @param parent the parent container. |
| * @param child the child view. |
| * @param bounds the bounds of the child view. The bounds are relative to |
| * the value of the bounds for setContainerBoundsProvider. By default, |
| * this is relative to the screen. |
| */ |
| void provideBounds(ViewGroup parent, View child, Rect bounds); |
| } |
| |
| /** Interface for providing the bounds of the container for use in calculating item fades. */ |
| interface ContainerBoundsProvider { |
| /** |
| * Provide the bounds of the container for use in calculating item fades. |
| * |
| * @param parent the parent of the container. |
| * @param bounds the baseline bounds to which the child bounds are relative. |
| */ |
| 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. |
| */ |
| 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. |
| */ |
| 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. |
| */ |
| 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. |
| */ |
| 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); |
| } |
| } |
| |
| ViewGroupFader( |
| ViewGroup parent, |
| AnimationCallback callback, |
| ChildViewBoundsProvider childViewBoundsProvider) { |
| this.mParent = parent; |
| this.mCallback = callback; |
| this.mChildViewBoundsProvider = childViewBoundsProvider; |
| this.mContainerBoundsProvider = new ScreenContainerBoundsProvider(); |
| } |
| |
| AnimationCallback getAnimationCallback() { |
| return mCallback; |
| } |
| |
| /** |
| * Sets the lower bound of the scale the view can reach, on a scale of 0 to 1. |
| * |
| * @param scale the value for the lower bound of the scale. |
| */ |
| void setScaleLowerBound(float scale) { |
| mScaleLowerBound = scale; |
| } |
| |
| /** |
| * Sets the lower bound of the alpha the view can reach, on a scale of 0 to 1. |
| * |
| * @param alpha the value for the lower bound of the alpha. |
| */ |
| void setAlphaLowerBound(float alpha) { |
| mAlphaLowerBound = alpha; |
| } |
| |
| void setTopInterpolator(BaseInterpolator interpolator) { |
| this.mTopInterpolator = interpolator; |
| } |
| |
| void setBottomInterpolator(BaseInterpolator interpolator) { |
| this.mBottomInterpolator = interpolator; |
| } |
| |
| void setContainerBoundsProvider(ContainerBoundsProvider boundsProvider) { |
| this.mContainerBoundsProvider = boundsProvider; |
| } |
| |
| void updateFade() { |
| mContainerBoundsProvider.provideBounds(mParent, mContainerBounds); |
| mTopBoundPixels = mContainerBounds.height() * mChainedBoundsTop; |
| mBottomBoundPixels = mContainerBounds.height() * mChainedBoundsBottom; |
| |
| updateListElementFades(mParent, true); |
| } |
| |
| /** For each list element, calculate and adjust the scale and alpha based on its position */ |
| private void updateListElementFades(ViewGroup parent, boolean shouldFade) { |
| for (int i = 0; i < parent.getChildCount(); i++) { |
| View child = parent.getChildAt(i); |
| if (child.getVisibility() != View.VISIBLE) { |
| continue; |
| } |
| |
| if (shouldFade) { |
| fadeElement(parent, child); |
| } |
| } |
| } |
| |
| private void fadeElement(ViewGroup parent, View child) { |
| mChildViewBoundsProvider.provideBounds(parent, child, mOffsetViewBounds); |
| setViewPropertiesByPosition(child, mOffsetViewBounds, mTopBoundPixels, mBottomBoundPixels); |
| } |
| |
| /** Set the bounds and change the view's scale and alpha accordingly */ |
| private void setViewPropertiesByPosition( |
| View view, Rect bounds, float topBoundPixels, float bottomBoundPixels) { |
| float fadeOutRegionFraction; |
| if (view.getHeight() < topBoundPixels && view.getHeight() > bottomBoundPixels) { |
| // Scale from LOWER_REGION_FRACTION to UPPER_REGION_FRACTION based on the ratio of view |
| // height to chain region height. |
| fadeOutRegionFraction = lerp( |
| mChainedLowerRegion, |
| mChainedUpperRegion, |
| (view.getHeight() - bottomBoundPixels) / (topBoundPixels - bottomBoundPixels)); |
| } else if (view.getHeight() < bottomBoundPixels) { |
| fadeOutRegionFraction = mChainedLowerRegion; |
| } else { |
| fadeOutRegionFraction = mChainedUpperRegion; |
| } |
| int fadeOutRegionHeight = (int) (mContainerBounds.height() * fadeOutRegionFraction); |
| int topFadeBoundary = fadeOutRegionHeight + mContainerBounds.top; |
| int bottomFadeBoundary = mContainerBounds.bottom - fadeOutRegionHeight; |
| boolean wasFullSize = (view.getScaleX() == 1); |
| |
| MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); |
| view.setPivotX(view.getWidth() * 0.5f); |
| if (bounds.top > bottomFadeBoundary && mCallback.shouldFadeFromBottom(view)) { |
| view.setPivotY((float) -lp.topMargin); |
| scaleAndFadeByRelativeOffsetFraction( |
| view, |
| mBottomInterpolator.getInterpolation( |
| (float) (mContainerBounds.bottom - bounds.top) / fadeOutRegionHeight)); |
| } else if (bounds.bottom < topFadeBoundary && mCallback.shouldFadeFromTop(view)) { |
| view.setPivotY(view.getMeasuredHeight() + (float) lp.bottomMargin); |
| scaleAndFadeByRelativeOffsetFraction( |
| view, |
| mTopInterpolator.getInterpolation( |
| (float) (bounds.bottom - mContainerBounds.top) / fadeOutRegionHeight)); |
| } else { |
| if (!wasFullSize) { |
| mCallback.viewHasBecomeFullSize(view); |
| } |
| setDefaultSizeAndAlphaForView(view); |
| } |
| } |
| |
| /** |
| * Change the scale and opacity of the view based on its offset fraction to the |
| * determining bound. |
| */ |
| private void scaleAndFadeByRelativeOffsetFraction(View view, float offsetFraction) { |
| float alpha = lerp(mAlphaLowerBound, 1, offsetFraction); |
| view.setTransitionAlpha(alpha); |
| float scale = lerp(mScaleLowerBound, 1, offsetFraction); |
| view.setScaleX(scale); |
| view.setScaleY(scale); |
| } |
| |
| /** Set the scale and alpha of the view to the full default */ |
| private void setDefaultSizeAndAlphaForView(View view) { |
| view.setTransitionAlpha(1f); |
| view.setScaleX(1f); |
| view.setScaleY(1f); |
| } |
| |
| /** |
| * Linear interpolation between [min, max] using [fraction]. |
| * |
| * @param min the starting point of the interpolation range. |
| * @param max the ending point of the interpolation range. |
| * @param fraction the proportion of the range to linearly interpolate for. |
| * @return the interpolated value. |
| */ |
| private static float lerp(float min, float max, float fraction) { |
| return min + (max - min) * fraction; |
| } |
| } |