Merge "Controls UI - Loading animations" into rvc-dev am: 31db4f456b
Change-Id: I83d6d50c6e814c672ceb5da0b522b81dc294fd51
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ private const val BUCKET_SIZE = 1000
|
||||
private const val THERMOSTAT_RANGE = DeviceTypes.TYPE_THERMOSTAT * BUCKET_SIZE
|
||||
|
||||
private val deviceColorMap = mapOf<Int, Pair<Int, Int>>(
|
||||
(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<Int, Pair<Int, Int>>(
|
||||
}
|
||||
|
||||
private val deviceIconMap = mapOf<Int, IconState>(
|
||||
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<Int, IconState>(
|
||||
),
|
||||
(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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user