Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad45b17b authored by Michael Mikhail's avatar Michael Mikhail Committed by Android (Google) Code Review
Browse files

Merge "Add ringer buttons animation" into main

parents 1a3eda8b 5bf40cec
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -83,7 +83,7 @@ class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {

            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
            assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
                .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
                .isEqualTo(RingerDrawerState.Closed(normalRingerMode, normalRingerMode))
        }

    @Test
@@ -91,8 +91,9 @@ class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {
        testScope.runTest {
            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
            val vibrateRingerMode = RingerMode(RINGER_MODE_VIBRATE)
            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)

            setUpRingerModeAndOpenDrawer(RingerMode(RINGER_MODE_NORMAL))
            setUpRingerModeAndOpenDrawer(normalRingerMode)
            // Select vibrate ringer mode.
            underTest.onRingerButtonClicked(vibrateRingerMode)
            controller.getState()
@@ -103,7 +104,8 @@ class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {
            var uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
            assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
                .isEqualTo(vibrateRingerMode)
            assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
            assertThat(uiModel.drawerState)
                .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode, normalRingerMode))

            val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
            // Open drawer
@@ -120,7 +122,8 @@ class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {
            uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
            assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
                .isEqualTo(silentRingerMode)
            assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(silentRingerMode))
            assertThat(uiModel.drawerState)
                .isEqualTo(RingerDrawerState.Closed(silentRingerMode, vibrateRingerMode))
            assertThat(controller.hasScheduledTouchFeedback).isFalse()
            assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
        }
+1 −1
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <Transition
        android:id="@+id/transition"
        android:id="@+id/close_to_open_transition"
        app:constraintSetEnd="@+id/volume_dialog_ringer_drawer_open"
        app:constraintSetStart="@+id/volume_dialog_ringer_drawer_close"
        app:transitionEasing="path(0.05f, 0.7f, 0.1f, 1f)"
+163 −18
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.volume.dialog.ringer.ui.binder

import android.animation.ArgbEvaluator
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageButton
@@ -23,6 +25,10 @@ import androidx.annotation.LayoutRes
import androidx.compose.ui.util.fastForEachIndexed
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.R as internalR
import com.android.settingslib.Utils
import com.android.systemui.lifecycle.WindowLifecycleState
@@ -31,24 +37,44 @@ import com.android.systemui.lifecycle.viewModel
import com.android.systemui.res.R
import com.android.systemui.util.children
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonUiModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModelState
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel
import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
import javax.inject.Inject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

private const val CLOSE_DRAWER_DELAY = 300L

