/* * 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 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) { if (!(transformedView.getParent() instanceof ViewGroup)) { return; } ViewGroup view = (ViewGroup) transformedView.getParent(); while (true) { ArraySet clipSet = (ArraySet) 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.getId() == com.android.internal.R.id.actions_container) { ActionListTransformState result = ActionListTransformState.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, boolean force) { if (!force && mTransformedView.getVisibility() == View.GONE) { return; } if (mTransformedView.getVisibility() != View.GONE) { mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } mTransformedView.animate().cancel(); mTransformedView.setAlpha(visible ? 1.0f : 0.0f); resetTransformedView(); } public void prepareFadeIn() { resetTransformedView(); } protected 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; } }