/* * 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; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.util.ArrayMap; import android.util.ArraySet; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.notification.TransformState; import com.android.systemui.statusbar.stack.StackStateAnimator; import java.util.Stack; /** * A view that can be transformed to and from. */ public class ViewTransformationHelper implements TransformableView { private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view; private ArrayMap mTransformedViews = new ArrayMap<>(); private ArrayMap mCustomTransformations = new ArrayMap<>(); private ValueAnimator mViewTransformationAnimation; public void addTransformedView(int key, View transformedView) { mTransformedViews.put(key, transformedView); } public void reset() { mTransformedViews.clear(); } public void setCustomTransformation(CustomTransformation transformation, int viewType) { mCustomTransformations.put(viewType, transformation); } @Override public TransformState getCurrentState(int fadingView) { View view = mTransformedViews.get(fadingView); if (view != null && view.getVisibility() != View.GONE) { return TransformState.createFrom(view); } return null; } @Override public void transformTo(final TransformableView notification, final Runnable endRunnable) { if (mViewTransformationAnimation != null) { mViewTransformationAnimation.cancel(); } mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { transformTo(notification, animation.getAnimatedFraction()); } }); mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR); mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() { public boolean mCancelled; @Override public void onAnimationEnd(Animator animation) { if (!mCancelled) { if (endRunnable != null) { endRunnable.run(); } setVisible(false); } else { abortTransformations(); } } @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } }); mViewTransformationAnimation.start(); } @Override public void transformTo(TransformableView notification, float transformationAmount) { for (Integer viewType : mTransformedViews.keySet()) { TransformState ownState = getCurrentState(viewType); if (ownState != null) { CustomTransformation customTransformation = mCustomTransformations.get(viewType); if (customTransformation != null && customTransformation.transformTo( ownState, notification, transformationAmount)) { ownState.recycle(); continue; } TransformState otherState = notification.getCurrentState(viewType); if (otherState != null) { ownState.transformViewTo(otherState, transformationAmount); otherState.recycle(); } else { ownState.disappear(transformationAmount, notification); } ownState.recycle(); } } } @Override public void transformFrom(final TransformableView notification) { if (mViewTransformationAnimation != null) { mViewTransformationAnimation.cancel(); } mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { transformFrom(notification, animation.getAnimatedFraction()); } }); mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() { public boolean mCancelled; @Override public void onAnimationEnd(Animator animation) { if (!mCancelled) { setVisible(true); } else { abortTransformations(); } } @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } }); mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR); mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); mViewTransformationAnimation.start(); } @Override public void transformFrom(TransformableView notification, float transformationAmount) { for (Integer viewType : mTransformedViews.keySet()) { TransformState ownState = getCurrentState(viewType); if (ownState != null) { CustomTransformation customTransformation = mCustomTransformations.get(viewType); if (customTransformation != null && customTransformation.transformFrom( ownState, notification, transformationAmount)) { ownState.recycle(); continue; } TransformState otherState = notification.getCurrentState(viewType); if (otherState != null) { ownState.transformViewFrom(otherState, transformationAmount); otherState.recycle(); } else { ownState.appear(transformationAmount, notification); } ownState.recycle(); } } } @Override public void setVisible(boolean visible) { if (mViewTransformationAnimation != null) { mViewTransformationAnimation.cancel(); } for (Integer viewType : mTransformedViews.keySet()) { TransformState ownState = getCurrentState(viewType); if (ownState != null) { ownState.setVisible(visible, false /* force */); ownState.recycle(); } } } private void abortTransformations() { for (Integer viewType : mTransformedViews.keySet()) { TransformState ownState = getCurrentState(viewType); if (ownState != null) { ownState.abortTransformation(); ownState.recycle(); } } } /** * Add the remaining transformation views such that all views are being transformed correctly * @param viewRoot the root below which all elements need to be transformed */ public void addRemainingTransformTypes(View viewRoot) { // lets now tag the right views int numValues = mTransformedViews.size(); for (int i = 0; i < numValues; i++) { View view = mTransformedViews.valueAt(i); while (view != viewRoot.getParent()) { view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true); view = (View) view.getParent(); } } Stack stack = new Stack<>(); // Add the right views now stack.push(viewRoot); while (!stack.isEmpty()) { View child = stack.pop(); Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW); if (containsView == null) { // This one is unhandled, let's add it to our list. int id = child.getId(); if (id != View.NO_ID) { // We only fade views with an id addTransformedView(id, child); continue; } } child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null); if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){ ViewGroup group = (ViewGroup) child; for (int i = 0; i < group.getChildCount(); i++) { stack.push(group.getChildAt(i)); } } } } public void resetTransformedView(View view) { TransformState state = TransformState.createFrom(view); state.setVisible(true /* visible */, true /* force */); state.recycle(); } /** * @return a set of all views are being transformed. */ public ArraySet getAllTransformingViews() { return new ArraySet<>(mTransformedViews.values()); } public static abstract class CustomTransformation { /** * Transform a state to the given view * @param ownState the state to transform * @param notification the view to transform to * @param transformationAmount how much transformation should be done * @return whether a custom transformation is performed */ public abstract boolean transformTo(TransformState ownState, TransformableView notification, float transformationAmount); /** * Transform to this state from the given view * @param ownState the state to transform to * @param notification the view to transform from * @param transformationAmount how much transformation should be done * @return whether a custom transformation is performed */ public abstract boolean transformFrom(TransformState ownState, TransformableView notification, float transformationAmount); /** * Perform a custom initialisation before transforming. * * @param ownState our own state * @param otherState the other state * @return whether a custom initialization is done */ public boolean initTransformation(TransformState ownState, TransformState otherState) { return false; } public boolean customTransformTarget(TransformState ownState, TransformState otherState) { return false; } /** * Get a custom interpolator for this animation * @param interpolationType the type of the interpolation, i.e TranslationX / TranslationY * @param isFrom true if this transformation from the other view */ public Interpolator getCustomInterpolator(int interpolationType, boolean isFrom) { return null; } } }