commit 0be5ba54d30a9da6db2039a9ba76529ffff7071a
parent 738aad16509302b669354d90ac832a543ccb9691
Author: RebecaTudor <rebecatudor273@gmail.com>
Date: Mon, 29 Dec 2025 22:14:20 +0000
Bug 1975017 - Part 1 - Add compose slider. r=android-reviewers,gmalekpour
Differential Revision: https://phabricator.services.mozilla.com/D268664
Diffstat:
1 file changed, 252 insertions(+), 0 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/FontSizeSlider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/FontSizeSlider.kt
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.settings
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import mozilla.components.ui.colors.PhotonColors
+import org.mozilla.fenix.R
+import kotlin.math.roundToInt
+
+private const val HALF_ALPHA = 0.5F
+private const val BASE_SAMPLE_TEXT_SIZE = 14
+private const val BASE_SAMPLE_HEIGHT_LINE_DIFFERENCE = 6
+private const val START_VALUE = 50
+private const val END_VALUE = 200
+private const val INCREASE_STEP = 5
+
+/**
+ * The slider that changes websites font size.
+ *
+ * @param isEnabled Whether or not the slider can be used.
+ * @param value The current value of the slider.
+ * @param onValueChange Callback invoked continuously while the user moves the thumb.
+ * @param onValueChangeFinished Callback invoked once the dragging end.
+ */
+@Composable
+fun FontSizePreference(
+ isEnabled: Boolean,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ onValueChangeFinished: () -> Unit,
+) {
+ val alpha = if (isEnabled) 1f else HALF_ALPHA
+ // The values used to align with the top bar
+ val paddingFontSizeSection = PaddingValues(start = 72.dp, top = 16.dp, end = 16.dp, bottom = 16.dp)
+
+ Column(
+ modifier = Modifier
+ .alpha(alpha)
+ .padding(paddingFontSizeSection),
+ ) {
+ Text(
+ text = stringResource(id = R.string.preference_accessibility_font_size_title),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+
+ Text(
+ text = stringResource(id = R.string.preference_accessibility_text_size_summary),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ FontSizeSlider(
+ isEnabled = isEnabled,
+ value = value,
+ onValueChange = onValueChange,
+ onValueChangeFinished = onValueChangeFinished,
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ SampleText(fontSize = value)
+ }
+}
+
+@Composable
+private fun SampleText(fontSize: Float) {
+ val textSize = (BASE_SAMPLE_TEXT_SIZE * (fontSize / 100f))
+
+ Box(
+ modifier = Modifier
+ .wrapContentSize()
+ .background(color = PhotonColors.Violet05)
+ .padding(16.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.accessibility_text_size_sample_text_1),
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = textSize.sp,
+ lineHeight = (textSize + BASE_SAMPLE_HEIGHT_LINE_DIFFERENCE).sp,
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun FontSizeSlider(
+ isEnabled: Boolean,
+ start: Int = START_VALUE,
+ end: Int = END_VALUE,
+ step: Int = INCREASE_STEP,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ onValueChangeFinished: () -> Unit,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Slider(
+ value = value,
+ onValueChange = { newInput ->
+ // The slider input is continuous, but we want the value to change by 5.
+ val newSliderValue = newInput.snapToStep(start, step)
+
+ onValueChange(newSliderValue.toFloat())
+ },
+ valueRange = start.toFloat()..end.toFloat(),
+ modifier = Modifier.weight(1f),
+ enabled = isEnabled,
+ onValueChangeFinished = onValueChangeFinished,
+ thumb = { Thumb(isEnabled) },
+ track = { _ ->
+ // Calculate fraction of the slider that is active
+ val fraction by remember(value) {
+ derivedStateOf {
+ (value - start) / (end - start)
+ }
+ }
+
+ Track(fraction, isEnabled)
+ },
+ )
+
+ Text(
+ text = "${value.toInt()} %",
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+}
+
+/**
+ * Rounds the value to the nearest step.
+ */
+fun Float.snapToStep(start: Int, step: Int) = ((this - start) / step).roundToInt() * step + start
+
+/**
+ * Thumb is the draggable handle of the slider that user moves to change the value.
+ */
+@Composable
+private fun Thumb(isEnabled: Boolean) {
+ if (isEnabled) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 6.dp)
+ .size(12.dp)
+ .background(MaterialTheme.colorScheme.primary, CircleShape),
+ )
+ } else {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .padding(vertical = 6.dp)
+ .size(8.dp)
+ .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape)
+ .padding(6.dp),
+ ) {}
+ }
+}
+
+@Composable
+private fun Track(fraction: Float, isEnabled: Boolean) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(2.dp)
+ .background(
+ MaterialTheme.colorScheme.surfaceContainerHighest,
+ RoundedCornerShape(12.dp),
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ FilledTrack(fraction = fraction, isEnabled = isEnabled)
+ }
+}
+
+@Composable
+private fun FilledTrack(fraction: Float, isEnabled: Boolean) {
+ val color =
+ if (isEnabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHighest
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(fraction)
+ .height(2.dp)
+ .background(
+ color = color,
+ shape = RoundedCornerShape(12.dp),
+ ),
+ ) {}
+}
+
+@Preview
+@Composable
+private fun FontSizePreferencePreview() {
+ MaterialTheme {
+ Box(Modifier.background(MaterialTheme.colorScheme.surface)) {
+ FontSizePreference(
+ isEnabled = true,
+ value = 100f,
+ onValueChange = {},
+ onValueChangeFinished = {},
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun FontSizePreferenceDisabledPreview() {
+ MaterialTheme {
+ Box(Modifier.background(MaterialTheme.colorScheme.surface)) {
+ FontSizePreference(
+ isEnabled = false,
+ value = 200f,
+ onValueChange = {},
+ onValueChangeFinished = {},
+ )
+ }
+ }
+}