/*
|
* 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.notification.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.notification.row.ExpandableNotificationRow;
|
import com.android.systemui.statusbar.notification.row.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_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_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;
|
|
// 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 boolean inShelf;
|
|
/**
|
* A state indicating whether a headsup is currently fully visible, even when not scrolled.
|
* Only valid if the view is heads upped.
|
*/
|
public boolean headsUpIsVisible;
|
|
/**
|
* 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.
|
*
|
* <p>See <code>LOCATION_</code> flags.</p>
|
*/
|
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;
|
dark = svs.dark;
|
hideSensitive = svs.hideSensitive;
|
belowSpeedBump = svs.belowSpeedBump;
|
clipTopAmount = svs.clipTopAmount;
|
notGoneIndex = svs.notGoneIndex;
|
location = svs.location;
|
headsUpIsVisible = svs.headsUpIsVisible;
|
}
|
}
|
|
/**
|
* 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 */);
|
}
|
|
// 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);
|
|
if (headsUpIsVisible) {
|
expandableView.setHeadsUpIsVisible();
|
}
|
}
|
}
|
|
@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 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,
|
false /* isHeadsUpAppear */);
|
}
|
|
if (!expandableView.isInShelf() && this.inShelf) {
|
expandableView.setTransformingInShelf(true);
|
}
|
expandableView.setInShelf(this.inShelf);
|
|
if (headsUpIsVisible) {
|
expandableView.setHeadsUpIsVisible();
|
}
|
}
|
|
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 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);
|
}
|
}
|
|
@Override
|
public void cancelAnimations(View view) {
|
super.cancelAnimations(view);
|
Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
|
if (animator != null) {
|
animator.cancel();
|
}
|
animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET);
|
if (animator != null) {
|
animator.cancel();
|
}
|
}
|
}
|