/* * Copyright (C) 2015 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.stack; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.view.View; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; /** * A state of an expandable view */ public class ExpandableViewState extends ViewState { private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag; private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag; private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag; private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag; private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag; private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag; private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag; private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag; // These are flags such that we can create masks for filtering. /** * No known location. This is the default and should not be set after an invocation of the * algorithm. */ public static final int LOCATION_UNKNOWN = 0x00; /** * The location is the first heads up notification, so on the very top. */ public static final int LOCATION_FIRST_HUN = 0x01; /** * The location is hidden / scrolled away on the top. */ public static final int LOCATION_HIDDEN_TOP = 0x02; /** * The location is in the main area of the screen and visible. */ public static final int LOCATION_MAIN_AREA = 0x04; /** * The location is in the bottom stack and it's peeking */ public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08; /** * The location is in the bottom stack and it's hidden. */ public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10; /** * The view isn't laid out at all. */ public static final int LOCATION_GONE = 0x40; /** * The visible locations of a view. */ public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN | ExpandableViewState.LOCATION_MAIN_AREA; public int height; public boolean dimmed; public boolean dark; public boolean hideSensitive; public boolean belowSpeedBump; public float shadowAlpha; public boolean inShelf; /** * How much the child overlaps with the previous child on top. This is used to * show the background properly when the child on top is translating away. */ public int clipTopAmount; /** * The index of the view, only accounting for views not equal to GONE */ public int notGoneIndex; /** * The location this view is currently rendered at. * *

See LOCATION_ flags.

*/ public int location; @Override public void copyFrom(ViewState viewState) { super.copyFrom(viewState); if (viewState instanceof ExpandableViewState) { ExpandableViewState svs = (ExpandableViewState) viewState; height = svs.height; dimmed = svs.dimmed; shadowAlpha = svs.shadowAlpha; dark = svs.dark; hideSensitive = svs.hideSensitive; belowSpeedBump = svs.belowSpeedBump; clipTopAmount = svs.clipTopAmount; notGoneIndex = svs.notGoneIndex; location = svs.location; } } /** * Applies a {@link ExpandableViewState} to a {@link ExpandableView}. */ @Override public void applyToView(View view) { super.applyToView(view); if (view instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) view; int height = expandableView.getActualHeight(); int newHeight = this.height; // apply height if (height != newHeight) { expandableView.setActualHeight(newHeight, false /* notifyListeners */); } float shadowAlpha = expandableView.getShadowAlpha(); float newShadowAlpha = this.shadowAlpha; // apply shadowAlpha if (shadowAlpha != newShadowAlpha) { expandableView.setShadowAlpha(newShadowAlpha); } // apply dimming expandableView.setDimmed(this.dimmed, false /* animate */); // apply hiding sensitive expandableView.setHideSensitive( this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); // apply below shelf speed bump expandableView.setBelowSpeedBump(this.belowSpeedBump); // apply dark expandableView.setDark(this.dark, false /* animate */, 0 /* delay */); // apply clipping float oldClipTopAmount = expandableView.getClipTopAmount(); if (oldClipTopAmount != this.clipTopAmount) { expandableView.setClipTopAmount(this.clipTopAmount); } expandableView.setTransformingInShelf(false); expandableView.setInShelf(inShelf); } } @Override public void animateTo(View child, AnimationProperties properties) { super.animateTo(child, properties); if (!(child instanceof ExpandableView)) { return; } ExpandableView expandableView = (ExpandableView) child; AnimationFilter animationFilter = properties.getAnimationFilter(); // start height animation if (this.height != expandableView.getActualHeight()) { startHeightAnimation(expandableView, properties); } else { abortAnimation(child, TAG_ANIMATOR_HEIGHT); } // start shadow alpha animation if (this.shadowAlpha != expandableView.getShadowAlpha()) { startShadowAlphaAnimation(expandableView, properties); } else { abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA); } // start top inset animation if (this.clipTopAmount != expandableView.getClipTopAmount()) { startInsetAnimation(expandableView, properties); } else { abortAnimation(child, TAG_ANIMATOR_TOP_INSET); } // start dimmed animation expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed); // apply below the speed bump expandableView.setBelowSpeedBump(this.belowSpeedBump); // start hiding sensitive animation expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, properties.delay, properties.duration); // start dark animation expandableView.setDark(this.dark, animationFilter.animateDark, properties.delay); if (properties.wasAdded(child) && !hidden) { expandableView.performAddAnimation(properties.delay, properties.duration); } if (!expandableView.isInShelf() && this.inShelf) { expandableView.setTransformingInShelf(true); } expandableView.setInShelf(this.inShelf); } private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); int newEndValue = this.height; if (previousEndValue != null && previousEndValue == newEndValue) { return; } ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); AnimationFilter filter = properties.getAnimationFilter(); if (!filter.animateHeight) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the // relative change to the end value PropertyValuesHolder[] values = previousAnimator.getValues(); int relativeDiff = newEndValue - previousEndValue; int newStartValue = previousStartValue + relativeDiff; values[0].setIntValues(newStartValue, newEndValue); child.setTag(TAG_START_HEIGHT, newStartValue); child.setTag(TAG_END_HEIGHT, newEndValue); previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); return; } else { // no new animation needed, let's just apply the value child.setActualHeight(newEndValue, false); return; } } ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { child.setActualHeight((int) animation.getAnimatedValue(), false /* notifyListeners */); } }); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); animator.setDuration(newDuration); if (properties.delay > 0 && (previousAnimator == null || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); if (listener != null) { animator.addListener(listener); } // remove the tag when the animation is finished animator.addListener(new AnimatorListenerAdapter() { boolean mWasCancelled; @Override public void onAnimationEnd(Animator animation) { child.setTag(TAG_ANIMATOR_HEIGHT, null); child.setTag(TAG_START_HEIGHT, null); child.setTag(TAG_END_HEIGHT, null); child.setActualHeightAnimating(false); if (!mWasCancelled && child instanceof ExpandableNotificationRow) { ((ExpandableNotificationRow) child).setGroupExpansionChanging( false /* isExpansionChanging */); } } @Override public void onAnimationStart(Animator animation) { mWasCancelled = false; } @Override public void onAnimationCancel(Animator animation) { mWasCancelled = true; } }); startAnimator(animator, listener); child.setTag(TAG_ANIMATOR_HEIGHT, animator); child.setTag(TAG_START_HEIGHT, child.getActualHeight()); child.setTag(TAG_END_HEIGHT, newEndValue); child.setActualHeightAnimating(true); } private void startShadowAlphaAnimation(final ExpandableView child, AnimationProperties properties) { Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA); Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA); float newEndValue = this.shadowAlpha; if (previousEndValue != null && previousEndValue == newEndValue) { return; } ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA); AnimationFilter filter = properties.getAnimationFilter(); if (!filter.animateShadowAlpha) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the // relative change to the end value PropertyValuesHolder[] values = previousAnimator.getValues(); float relativeDiff = newEndValue - previousEndValue; float newStartValue = previousStartValue + relativeDiff; values[0].setFloatValues(newStartValue, newEndValue); child.setTag(TAG_START_SHADOW_ALPHA, newStartValue); child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); return; } else { // no new animation needed, let's just apply the value child.setShadowAlpha(newEndValue); return; } } ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { child.setShadowAlpha((float) animation.getAnimatedValue()); } }); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); animator.setDuration(newDuration); if (properties.delay > 0 && (previousAnimator == null || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); if (listener != null) { animator.addListener(listener); } // remove the tag when the animation is finished animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null); child.setTag(TAG_START_SHADOW_ALPHA, null); child.setTag(TAG_END_SHADOW_ALPHA, null); } }); startAnimator(animator, listener); child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator); child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha()); child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); } private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) { Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); int newEndValue = this.clipTopAmount; if (previousEndValue != null && previousEndValue == newEndValue) { return; } ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET); AnimationFilter filter = properties.getAnimationFilter(); if (!filter.animateTopInset) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the // relative change to the end value PropertyValuesHolder[] values = previousAnimator.getValues(); int relativeDiff = newEndValue - previousEndValue; int newStartValue = previousStartValue + relativeDiff; values[0].setIntValues(newStartValue, newEndValue); child.setTag(TAG_START_TOP_INSET, newStartValue); child.setTag(TAG_END_TOP_INSET, newEndValue); previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); return; } else { // no new animation needed, let's just apply the value child.setClipTopAmount(newEndValue); return; } } ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { child.setClipTopAmount((int) animation.getAnimatedValue()); } }); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); animator.setDuration(newDuration); if (properties.delay > 0 && (previousAnimator == null || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); if (listener != null) { animator.addListener(listener); } // remove the tag when the animation is finished animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { child.setTag(TAG_ANIMATOR_TOP_INSET, null); child.setTag(TAG_START_TOP_INSET, null); child.setTag(TAG_END_TOP_INSET, null); } }); startAnimator(animator, listener); child.setTag(TAG_ANIMATOR_TOP_INSET, animator); child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount()); child.setTag(TAG_END_TOP_INSET, newEndValue); } /** * Get the end value of the height animation running on a view or the actualHeight * if no animation is running. */ public static int getFinalActualHeight(ExpandableView view) { if (view == null) { return 0; } ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); if (heightAnimator == null) { return view.getActualHeight(); } else { return getChildTag(view, TAG_END_HEIGHT); } } }