From dc6bb1d38ada0c4e6a0f8b28d681125f97220020 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Thu, 14 May 2020 13:33:41 -0400 Subject: [PATCH] Controls UI - Loading animations Do not allow behaviors to update the status string directly. Potentially queue up text updates and change it during an animation. Bug: 154625942 Test: visual Change-Id: I916ca083e0b44e01879ba659ac82a3189017631b --- .../systemui/controls/ui/ControlViewHolder.kt | 148 +++++++++++++++--- .../systemui/controls/ui/DefaultBehavior.kt | 2 +- .../systemui/controls/ui/RenderInfo.kt | 10 +- .../systemui/controls/ui/StatusBehavior.kt | 7 +- .../controls/ui/TemperatureControlBehavior.kt | 2 +- .../systemui/controls/ui/ToggleBehavior.kt | 3 +- .../controls/ui/ToggleRangeBehavior.kt | 24 +-- .../systemui/controls/ui/TouchBehavior.kt | 9 +- .../globalactions/GlobalActionsDialog.java | 12 +- 9 files changed, 155 insertions(+), 62 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index 17e423416064f..f979bbb7af35d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -18,10 +18,15 @@ package com.android.systemui.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.animation.ValueAnimator +import android.annotation.ColorRes import android.app.Dialog import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.ClipDrawable +import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.service.controls.Control @@ -34,6 +39,7 @@ import android.service.controls.templates.TemperatureControlTemplate import android.service.controls.templates.ToggleRangeTemplate import android.service.controls.templates.ToggleTemplate import android.util.MathUtils +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.widget.ImageView @@ -63,6 +69,8 @@ class ControlViewHolder( private const val UPDATE_DELAY_IN_MILLIS = 3000L private const val ALPHA_ENABLED = 255 private const val ALPHA_DISABLED = 0 + private const val STATUS_ALPHA_ENABLED = 1f + private const val STATUS_ALPHA_DIMMED = 0.45f private val FORCE_PANEL_DEVICES = setOf( DeviceTypes.TYPE_THERMOSTAT, DeviceTypes.TYPE_CAMERA @@ -94,9 +102,11 @@ class ControlViewHolder( private val toggleBackgroundIntensity: Float = layout.context.resources .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1) private var stateAnimator: ValueAnimator? = null + private var statusAnimator: Animator? = null private val baseLayer: GradientDrawable val icon: ImageView = layout.requireViewById(R.id.icon) - val status: TextView = layout.requireViewById(R.id.status) + private val status: TextView = layout.requireViewById(R.id.status) + private var nextStatusText: CharSequence = "" val title: TextView = layout.requireViewById(R.id.title) val subtitle: TextView = layout.requireViewById(R.id.subtitle) val context: Context = layout.getContext() @@ -105,6 +115,7 @@ class ControlViewHolder( var cancelUpdate: Runnable? = null var behavior: Behavior? = null var lastAction: ControlAction? = null + var isLoading = false private var lastChallengeDialog: Dialog? = null private val onDialogCancel: () -> Unit = { lastChallengeDialog = null } @@ -144,6 +155,7 @@ class ControlViewHolder( }) } + isLoading = false behavior = bindBehavior(behavior, findBehaviorClass(controlStatus, template, deviceType)) updateContentDescription() } @@ -189,11 +201,11 @@ class ControlViewHolder( val previousText = status.getText() cancelUpdate = uiExecutor.executeDelayed({ - status.setText(previousText) - updateContentDescription() - }, UPDATE_DELAY_IN_MILLIS) + setStatusText(previousText) + updateContentDescription() + }, UPDATE_DELAY_IN_MILLIS) - status.setText(tempStatus) + setStatusText(tempStatus) updateContentDescription() } @@ -231,18 +243,50 @@ class ControlViewHolder( } internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) { - setEnabled(enabled) - val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset) - val fg = context.resources.getColorStateList(ri.foreground, context.theme) + val newText = nextStatusText + nextStatusText = "" + val control = cws.control + + var shouldAnimate = animated + if (newText == status.text) { + shouldAnimate = false + } + animateStatusChange(shouldAnimate) { + updateStatusRow(enabled, newText, ri.icon, fg, control) + } + + animateBackgroundChange(shouldAnimate, enabled, ri.enabledBackground) + } + + fun getStatusText() = status.text + + fun setStatusTextSize(textSize: Float) = + status.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + + fun setStatusText(text: CharSequence, immediately: Boolean = false) { + if (immediately) { + status.alpha = STATUS_ALPHA_ENABLED + status.text = text + nextStatusText = "" + } else { + nextStatusText = text + } + } + + private fun animateBackgroundChange( + animated: Boolean, + enabled: Boolean, + @ColorRes bgColor: Int + ) { val bg = context.resources.getColor(R.color.control_default_background, context.theme) var (newClipColor, newAlpha) = if (enabled) { // allow color overrides for the enabled state only val color = cws.control?.getCustomColor()?.let { val state = intArrayOf(android.R.attr.state_enabled) it.getColorForState(state, it.getDefaultColor()) - } ?: context.resources.getColor(ri.enabledBackground, context.theme) + } ?: context.resources.getColor(bgColor, context.theme) listOf(color, ALPHA_ENABLED) } else { listOf( @@ -251,21 +295,6 @@ class ControlViewHolder( ) } - status.setTextColor(fg) - - cws.control?.getCustomIcon()?.let { - // do not tint custom icons, assume the intended icon color is correct - icon.imageTintList = null - icon.setImageIcon(it) - } ?: run { - icon.setImageDrawable(ri.icon) - - // do not color app icons - if (deviceType != DeviceTypes.TYPE_ROUTINE) { - icon.imageTintList = fg - } - } - (clipLayer.getDrawable() as GradientDrawable).apply { val newBaseColor = if (behavior is ToggleRangeBehavior) { ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity) @@ -303,6 +332,77 @@ class ControlViewHolder( } } + private fun animateStatusChange(animated: Boolean, statusRowUpdater: () -> Unit) { + statusAnimator?.cancel() + + if (!animated) { + statusRowUpdater.invoke() + return + } + + if (isLoading) { + statusRowUpdater.invoke() + statusAnimator = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply { + repeatMode = ValueAnimator.REVERSE + repeatCount = ValueAnimator.INFINITE + duration = 500L + interpolator = Interpolators.LINEAR + startDelay = 900L + start() + } + } else { + val fadeOut = ObjectAnimator.ofFloat(status, "alpha", 0f).apply { + duration = 200L + interpolator = Interpolators.LINEAR + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + statusRowUpdater.invoke() + } + }) + } + val fadeIn = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply { + duration = 200L + interpolator = Interpolators.LINEAR + } + statusAnimator = AnimatorSet().apply { + playSequentially(fadeOut, fadeIn) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + status.alpha = STATUS_ALPHA_ENABLED + statusAnimator = null + } + }) + start() + } + } + } + + private fun updateStatusRow( + enabled: Boolean, + text: CharSequence, + drawable: Drawable, + color: ColorStateList, + control: Control? + ) { + setEnabled(enabled) + + status.text = text + status.setTextColor(color) + + control?.getCustomIcon()?.let { + // do not tint custom icons, assume the intended icon color is correct + icon.imageTintList = null + icon.setImageIcon(it) + } ?: run { + icon.setImageDrawable(drawable) + + // do not color app icons + if (deviceType != DeviceTypes.TYPE_ROUTINE) { + icon.imageTintList = color + } + } + } + private fun setEnabled(enabled: Boolean) { status.setEnabled(enabled) icon.setEnabled(enabled) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt index 722ade923208c..0c8e3ffc5201a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt @@ -24,7 +24,7 @@ class DefaultBehavior : Behavior { } override fun bind(cws: ControlWithState, colorOffset: Int) { - cvh.status.setText(cws.control?.getStatusText() ?: "") + cvh.setStatusText(cws.control?.getStatusText() ?: "") cvh.applyRenderInfo(false, colorOffset) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt index 124df32af5e0c..ba331f459a0d9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt @@ -97,6 +97,8 @@ private const val BUCKET_SIZE = 1000 private const val THERMOSTAT_RANGE = DeviceTypes.TYPE_THERMOSTAT * BUCKET_SIZE private val deviceColorMap = mapOf>( + (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to + Pair(R.color.control_default_foreground, R.color.control_default_background), (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to Pair(R.color.thermo_heat_foreground, R.color.control_enabled_thermo_heat_background), (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_COOL) to @@ -108,13 +110,9 @@ private val deviceColorMap = mapOf>( } private val deviceIconMap = mapOf( - THERMOSTAT_RANGE to IconState( - R.drawable.ic_device_thermostat_off, - R.drawable.ic_device_thermostat_on - ), (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to IconState( R.drawable.ic_device_thermostat_off, - R.drawable.ic_device_thermostat_on + R.drawable.ic_device_thermostat_off ), (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to IconState( R.drawable.ic_device_thermostat_off, @@ -130,7 +128,7 @@ private val deviceIconMap = mapOf( ), (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_ECO) to IconState( R.drawable.ic_device_thermostat_off, - R.drawable.ic_device_thermostat_on + R.drawable.ic_device_thermostat_off ), DeviceTypes.TYPE_THERMOSTAT to IconState( R.drawable.ic_device_thermostat_off, diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt index d8dceba352516..bf3835dba4fd8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt @@ -32,9 +32,12 @@ class StatusBehavior : Behavior { val msg = when (status) { Control.STATUS_ERROR -> R.string.controls_error_generic Control.STATUS_NOT_FOUND -> R.string.controls_error_removed - else -> com.android.internal.R.string.loading + else -> { + cvh.isLoading = true + com.android.internal.R.string.loading + } } - cvh.status.setText(cvh.context.getString(msg)) + cvh.setStatusText(cvh.context.getString(msg)) cvh.applyRenderInfo(false, colorOffset) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt index 2795c7af6c828..a7dc09bb17e56 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt @@ -39,7 +39,7 @@ class TemperatureControlBehavior : Behavior { override fun bind(cws: ControlWithState, colorOffset: Int) { this.control = cws.control!! - cvh.status.setText(control.getStatusText()) + cvh.setStatusText(control.getStatusText()) val ld = cvh.layout.getBackground() as LayerDrawable clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt index c432c09eeda42..dc7247cd70963 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt @@ -34,7 +34,6 @@ class ToggleBehavior : Behavior { override fun initialize(cvh: ControlViewHolder) { this.cvh = cvh - cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */) cvh.layout.setOnClickListener(View.OnClickListener() { cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked()) @@ -44,7 +43,7 @@ class ToggleBehavior : Behavior { override fun bind(cws: ControlWithState, colorOffset: Int) { this.control = cws.control!! - cvh.status.setText(control.getStatusText()) + cvh.setStatusText(control.getStatusText()) val controlTemplate = control.getControlTemplate() template = when (controlTemplate) { is ToggleTemplate -> controlTemplate diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index a09ed09bfcec4..1f0ca9be2eccd 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -31,7 +31,6 @@ import android.service.controls.templates.TemperatureControlTemplate import android.service.controls.templates.ToggleRangeTemplate import android.util.Log import android.util.MathUtils -import android.util.TypedValue import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent @@ -39,7 +38,6 @@ import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo -import android.widget.TextView import com.android.systemui.Interpolators import com.android.systemui.R import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL @@ -57,7 +55,6 @@ class ToggleRangeBehavior : Behavior { lateinit var control: Control lateinit var cvh: ControlViewHolder lateinit var rangeTemplate: RangeTemplate - lateinit var status: TextView lateinit var context: Context var currentStatusText: CharSequence = "" var currentRangeValue: String = "" @@ -71,10 +68,7 @@ class ToggleRangeBehavior : Behavior { override fun initialize(cvh: ControlViewHolder) { this.cvh = cvh - status = cvh.status - context = status.getContext() - - cvh.applyRenderInfo(false /* enabled */, colorOffset, false /* animated */) + context = cvh.context val gestureListener = ToggleRangeGestureListener(cvh.layout) val gestureDetector = GestureDetector(context, gestureListener) @@ -131,7 +125,6 @@ class ToggleRangeBehavior : Behavior { this.colorOffset = colorOffset currentStatusText = control.getStatusText() - status.setText(currentStatusText) // ControlViewHolder sets a long click listener, but we want to handle touch in // here instead, otherwise we'll have state conflicts. @@ -222,7 +215,7 @@ class ToggleRangeBehavior : Behavior { } fun beginUpdateRange() { - status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources() + cvh.setStatusTextSize(context.getResources() .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat()) } @@ -261,14 +254,13 @@ class ToggleRangeBehavior : Behavior { val newValue = levelToRangeValue(newLevel) currentRangeValue = format(rangeTemplate.getFormatString().toString(), DEFAULT_FORMAT, newValue) - val text = if (isDragging) { - currentRangeValue + if (isDragging) { + cvh.setStatusText(currentRangeValue, /* immediately */ true) } else { - "$currentStatusText $currentRangeValue" + cvh.setStatusText("$currentStatusText $currentRangeValue") } - status.setText(text) } else { - status.setText(currentStatusText) + cvh.setStatusText(currentStatusText) } } @@ -296,9 +288,9 @@ class ToggleRangeBehavior : Behavior { } fun endUpdateRange() { - status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources() + cvh.setStatusTextSize(context.getResources() .getDimensionPixelSize(R.dimen.control_status_normal).toFloat()) - status.setText("$currentStatusText $currentRangeValue") + cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true) cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(levelToRangeValue(clipLayer.getLevel())))) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt index 8ce2e617669ce..48f9458741352 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt @@ -23,6 +23,7 @@ import android.service.controls.Control import android.service.controls.templates.ControlTemplate import com.android.systemui.R +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL /** @@ -37,7 +38,6 @@ class TouchBehavior : Behavior { override fun initialize(cvh: ControlViewHolder) { this.cvh = cvh - cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */) cvh.layout.setOnClickListener(View.OnClickListener() { cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control) @@ -46,13 +46,14 @@ class TouchBehavior : Behavior { override fun bind(cws: ControlWithState, colorOffset: Int) { this.control = cws.control!! - cvh.status.setText(control.getStatusText()) + cvh.setStatusText(control.getStatusText()) template = control.getControlTemplate() val ld = cvh.layout.getBackground() as LayerDrawable clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) - clipLayer.setLevel(MIN_LEVEL) - cvh.applyRenderInfo(false, colorOffset) + val enabled = if (colorOffset > 0) true else false + clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL) + cvh.applyRenderInfo(enabled, colorOffset) } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 8e1854a469f1c..62744673e76e4 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -2221,7 +2221,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mBackgroundDrawable.setAlpha(0); float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); ObjectAnimator alphaAnimator = - ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 0f, 1f); + ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f); alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); alphaAnimator.setDuration(183); alphaAnimator.addUpdateListener((animation) -> { @@ -2234,8 +2234,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mContainer, "translationX", xOffset, 0f); - alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); - alphaAnimator.setDuration(350); + xAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + xAnimator.setDuration(350); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(alphaAnimator, xAnimator); @@ -2247,7 +2247,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, dismissWithAnimation(() -> { mContainer.setTranslationX(0); ObjectAnimator alphaAnimator = - ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 1f, 0f); + ObjectAnimator.ofFloat(mContainer, "alpha", 1f, 0f); alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); alphaAnimator.setDuration(233); alphaAnimator.addUpdateListener((animation) -> { @@ -2261,8 +2261,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mContainer, "translationX", 0f, xOffset); - alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); - alphaAnimator.setDuration(350); + xAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + xAnimator.setDuration(350); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(alphaAnimator, xAnimator);