/*
* 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);
}
}
}