@VolumeDialogScope
class VolumeDialogRingerViewBinder
@Inject
constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Factory) {
    private val roundnessSpringForce =
        SpringForce(0F).apply {
            stiffness = 800F
            dampingRatio = 0.6F
        }
    private val colorSpringForce =
        SpringForce(0F).apply {
            stiffness = 3800F
            dampingRatio = 1F
        }
    private val rgbEvaluator = ArgbEvaluator()

    fun bind(view: View) {
        with(view) {
            val volumeDialogBackgroundView = requireViewById<View>(R.id.volume_dialog_background)
            val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
            val unselectedButtonUiModel = RingerButtonUiModel.getUnselectedButton(context)
            val selectedButtonUiModel = RingerButtonUiModel.getSelectedButton(context)

            repeatWhenAttached {
                viewModel(
                    traceName = "VolumeDialogRingerViewBinder",
@@ -61,26 +87,53 @@ constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Fact
                                is RingerViewModelState.Available -> {
                                    val uiModel = ringerState.uiModel

                                    bindDrawerButtons(viewModel, uiModel)

                                    // Set up view background and visibility
                                    drawerContainer.visibility = View.VISIBLE
                                    when (uiModel.drawerState) {
                                        is RingerDrawerState.Initial -> {
                                            drawerContainer.animateAndBindDrawerButtons(
                                                viewModel,
                                                uiModel,
                                                selectedButtonUiModel,
                                                unselectedButtonUiModel,
                                            )
                                            drawerContainer.closeDrawer(uiModel.currentButtonIndex)
                                            volumeDialogBackgroundView.setBackgroundResource(
                                                R.drawable.volume_dialog_background
                                            )
                                        }
                                        is RingerDrawerState.Closed -> {
                                            drawerContainer.closeDrawer(uiModel.currentButtonIndex)
                                            volumeDialogBackgroundView.setBackgroundResource(
                                            if (
                                                uiModel.selectedButton.ringerMode ==
                                                    uiModel.drawerState.currentMode
                                            ) {
                                                drawerContainer.animateAndBindDrawerButtons(
                                                    viewModel,
                                                    uiModel,
                                                    selectedButtonUiModel,
                                                    unselectedButtonUiModel,
                                                ) {
                                                    drawerContainer.closeDrawer(
                                                        uiModel.currentButtonIndex
                                                    )
                                                    volumeDialogBackgroundView
                                                        .setBackgroundResource(
                                                            R.drawable.volume_dialog_background
                                                        )
                                                }
                                            }
                                        }
                                        is RingerDrawerState.Open -> {
                                            drawerContainer.animateAndBindDrawerButtons(
                                                viewModel,
                                                uiModel,
                                                selectedButtonUiModel,
                                                unselectedButtonUiModel,
                                            )
                                            // Open drawer
                                            drawerContainer.transitionToEnd()
                                            drawerContainer.transitionToState(
                                                R.id.volume_dialog_ringer_drawer_open
                                            )
                                            if (
                                                uiModel.currentButtonIndex !=
                                                    uiModel.availableButtons.size - 1
@@ -106,45 +159,93 @@ constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Fact
        }
    }

    private fun View.bindDrawerButtons(
    private suspend fun MotionLayout.animateAndBindDrawerButtons(
        viewModel: VolumeDialogRingerDrawerViewModel,
        uiModel: RingerViewModel,
        selectedButtonUiModel: RingerButtonUiModel,
        unselectedButtonUiModel: RingerButtonUiModel,
        onAnimationEnd: Runnable? = null,
    ) {
        ensureChildCount(R.layout.volume_ringer_button, uiModel.availableButtons.size)
        if (
            uiModel.drawerState is RingerDrawerState.Closed &&
                uiModel.drawerState.currentMode != uiModel.drawerState.previousMode
        ) {
        val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
            val count = uiModel.availableButtons.size
        drawerContainer.ensureChildCount(R.layout.volume_ringer_button, count)
            val selectedButton =
                getChildAt(count - uiModel.currentButtonIndex - 1)
                    .requireViewById<ImageButton>(R.id.volume_drawer_button)
            val previousIndex =
                uiModel.availableButtons.indexOfFirst {
                    it?.ringerMode == uiModel.drawerState.previousMode
                }
            val unselectedButton =
                getChildAt(count - previousIndex - 1)
                    .requireViewById<ImageButton>(R.id.volume_drawer_button)

            // On roundness animation end.
            val roundnessAnimationEndListener =
                DynamicAnimation.OnAnimationEndListener { _, _, _, _ ->
                    postDelayed(
                        { bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) },
                        CLOSE_DRAWER_DELAY,
                    )
                }

            // We only need to execute on roundness animation end once.
            selectedButton.animateTo(selectedButtonUiModel, roundnessAnimationEndListener)
            unselectedButton.animateTo(unselectedButtonUiModel)
        } else {
            bindButtons(viewModel, uiModel, onAnimationEnd)
        }
    }

    private fun MotionLayout.bindButtons(
        viewModel: VolumeDialogRingerDrawerViewModel,
        uiModel: RingerViewModel,
        onAnimationEnd: Runnable? = null,
        isAnimated: Boolean = false,
    ) {
        val count = uiModel.availableButtons.size
        uiModel.availableButtons.fastForEachIndexed { index, ringerButton ->
            ringerButton?.let {
                val view = drawerContainer.getChildAt(count - index - 1)
                // TODO (b/369995871): object animator for button switch ( active <-> inactive )
                val view = getChildAt(count - index - 1)
                if (index == uiModel.currentButtonIndex) {
                    view.bindDrawerButton(uiModel.selectedButton, viewModel, isSelected = true)
                    view.bindDrawerButton(
                        uiModel.selectedButton,
                        viewModel,
                        isSelected = true,
                        isAnimated = isAnimated,
                    )
                } else {
                    view.bindDrawerButton(it, viewModel)
                    view.bindDrawerButton(it, viewModel, isAnimated)
                }
            }
        }
        onAnimationEnd?.run()
    }

    private fun View.bindDrawerButton(
        buttonViewModel: RingerButtonViewModel,
        viewModel: VolumeDialogRingerDrawerViewModel,
        isSelected: Boolean = false,
        isAnimated: Boolean = false,
    ) {
        with(requireViewById<ImageButton>(R.id.volume_drawer_button)) {
            setImageResource(buttonViewModel.imageResId)
            contentDescription = context.getString(buttonViewModel.contentDescriptionResId)
            if (isSelected) {
            if (isSelected && !isAnimated) {
                setBackgroundResource(R.drawable.volume_drawer_selection_bg)
                setColorFilter(
                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnPrimary)
                )
            } else {
                background = background.mutate()
            } else if (!isAnimated) {
                setBackgroundResource(R.drawable.volume_ringer_item_bg)
                setColorFilter(
                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnSurface)
                )
                background = background.mutate()
            }
            setOnClickListener {
                viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
@@ -171,9 +272,10 @@ constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Fact
    }

    private fun MotionLayout.closeDrawer(selectedIndex: Int) {
        setTransition(R.id.close_to_open_transition)
        cloneConstraintSet(R.id.volume_dialog_ringer_drawer_close)
            .adjustClosedConstraintsForDrawer(selectedIndex, this)
        transitionToStart()
        transitionToState(R.id.volume_dialog_ringer_drawer_close)
    }

    private fun ConstraintSet.adjustOpenConstraintsForDrawer(motionLayout: MotionLayout) {
@@ -263,4 +365,47 @@ constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Fact
        connect(button.id, ConstraintSet.START, motionLayout.id, ConstraintSet.START)
        connect(button.id, ConstraintSet.END, motionLayout.id, ConstraintSet.END)
    }

    private suspend fun ImageButton.animateTo(
        ringerButtonUiModel: RingerButtonUiModel,
        roundnessAnimationEndListener: DynamicAnimation.OnAnimationEndListener? = null,
    ) {
        val roundnessAnimation =
            SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce)
        val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce)
        val radius = (background as GradientDrawable).cornerRadius
        val cornerRadiusDiff =
            ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
        val roundnessAnimationUpdateListener =
            DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
                (background as GradientDrawable).cornerRadius = radius + value * cornerRadiusDiff
                background.invalidateSelf()
            }
        val colorAnimationUpdateListener =
            DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
                val currentIconColor =
                    rgbEvaluator.evaluate(
                        value.coerceIn(0F, 1F),
                        imageTintList?.colors?.first(),
                        ringerButtonUiModel.tintColor,
                    ) as Int
                val currentBgColor =
                    rgbEvaluator.evaluate(
                        value.coerceIn(0F, 1F),
                        (background as GradientDrawable).color?.colors?.get(0),
                        ringerButtonUiModel.backgroundColor,
                    ) as Int

                (background as GradientDrawable).setColor(currentBgColor)
                background.invalidateSelf()
                setColorFilter(currentIconColor)
            }
        coroutineScope {
            launch { colorAnimation.suspendAnimate(colorAnimationUpdateListener) }
            roundnessAnimation.suspendAnimate(
                roundnessAnimationUpdateListener,
                roundnessAnimationEndListener,
            )
        }
    }
}
+61 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.volume.dialog.ringer.ui.viewmodel

import android.content.Context
import com.android.internal.R as internalR
import com.android.settingslib.Utils
import com.android.systemui.res.R

/** Models the UI state of ringer button */
data class RingerButtonUiModel(
    /** Icon color. */
    val tintColor: Int,
    val backgroundColor: Int,
    val cornerRadius: Int,
) {
    companion object {
        fun getUnselectedButton(context: Context): RingerButtonUiModel {
            return RingerButtonUiModel(
                tintColor =
                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnSurface),
                backgroundColor =
                    Utils.getColorAttrDefaultColor(
                        context,
                        internalR.attr.materialColorSurfaceContainerHighest,
                    ),
                cornerRadius =
                    context.resources.getDimensionPixelSize(
                        R.dimen.volume_dialog_background_square_corner_radius
                    ),
            )
        }

        fun getSelectedButton(context: Context): RingerButtonUiModel {
            return RingerButtonUiModel(
                tintColor =
                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnPrimary),
                backgroundColor =
                    Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorPrimary),
                cornerRadius =
                    context.resources.getDimensionPixelSize(
                        R.dimen.volume_dialog_ringer_selected_button_background_radius
                    ),
            )
        }
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -25,7 +25,8 @@ sealed interface RingerDrawerState {
    data class Open(val mode: RingerMode) : RingerDrawerState

    /** When clicked to close drawer */
    data class Closed(val mode: RingerMode) : RingerDrawerState
    data class Closed(val currentMode: RingerMode, val previousMode: RingerMode) :
        RingerDrawerState

    /** Initial state when volume dialog is shown with a closed drawer. */
    interface Initial : RingerDrawerState {
Loading