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

Commit 19f5e2e5 authored by Luna Zhang's avatar Luna Zhang
Browse files

Add support for horizontal volume rocker

The volume rocker should be horizontal and placed at the top right corner of screen when audio tile details view is enabled.

Bug: b/378513663
Test: Manually tested
Flag: com.android.systemui.qs_tile_detailed_view
Change-Id: Ie74100a5e6045d11d3960879127fd771f6446845
parent 0870068e
Loading
Loading
Loading
Loading
+103 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright (C) 2025 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.
  -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/volume_dialog"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="top|right"
    android:alpha="0"
    android:clipChildren="false"
    android:minHeight="@dimen/volume_dialog_window_width">

    <View
        android:id="@+id/volume_dialog_background"
        android:layout_width="0dp"
        android:layout_height="@dimen/volume_dialog_width"
        android:layout_marginStart="@dimen/volume_dialog_background_top_margin"
        android:layout_marginEnd="@dimen/volume_dialog_background_margin_negative"
        android:background="@drawable/volume_dialog_background"
        app:layout_constraintStart_toStartOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintEnd_toEndOf="@+id/volume_dialog_bottom_section_container"
        app:layout_constraintBottom_toBottomOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintTop_toTopOf="@+id/volume_dialog_main_slider_container" />

    <FrameLayout
        android:id="@+id/volume_dialog_top_section_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="@dimen/volume_dialog_components_spacing"
        android:clipChildren="false"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="@+id/volume_dialog_background"
        app:layout_constraintWidth_default="spread"
        app:layout_constraintHeight_default="spread">

        <include layout="@layout/volume_dialog_top_section" />
    </FrameLayout>

    <include
        android:id="@+id/volume_dialog_main_slider_container"
        layout="@layout/volume_dialog_slider"
        android:layout_width="0dp"
        android:layout_height="@dimen/volume_dialog_slider_width"
        android:layout_marginStart="@dimen/volume_dialog_slider_vertical_margin"
        android:layout_marginEnd="@dimen/volume_dialog_slider_vertical_margin"
        android:layout_marginTop="@dimen/volume_dialog_window_margin"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_max="@dimen/volume_dialog_slider_height"
        app:layout_constraintHorizontal_bias="1" />

    <FrameLayout
        android:id="@+id/volume_dialog_bottom_section_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="@dimen/volume_dialog_components_spacing"
        android:clipChildren="false"
        app:layout_constraintStart_toEndOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/volume_dialog_background"
        app:layout_constraintBottom_toBottomOf="@+id/volume_dialog_background"
        app:layout_constraintWidth_default="wrap"
        app:layout_constraintHeight_default="wrap"
        app:layout_constraintHorizontal_bias="0">

        <include layout="@layout/volume_dialog_bottom_section" />
    </FrameLayout>

    <LinearLayout
        android:id="@+id/volume_dialog_floating_sliders_container"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/volume_dialog_floating_sliders_vertical_padding_negative"
        android:layout_marginEnd="@dimen/volume_dialog_floating_sliders_vertical_padding_negative"
        android:clipChildren="false"
        android:clipToOutline="false"
        android:clipToPadding="false"
        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
        android:gravity="end"
        android:orientation="vertical"
        android:showDividers="middle|beginning|end"
        app:layout_constraintStart_toStartOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintEnd_toEndOf="@+id/volume_dialog_main_slider_container"
        app:layout_constraintBottom_toTopOf="@+id/volume_dialog_background" />

