AOD clock and notification animation
Better transitions for pulsing notifications and other keyguard
messages.
Fixes: 77601057
Fixes: 67641732
Test: visual
Test: atest packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
Test: atest packages/SystemUI/tests/src/com/android/systemui/util/wakelock/KeepAwakeAnimationListenerTest.java
Merged-In: I3e3ec844834bf7f5d9ab341f308e78a002248a87
Change-Id: I1e6d95c62dc7463e8084a64f04c6dee3173967a6
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index 9a26b35..c31cea2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -34,6 +34,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.LinearLayout;
@@ -46,8 +47,9 @@
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.keyguard.KeyguardSliceProvider;
+import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
-import com.android.systemui.util.wakelock.WakeLock;
+import com.android.systemui.util.wakelock.KeepAwakeAnimationListener;
import java.util.HashMap;
import java.util.List;
@@ -64,21 +66,29 @@
* View visible under the clock on the lock screen and AoD.
*/
public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
- Observer<Slice>, TunerService.Tunable {
+ Observer<Slice>, TunerService.Tunable, ConfigurationController.ConfigurationListener {
private static final String TAG = "KeyguardSliceView";
+ public static final int DEFAULT_ANIM_DURATION = 550;
+
private final HashMap<View, PendingIntent> mClickActions;
private Uri mKeyguardSliceUri;
- private TextView mTitle;
- private LinearLayout mRow;
+ @VisibleForTesting
+ TextView mTitle;
+ private Row mRow;
private int mTextColor;
private float mDarkAmount = 0;
private LiveData<Slice> mLiveData;
private int mIconSize;
- private Consumer<Boolean> mListener;
+ /**
+ * Listener called whenever the view contents change.
+ * Boolean will be true when the change happens animated.
+ */
+ private Consumer<Boolean> mContentChangeListener;
private boolean mHasHeader;
- private boolean mHideContent;
+ private Slice mSlice;
+ private boolean mPulsing;
public KeyguardSliceView(Context context) {
this(context, null, 0);
@@ -95,6 +105,18 @@
tunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
mClickActions = new HashMap<>();
+
+ LayoutTransition transition = new LayoutTransition();
+ transition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2);
+ transition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION);
+ transition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2);
+ transition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+ transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+ transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.FAST_OUT_SLOW_IN);
+ transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
+ transition.setAnimateParentHierarchy(false);
+ transition.addTransitionListener(new SliceViewTransitionListener());
+ setLayoutTransition(transition);
}
@Override
@@ -103,7 +125,6 @@
mTitle = findViewById(R.id.title);
mRow = findViewById(R.id.row);
mTextColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
- mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size);
}
@Override
@@ -112,6 +133,7 @@
// Make sure we always have the most current slice
mLiveData.observeForever(this);
+ Dependency.get(ConfigurationController.class).addCallback(this);
}
@Override
@@ -119,17 +141,29 @@
super.onDetachedFromWindow();
mLiveData.removeObserver(this);
+ Dependency.get(ConfigurationController.class).removeCallback(this);
}
- private void showSlice(Slice slice) {
+ private void showSlice() {
+ if (mPulsing) {
+ mTitle.setVisibility(GONE);
+ mRow.setVisibility(GONE);
+ mContentChangeListener.accept(getLayoutTransition() != null);
+ return;
+ }
- ListContent lc = new ListContent(getContext(), slice);
+ if (mSlice == null) {
+ return;
+ }
+
+ ListContent lc = new ListContent(getContext(), mSlice);
mHasHeader = lc.hasHeader();
List<SliceItem> subItems = lc.getRowItems();
if (!mHasHeader) {
mTitle.setVisibility(GONE);
} else {
mTitle.setVisibility(VISIBLE);
+
// If there's a header it'll be the first subitem
RowContent header = new RowContent(getContext(), subItems.get(0),
true /* showStartItem */);
@@ -154,6 +188,7 @@
final int subItemsCount = subItems.size();
final int blendedColor = getTextColor();
final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
+ mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
for (int i = startIndex; i < subItemsCount; i++) {
SliceItem item = subItems.get(i);
RowContent rc = new RowContent(getContext(), item, true /* showStartItem */);
@@ -200,15 +235,20 @@
}
}
- updateVisibility();
- mListener.accept(mHasHeader);
+ if (mContentChangeListener != null) {
+ mContentChangeListener.accept(getLayoutTransition() != null);
+ }
}
- private void updateVisibility() {
- final boolean hasContent = mHasHeader || mRow.getChildCount() > 0;
- final int visibility = hasContent && !mHideContent ? VISIBLE : GONE;
- if (visibility != getVisibility()) {
- setVisibility(visibility);
+ public void setPulsing(boolean pulsing, boolean animate) {
+ mPulsing = pulsing;
+ LayoutTransition transition = getLayoutTransition();
+ if (!animate) {
+ setLayoutTransition(null);
+ }
+ showSlice();
+ if (!animate) {
+ setLayoutTransition(transition);
}
}
@@ -252,8 +292,9 @@
return optimalString.toString();
}
- public void setDark(float darkAmount) {
+ public void setDarkAmount(float darkAmount) {
mDarkAmount = darkAmount;
+ mRow.setDarkAmount(darkAmount);
updateTextColors();
}
@@ -281,12 +322,17 @@
}
}
- public void setListener(Consumer<Boolean> listener) {
- mListener = listener;
+ /**
+ * Listener that gets invoked every time the title or the row visibility changes.
+ * Parameter will be {@code true} whenever the change happens animated.
+ * @param contentChangeListener The listener.
+ */
+ public void setContentChangeListener(Consumer<Boolean> contentChangeListener) {
+ mContentChangeListener = contentChangeListener;
}
public boolean hasHeader() {
- return mHasHeader;
+ return mTitle.getVisibility() == VISIBLE;
}
/**
@@ -295,7 +341,8 @@
*/
@Override
public void onChanged(Slice slice) {
- showSlice(slice);
+ mSlice = slice;
+ showSlice();
}
@Override
@@ -333,15 +380,20 @@
updateTextColors();
}
- public void setHideContent(boolean hideContent) {
- mHideContent = hideContent;
- updateVisibility();
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size);
}
public static class Row extends LinearLayout {
- private static final long ROW_ANIM_DURATION = 350;
- private final WakeLock mAnimationWakeLock;
+ /**
+ * This view is visible in AOD, which means that the device will sleep if we
+ * don't hold a wake lock. We want to enter doze only after all views have reached
+ * their desired positions.
+ */
+ private final Animation.AnimationListener mKeepAwakeListener;
+ private float mDarkAmount;
public Row(Context context) {
this(context, null);
@@ -357,16 +409,13 @@
public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mAnimationWakeLock = WakeLock.createPartial(context, "slice animation");
+ mKeepAwakeListener = new KeepAwakeAnimationListener(mContext);
}
@Override
protected void onFinishInflate() {
LayoutTransition transition = new LayoutTransition();
- transition.setDuration(ROW_ANIM_DURATION);
- transition.setStagger(LayoutTransition.CHANGING, ROW_ANIM_DURATION);
- transition.setStagger(LayoutTransition.CHANGE_APPEARING, ROW_ANIM_DURATION);
- transition.setStagger(LayoutTransition.CHANGE_DISAPPEARING, ROW_ANIM_DURATION);
+ transition.setDuration(DEFAULT_ANIM_DURATION);
PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1);
PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1);
@@ -378,7 +427,8 @@
Interpolators.ACCELERATE_DECELERATE);
transition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
Interpolators.ACCELERATE_DECELERATE);
- transition.setStartDelay(LayoutTransition.CHANGE_APPEARING, ROW_ANIM_DURATION);
+ transition.setStartDelay(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION);
+ transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, DEFAULT_ANIM_DURATION);
ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
@@ -386,31 +436,11 @@
ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
- transition.setDuration(LayoutTransition.DISAPPEARING, ROW_ANIM_DURATION / 2);
+ transition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4);
transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
transition.setAnimateParentHierarchy(false);
setLayoutTransition(transition);
-
- // This view is visible in AOD, which means that the device will sleep if we
- // don't hold a wake lock. We want to enter doze only after all views have reached
- // their desired positions.
- setLayoutAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {
- mAnimationWakeLock.acquire();
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- mAnimationWakeLock.release();
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {
-
- }
- });
}
@Override
@@ -424,23 +454,58 @@
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
+
+ public void setDarkAmount(float darkAmount) {
+ boolean isAwake = darkAmount != 0;
+ boolean wasAwake = mDarkAmount != 0;
+ if (isAwake == wasAwake) {
+ return;
+ }
+ mDarkAmount = darkAmount;
+ setLayoutAnimationListener(isAwake ? null : mKeepAwakeListener);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
}
/**
* Representation of an item that appears under the clock on main keyguard message.
*/
- private class KeyguardSliceButton extends Button {
+ @VisibleForTesting
+ static class KeyguardSliceButton extends Button implements
+ ConfigurationController.ConfigurationListener {
+ private final Context mContext;
public KeyguardSliceButton(Context context) {
super(context, null /* attrs */, 0 /* styleAttr */,
com.android.keyguard.R.style.TextAppearance_Keyguard_Secondary);
- int horizontalPadding = (int) context.getResources()
+ mContext = context;
+ onDensityOrFontScaleChanged();
+ setEllipsize(TruncateAt.END);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ Dependency.get(ConfigurationController.class).addCallback(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ Dependency.get(ConfigurationController.class).removeCallback(this);
+ }
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ int horizontalPadding = (int) mContext.getResources()
.getDimension(R.dimen.widget_horizontal_padding);
setPadding(horizontalPadding / 2, 0, horizontalPadding / 2, 0);
- setCompoundDrawablePadding((int) context.getResources()
+ setCompoundDrawablePadding((int) mContext.getResources()
.getDimension(R.dimen.widget_icon_padding));
- setMaxLines(1);
- setEllipsize(TruncateAt.END);
}
@Override
@@ -465,4 +530,38 @@
}
}
}
+
+ private class SliceViewTransitionListener implements LayoutTransition.TransitionListener {
+ @Override
+ public void startTransition(LayoutTransition transition, ViewGroup container, View view,
+ int transitionType) {
+ switch (transitionType) {
+ case LayoutTransition.APPEARING:
+ int translation = getResources().getDimensionPixelSize(
+ R.dimen.pulsing_notification_appear_translation);
+ view.setTranslationY(translation);
+ view.animate()
+ .translationY(0)
+ .setDuration(DEFAULT_ANIM_DURATION)
+ .setInterpolator(Interpolators.ALPHA_IN)
+ .start();
+ break;
+ case LayoutTransition.DISAPPEARING:
+ if (view == mTitle) {
+ // Translate the view to the inverse of its height, so the layout event
+ // won't misposition it.
+ LayoutParams params = (LayoutParams) mTitle.getLayoutParams();
+ int margin = params.topMargin + params.bottomMargin;
+ mTitle.setTranslationY(-mTitle.getHeight() - margin);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void endTransition(LayoutTransition transition, ViewGroup container, View view,
+ int transitionType) {
+
+ }
+ }
}