| /* |
| * Copyright (C) 2016 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.systemui.statusbar.notification; |
| |
| import android.util.ArraySet; |
| import android.util.Pools; |
| import android.view.NotificationHeaderView; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.widget.ImageView; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import com.android.systemui.Interpolators; |
| import com.android.systemui.R; |
| import com.android.systemui.statusbar.CrossFadeHelper; |
| import com.android.systemui.statusbar.ExpandableNotificationRow; |
| import com.android.systemui.statusbar.ViewTransformationHelper; |
| |
| /** |
| * A transform state of a view. |
| */ |
| public class TransformState { |
| |
| private static final float UNDEFINED = -1f; |
| private static final int TRANSOFORM_X = 0x1; |
| private static final int TRANSOFORM_Y = 0x10; |
| private static final int TRANSOFORM_ALL = TRANSOFORM_X | TRANSOFORM_Y; |
| private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag; |
| private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag; |
| private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag; |
| private static final int TRANSFORMATION_START_X = R.id.transformation_start_x_tag; |
| private static final int TRANSFORMATION_START_Y = R.id.transformation_start_y_tag; |
| private static final int TRANSFORMATION_START_SCLALE_X = R.id.transformation_start_scale_x_tag; |
| private static final int TRANSFORMATION_START_SCLALE_Y = R.id.transformation_start_scale_y_tag; |
| private static Pools.SimplePool<TransformState> sInstancePool = new Pools.SimplePool<>(40); |
| |
| protected View mTransformedView; |
| private int[] mOwnPosition = new int[2]; |
| private float mTransformationEndY = UNDEFINED; |
| private float mTransformationEndX = UNDEFINED; |
| |
| public void initFrom(View view) { |
| mTransformedView = view; |
| } |
| |
| /** |
| * Transforms the {@link #mTransformedView} from the given transformviewstate |
| * @param otherState the state to transform from |
| * @param transformationAmount how much to transform |
| */ |
| public void transformViewFrom(TransformState otherState, float transformationAmount) { |
| mTransformedView.animate().cancel(); |
| if (sameAs(otherState)) { |
| if (mTransformedView.getVisibility() == View.INVISIBLE) { |
| // We have the same content, lets show ourselves |
| mTransformedView.setAlpha(1.0f); |
| mTransformedView.setVisibility(View.VISIBLE); |
| } |
| } else { |
| CrossFadeHelper.fadeIn(mTransformedView, transformationAmount); |
| } |
| transformViewFullyFrom(otherState, transformationAmount); |
| } |
| |
| public void transformViewFullyFrom(TransformState otherState, float transformationAmount) { |
| transformViewFrom(otherState, TRANSOFORM_ALL, null, transformationAmount); |
| } |
| |
| public void transformViewVerticalFrom(TransformState otherState, |
| ViewTransformationHelper.CustomTransformation customTransformation, |
| float transformationAmount) { |
| transformViewFrom(otherState, TRANSOFORM_Y, customTransformation, transformationAmount); |
| } |
| |
| public void transformViewVerticalFrom(TransformState otherState, float transformationAmount) { |
| transformViewFrom(otherState, TRANSOFORM_Y, null, transformationAmount); |
| } |
| |
| private void transformViewFrom(TransformState otherState, int transformationFlags, |
| ViewTransformationHelper.CustomTransformation customTransformation, |
| float transformationAmount) { |
| final View transformedView = mTransformedView; |
| boolean transformX = (transformationFlags & TRANSOFORM_X) != 0; |
| boolean transformY = (transformationFlags & TRANSOFORM_Y) != 0; |
| boolean transformScale = transformScale(); |
| // lets animate the positions correctly |
| if (transformationAmount == 0.0f |
| || transformX && getTransformationStartX() == UNDEFINED |
| || transformY && getTransformationStartY() == UNDEFINED |
| || transformScale && getTransformationStartScaleX() == UNDEFINED |
| || transformScale && getTransformationStartScaleY() == UNDEFINED) { |
| int[] otherPosition; |
| if (transformationAmount != 0.0f) { |
| otherPosition = otherState.getLaidOutLocationOnScreen(); |
| } else { |
| otherPosition = otherState.getLocationOnScreen(); |
| } |
| int[] ownStablePosition = getLaidOutLocationOnScreen(); |
| if (customTransformation == null |
| || !customTransformation.initTransformation(this, otherState)) { |
| if (transformX) { |
| setTransformationStartX(otherPosition[0] - ownStablePosition[0]); |
| } |
| if (transformY) { |
| setTransformationStartY(otherPosition[1] - ownStablePosition[1]); |
| } |
| // we also want to animate the scale if we're the same |
| View otherView = otherState.getTransformedView(); |
| if (transformScale && otherView.getWidth() != transformedView.getWidth()) { |
| setTransformationStartScaleX(otherView.getWidth() * otherView.getScaleX() |
| / (float) transformedView.getWidth()); |
| transformedView.setPivotX(0); |
| } else { |
| setTransformationStartScaleX(UNDEFINED); |
| } |
| if (transformScale && otherView.getHeight() != transformedView.getHeight()) { |
| setTransformationStartScaleY(otherView.getHeight() * otherView.getScaleY() |
| / (float) transformedView.getHeight()); |
| transformedView.setPivotY(0); |
| } else { |
| setTransformationStartScaleY(UNDEFINED); |
| } |
| } |
| if (!transformX) { |
| setTransformationStartX(UNDEFINED); |
| } |
| if (!transformY) { |
| setTransformationStartY(UNDEFINED); |
| } |
| if (!transformScale) { |
| setTransformationStartScaleX(UNDEFINED); |
| setTransformationStartScaleY(UNDEFINED); |
| } |
| setClippingDeactivated(transformedView, true); |
| } |
| float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( |
| transformationAmount); |
| if (transformX) { |
| transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(), |
| 0.0f, |
| interpolatedValue)); |
| } |
| if (transformY) { |
| transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(), |
| 0.0f, |
| interpolatedValue)); |
| } |
| if (transformScale) { |
| float transformationStartScaleX = getTransformationStartScaleX(); |
| if (transformationStartScaleX != UNDEFINED) { |
| transformedView.setScaleX( |
| NotificationUtils.interpolate(transformationStartScaleX, |
| 1.0f, |
| interpolatedValue)); |
| } |
| float transformationStartScaleY = getTransformationStartScaleY(); |
| if (transformationStartScaleY != UNDEFINED) { |
| transformedView.setScaleY( |
| NotificationUtils.interpolate(transformationStartScaleY, |
| 1.0f, |
| interpolatedValue)); |
| } |
| } |
| } |
| |
| protected boolean transformScale() { |
| return false; |
| } |
| |
| /** |
| * Transforms the {@link #mTransformedView} to the given transformviewstate |
| * @param otherState the state to transform from |
| * @param transformationAmount how much to transform |
| * @return whether an animation was started |
| */ |
| public boolean transformViewTo(TransformState otherState, float transformationAmount) { |
| mTransformedView.animate().cancel(); |
| if (sameAs(otherState)) { |
| // We have the same text, lets show ourselfs |
| if (mTransformedView.getVisibility() == View.VISIBLE) { |
| mTransformedView.setAlpha(0.0f); |
| mTransformedView.setVisibility(View.INVISIBLE); |
| } |
| return false; |
| } else { |
| CrossFadeHelper.fadeOut(mTransformedView, transformationAmount); |
| } |
| transformViewFullyTo(otherState, transformationAmount); |
| return true; |
| } |
| |
| public void transformViewFullyTo(TransformState otherState, float transformationAmount) { |
| transformViewTo(otherState, TRANSOFORM_ALL, null, transformationAmount); |
| } |
| |
| public void transformViewVerticalTo(TransformState otherState, |
| ViewTransformationHelper.CustomTransformation customTransformation, |
| float transformationAmount) { |
| transformViewTo(otherState, TRANSOFORM_Y, customTransformation, transformationAmount); |
| } |
| |
| public void transformViewVerticalTo(TransformState otherState, float transformationAmount) { |
| transformViewTo(otherState, TRANSOFORM_Y, null, transformationAmount); |
| } |
| |
| private void transformViewTo(TransformState otherState, int transformationFlags, |
| ViewTransformationHelper.CustomTransformation customTransformation, |
| float transformationAmount) { |
| // lets animate the positions correctly |
| |
| final View transformedView = mTransformedView; |
| boolean transformX = (transformationFlags & TRANSOFORM_X) != 0; |
| boolean transformY = (transformationFlags & TRANSOFORM_Y) != 0; |
| boolean transformScale = transformScale(); |
| // lets animate the positions correctly |
| if (transformationAmount == 0.0f) { |
| if (transformX) { |
| float transformationStartX = getTransformationStartX(); |
| float start = transformationStartX != UNDEFINED ? transformationStartX |
| : transformedView.getTranslationX(); |
| setTransformationStartX(start); |
| } |
| if (transformY) { |
| float transformationStartY = getTransformationStartY(); |
| float start = transformationStartY != UNDEFINED ? transformationStartY |
| : transformedView.getTranslationY(); |
| setTransformationStartY(start); |
| } |
| View otherView = otherState.getTransformedView(); |
| if (transformScale && otherView.getWidth() != transformedView.getWidth()) { |
| setTransformationStartScaleX(transformedView.getScaleX()); |
| transformedView.setPivotX(0); |
| } else { |
| setTransformationStartScaleX(UNDEFINED); |
| } |
| if (transformScale && otherView.getHeight() != transformedView.getHeight()) { |
| setTransformationStartScaleY(transformedView.getScaleY()); |
| transformedView.setPivotY(0); |
| } else { |
| setTransformationStartScaleY(UNDEFINED); |
| } |
| setClippingDeactivated(transformedView, true); |
| } |
| float interpolatedValue = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( |
| transformationAmount); |
| int[] otherStablePosition = otherState.getLaidOutLocationOnScreen(); |
| int[] ownPosition = getLaidOutLocationOnScreen(); |
| if (transformX) { |
| float endX = otherStablePosition[0] - ownPosition[0]; |
| if (customTransformation != null |
| && customTransformation.customTransformTarget(this, otherState)) { |
| endX = mTransformationEndX; |
| } |
| transformedView.setTranslationX(NotificationUtils.interpolate(getTransformationStartX(), |
| endX, |
| interpolatedValue)); |
| } |
| if (transformY) { |
| float endY = otherStablePosition[1] - ownPosition[1]; |
| if (customTransformation != null |
| && customTransformation.customTransformTarget(this, otherState)) { |
| endY = mTransformationEndY; |
| } |
| transformedView.setTranslationY(NotificationUtils.interpolate(getTransformationStartY(), |
| endY, |
| interpolatedValue)); |
| } |
| if (transformScale) { |
| View otherView = otherState.getTransformedView(); |
| float transformationStartScaleX = getTransformationStartScaleX(); |
| if (transformationStartScaleX != UNDEFINED) { |
| transformedView.setScaleX( |
| NotificationUtils.interpolate(transformationStartScaleX, |
| (otherView.getWidth() / (float) transformedView.getWidth()), |
| interpolatedValue)); |
| } |
| float transformationStartScaleY = getTransformationStartScaleY(); |
| if (transformationStartScaleY != UNDEFINED) { |
| transformedView.setScaleY( |
| NotificationUtils.interpolate(transformationStartScaleY, |
| (otherView.getHeight() / (float) transformedView.getHeight()), |
| interpolatedValue)); |
| } |
| } |
| } |
| |
| public static void setClippingDeactivated(final View transformedView, boolean deactivated) { |
| ViewGroup view = (ViewGroup) transformedView.getParent(); |
| while (true) { |
| ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET); |
| if (clipSet == null) { |
| clipSet = new ArraySet<>(); |
| view.setTag(CLIP_CLIPPING_SET, clipSet); |
| } |
| Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG); |
| if (clipChildren == null) { |
| clipChildren = view.getClipChildren(); |
| view.setTag(CLIP_CHILDREN_TAG, clipChildren); |
| } |
| Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING); |
| if (clipToPadding == null) { |
| clipToPadding = view.getClipToPadding(); |
| view.setTag(CLIP_TO_PADDING, clipToPadding); |
| } |
| ExpandableNotificationRow row = view instanceof ExpandableNotificationRow |
| ? (ExpandableNotificationRow) view |
| : null; |
| if (!deactivated) { |
| clipSet.remove(transformedView); |
| if (clipSet.isEmpty()) { |
| view.setClipChildren(clipChildren); |
| view.setClipToPadding(clipToPadding); |
| view.setTag(CLIP_CLIPPING_SET, null); |
| if (row != null) { |
| row.setClipToActualHeight(true); |
| } |
| } |
| } else { |
| clipSet.add(transformedView); |
| view.setClipChildren(false); |
| view.setClipToPadding(false); |
| if (row != null && row.isChildInGroup()) { |
| // We still want to clip to the parent's height |
| row.setClipToActualHeight(false); |
| } |
| } |
| if (row != null && !row.isChildInGroup()) { |
| return; |
| } |
| final ViewParent parent = view.getParent(); |
| if (parent instanceof ViewGroup) { |
| view = (ViewGroup) parent; |
| } else { |
| return; |
| } |
| } |
| } |
| |
| public int[] getLaidOutLocationOnScreen() { |
| int[] location = getLocationOnScreen(); |
| location[0] -= mTransformedView.getTranslationX(); |
| location[1] -= mTransformedView.getTranslationY(); |
| return location; |
| } |
| |
| public int[] getLocationOnScreen() { |
| mTransformedView.getLocationOnScreen(mOwnPosition); |
| return mOwnPosition; |
| } |
| |
| protected boolean sameAs(TransformState otherState) { |
| return false; |
| } |
| |
| public static TransformState createFrom(View view) { |
| if (view instanceof TextView) { |
| TextViewTransformState result = TextViewTransformState.obtain(); |
| result.initFrom(view); |
| return result; |
| } |
| if (view instanceof NotificationHeaderView) { |
| HeaderTransformState result = HeaderTransformState.obtain(); |
| result.initFrom(view); |
| return result; |
| } |
| if (view instanceof ImageView) { |
| ImageTransformState result = ImageTransformState.obtain(); |
| result.initFrom(view); |
| return result; |
| } |
| if (view instanceof ProgressBar) { |
| ProgressTransformState result = ProgressTransformState.obtain(); |
| result.initFrom(view); |
| return result; |
| } |
| TransformState result = obtain(); |
| result.initFrom(view); |
| return result; |
| } |
| |
| public void recycle() { |
| reset(); |
| if (getClass() == TransformState.class) { |
| sInstancePool.release(this); |
| } |
| } |
| |
| public void setTransformationEndY(float transformationEndY) { |
| mTransformationEndY = transformationEndY; |
| } |
| |
| public void setTransformationEndX(float transformationEndX) { |
| mTransformationEndX = transformationEndX; |
| } |
| |
| public float getTransformationStartX() { |
| Object tag = mTransformedView.getTag(TRANSFORMATION_START_X); |
| return tag == null ? UNDEFINED : (float) tag; |
| } |
| |
| public float getTransformationStartY() { |
| Object tag = mTransformedView.getTag(TRANSFORMATION_START_Y); |
| return tag == null ? UNDEFINED : (float) tag; |
| } |
| |
| public float getTransformationStartScaleX() { |
| Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_X); |
| return tag == null ? UNDEFINED : (float) tag; |
| } |
| |
| public float getTransformationStartScaleY() { |
| Object tag = mTransformedView.getTag(TRANSFORMATION_START_SCLALE_Y); |
| return tag == null ? UNDEFINED : (float) tag; |
| } |
| |
| public void setTransformationStartX(float transformationStartX) { |
| mTransformedView.setTag(TRANSFORMATION_START_X, transformationStartX); |
| } |
| |
| public void setTransformationStartY(float transformationStartY) { |
| mTransformedView.setTag(TRANSFORMATION_START_Y, transformationStartY); |
| } |
| |
| private void setTransformationStartScaleX(float startScaleX) { |
| mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, startScaleX); |
| } |
| |
| private void setTransformationStartScaleY(float startScaleY) { |
| mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, startScaleY); |
| } |
| |
| protected void reset() { |
| mTransformedView = null; |
| mTransformationEndX = UNDEFINED; |
| mTransformationEndY = UNDEFINED; |
| } |
| |
| public void setVisible(boolean visible) { |
| if (mTransformedView.getVisibility() == View.GONE) { |
| return; |
| } |
| mTransformedView.animate().cancel(); |
| mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); |
| mTransformedView.setAlpha(visible ? 1.0f : 0.0f); |
| if (visible) { |
| resetTransformedView(); |
| } |
| } |
| |
| public void prepareFadeIn() { |
| resetTransformedView(); |
| } |
| |
| private void resetTransformedView() { |
| mTransformedView.setTranslationX(0); |
| mTransformedView.setTranslationY(0); |
| mTransformedView.setScaleX(1.0f); |
| mTransformedView.setScaleY(1.0f); |
| setClippingDeactivated(mTransformedView, false); |
| abortTransformation(); |
| } |
| |
| public void abortTransformation() { |
| mTransformedView.setTag(TRANSFORMATION_START_X, UNDEFINED); |
| mTransformedView.setTag(TRANSFORMATION_START_Y, UNDEFINED); |
| mTransformedView.setTag(TRANSFORMATION_START_SCLALE_X, UNDEFINED); |
| mTransformedView.setTag(TRANSFORMATION_START_SCLALE_Y, UNDEFINED); |
| } |
| |
| public static TransformState obtain() { |
| TransformState instance = sInstancePool.acquire(); |
| if (instance != null) { |
| return instance; |
| } |
| return new TransformState(); |
| } |
| |
| public View getTransformedView() { |
| return mTransformedView; |
| } |
| } |