</androidx.constraintlayout.widget.ConstraintLayout>
+19 −4
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.factory.VolumeDialogComponentFactory
import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
import javax.inject.Inject
import kotlinx.coroutines.awaitCancellation
@@ -40,7 +41,10 @@ constructor(
    @Application context: Context,
    private val componentFactory: VolumeDialogComponentFactory,
    private val visibilityInteractor: VolumeDialogVisibilityInteractor,
    desktopAudioTileDetailsFeatureInteractor: DesktopAudioTileDetailsFeatureInteractor,
) : ComponentDialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
    // Use horizontal volume dialog if the audio tile details view is enabled
    private val isVolumeDialogVertical = !desktopAudioTileDetailsFeatureInteractor.isEnabled()

    init {
        with(window!!) {
@@ -58,8 +62,13 @@ constructor(
                attributes.apply {
                    title = "VolumeDialog" // Not the same as Window#setTitle
                }
            if (isVolumeDialogVertical) {
                setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
                setGravity(Gravity.END)
            } else {
                setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                setGravity(Gravity.TOP or Gravity.END)
            }
        }
        setCancelable(false)
        setCanceledOnTouchOutside(true)
@@ -67,11 +76,17 @@ constructor(

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (isVolumeDialogVertical) {
            setContentView(R.layout.volume_dialog)
        } else {
            setContentView(R.layout.volume_dialog_horizontal)
        }
        requireViewById<View>(R.id.volume_dialog).repeatWhenAttached {
            coroutineScopeTraced("[Volume]dialog") {
                val component = componentFactory.create(this)
                with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) }
                with(component.volumeDialogViewBinder()) {
                    bind(this@VolumeDialog, isVolumeDialogVertical)
                }

                awaitCancellation()
            }
+34 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.domain.interactor

import android.content.Context
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import javax.inject.Inject

@VolumeDialogPluginScope
class DesktopAudioTileDetailsFeatureInteractor
@Inject
constructor(@Application private val context: Context) {
    fun isEnabled(): Boolean {
        return QsDetailedView.isEnabled &&
            context.resources.getBoolean(R.bool.config_enableDesktopAudioTileDetailsView)
    }
}
+18 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.app.tracing.coroutines.launchInTraced
import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel.OverscrollEventModel
@@ -31,7 +32,13 @@ import kotlinx.coroutines.flow.onEach
@VolumeDialogSliderScope
class VolumeDialogOverscrollViewBinder
@Inject
constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
constructor(
    private val viewModel: VolumeDialogOverscrollViewModel,
    desktopAudioTileDetailsFeatureInteractor: DesktopAudioTileDetailsFeatureInteractor,
) {

    // Use horizontal volume dialog if the audio tile details view is enabled
    private val isVolumeDialogVertical = !desktopAudioTileDetailsFeatureInteractor.isEnabled()

    /**
     * [viewsToAnimate] is an array of [View] to be affected by the overscroll animation. [view] is
@@ -47,7 +54,9 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
                        dampingRatio = 0.6f
                    }
                )
                .addUpdateListener { _, value, _ -> viewsToAnimate.setTranslationY(value) }
                .addUpdateListener { _, value, _ ->
                    viewsToAnimate.setTranslation(value, isVolumeDialogVertical)
                }

        viewModel.overscrollEvent
            .onEach { event ->
@@ -57,7 +66,7 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
                    }
                    is OverscrollEventModel.Move -> {
                        animation.cancel()
                        viewsToAnimate.setTranslationY(event.touchOffsetPx)
                        viewsToAnimate.setTranslation(event.touchOffsetPx, isVolumeDialogVertical)
                        animationValueHolder.value = event.touchOffsetPx
                    }
                }
@@ -66,8 +75,12 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
    }
}

private fun Array<View>.setTranslationY(translation: Float) {
private fun Array<View>.setTranslation(translation: Float, isVertical: Boolean) {
    for (viewToAnimate in this) {
        if (isVertical) {
            viewToAnimate.translationY = translation
        } else {
            viewToAnimate.translationX = translation
        }
    }
}
+20 −4
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
@@ -61,8 +62,11 @@ constructor(
    private val viewModel: VolumeDialogSliderViewModel,
    private val overscrollViewModel: VolumeDialogOverscrollViewModel,
    private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
    private val desktopAudioTileDetailsFeatureInteractor: DesktopAudioTileDetailsFeatureInteractor,
) {
    fun bind(view: View) {
        // Use horizontal volume dialog if the audio tile details view is enabled
        val isVolumeDialogVertical = !desktopAudioTileDetailsFeatureInteractor.isEnabled()
        val sliderComposeView: ComposeView = view.requireViewById(R.id.volume_dialog_slider)
        sliderComposeView.setContent {
            PlatformTheme {
@@ -70,6 +74,7 @@ constructor(
                    viewModel = viewModel,
                    overscrollViewModel = overscrollViewModel,
                    hapticsViewModelFactory = hapticsViewModelFactory,
                    isVolumeDialogVertical = isVolumeDialogVertical,
                )
            }
        }
@@ -82,6 +87,7 @@ private fun VolumeDialogSlider(
    viewModel: VolumeDialogSliderViewModel,
    overscrollViewModel: VolumeDialogOverscrollViewModel,
    hapticsViewModelFactory: SliderHapticsViewModel.Factory,
    isVolumeDialogVertical: Boolean,
    modifier: Modifier = Modifier,
) {
    val colors =
@@ -119,7 +125,7 @@ private fun VolumeDialogSlider(
        onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
        isEnabled = !sliderStateModel.isDisabled,
        isReverseDirection = true,
        isVertical = true,
        isVertical = isVolumeDialogVertical,
        colors = colors,
        interactionSource = interactionSource,
        haptics =
@@ -127,7 +133,12 @@ private fun VolumeDialogSlider(
                hapticsViewModelFactory = hapticsViewModelFactory,
                hapticConfigs =
                    VolumeHapticsConfigsProvider.continuousConfigs(SliderHapticFeedbackFilter()),
                orientation = Orientation.Vertical,
                orientation =
                    if (isVolumeDialogVertical) {
                        Orientation.Vertical
                    } else {
                        Orientation.Horizontal
                    },
            ),
        stepDistance = 1f,
        track = { sliderState ->
@@ -135,7 +146,7 @@ private fun VolumeDialogSlider(
                sliderState,
                colors = colors,
                isEnabled = !sliderStateModel.isDisabled,
                isVertical = true,
                isVertical = isVolumeDialogVertical,
                activeTrackEndIcon = { iconsState ->
                    SliderIcon(
                        icon = {
@@ -160,7 +171,12 @@ private fun VolumeDialogSlider(
                interactionSource = interactions,
                enabled = !sliderStateModel.isDisabled,
                colors = colors,
                thumbSize = DpSize(52.dp, 4.dp),
                thumbSize =
                    if (isVolumeDialogVertical) {
                        DpSize(52.dp, 4.dp)
                    } else {
                        DpSize(4.dp, 52.dp)
                    },
            )
        },
        accessibilityParams = AccessibilityParams(contentDescription = sliderStateModel.label),
Loading