1+ /*
2+ * Copyright 2025 The Android Open Source Project
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * https://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
17+ package com.example.compose.snippets.accessibility
18+
19+ import androidx.compose.foundation.background
20+ import androidx.compose.foundation.gestures.TransformableState
21+ import androidx.compose.foundation.gestures.transformable
22+ import androidx.compose.foundation.layout.Arrangement
23+ import androidx.compose.foundation.layout.Box
24+ import androidx.compose.foundation.layout.Column
25+ import androidx.compose.foundation.layout.Row
26+ import androidx.compose.foundation.layout.Spacer
27+ import androidx.compose.foundation.layout.fillMaxSize
28+ import androidx.compose.foundation.layout.fillMaxWidth
29+ import androidx.compose.foundation.layout.height
30+ import androidx.compose.foundation.layout.padding
31+ import androidx.compose.foundation.layout.size
32+ import androidx.compose.foundation.layout.width
33+ import androidx.compose.foundation.shape.RoundedCornerShape
34+ import androidx.compose.material.icons.Icons
35+ import androidx.compose.material.icons.filled.Person
36+ import androidx.compose.material3.Card
37+ import androidx.compose.material3.Icon
38+ import androidx.compose.material3.MaterialTheme
39+ import androidx.compose.material3.Switch
40+ import androidx.compose.material3.Text
41+ import androidx.compose.runtime.Composable
42+ import androidx.compose.runtime.CompositionLocalProvider
43+ import androidx.compose.runtime.getValue
44+ import androidx.compose.runtime.mutableFloatStateOf
45+ import androidx.compose.runtime.mutableStateOf
46+ import androidx.compose.runtime.remember
47+ import androidx.compose.runtime.setValue
48+ import androidx.compose.ui.Alignment
49+ import androidx.compose.ui.Modifier
50+ import androidx.compose.ui.graphics.Color
51+ import androidx.compose.ui.platform.LocalDensity
52+ import androidx.compose.ui.text.style.TextAlign
53+ import androidx.compose.ui.tooling.preview.Preview
54+ import androidx.compose.ui.unit.Density
55+ import androidx.compose.ui.unit.dp
56+
57+ // [START android_compose_accessibility_scalable_content_density_scaling]
58+ private class DensityScalingState (
59+ // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
60+ private val minScale : Float = 0.75f ,
61+ private val maxScale : Float = 3.5f ,
62+ private val currentDensity : Density
63+ ) {
64+ val transformableState = TransformableState { zoomChange, _, _ ->
65+ scaleFactor.floatValue =
66+ (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
67+ }
68+ val scaleFactor = mutableFloatStateOf(1f )
69+ fun scaledDensity (): Density {
70+ return Density (
71+ currentDensity.density * scaleFactor.floatValue,
72+ currentDensity.fontScale
73+ )
74+ }
75+ }
76+
77+ // [START_EXCLUDE silent]
78+ @Preview
79+ // [END_EXCLUDE silent]
80+ @Composable
81+ fun DensityScalingSample () {
82+ val currentDensity = LocalDensity .current
83+ val scaleState =
84+ remember(currentDensity) { DensityScalingState (currentDensity = currentDensity) }
85+
86+ Box (
87+ modifier = Modifier
88+ .fillMaxSize()
89+ .transformable(state = scaleState.transformableState),
90+ contentAlignment = Alignment .TopCenter
91+ ) {
92+ CompositionLocalProvider (
93+ LocalDensity provides scaleState.scaledDensity()
94+ ) {
95+ DemoCard ()
96+ }
97+ }
98+ }
99+ // [END android_compose_accessibility_scalable_content_density_scaling]
100+
101+ // [START android_compose_accessibility_scalable_content_font_scaling]
102+ class FontScaleState (
103+ // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
104+ private val minScale : Float = 0.75f ,
105+ private val maxScale : Float = 3.5f ,
106+ private val currentDensity : Density
107+ ) {
108+ val transformableState = TransformableState { zoomChange, _, _ ->
109+ scaleFactor.floatValue =
110+ (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
111+ }
112+ val scaleFactor = mutableFloatStateOf(1f )
113+ fun scaledFont (): Density {
114+ return Density (
115+ currentDensity.density,
116+ currentDensity.fontScale * scaleFactor.floatValue
117+ )
118+ }
119+ }
120+
121+ // [START_EXCLUDE silent]
122+ @Preview
123+ // [END_EXCLUDE silent]
124+ @Composable
125+ fun FontScalingSample () {
126+ val currentDensity = LocalDensity .current
127+ val scaleState = remember { FontScaleState (currentDensity = currentDensity) }
128+
129+ Box (
130+ modifier = Modifier
131+ .fillMaxSize()
132+ .transformable(state = scaleState.transformableState),
133+ contentAlignment = Alignment .TopCenter
134+ ) {
135+ CompositionLocalProvider (
136+ LocalDensity provides scaleState.scaledFont()
137+ ) {
138+ DemoCard ()
139+ }
140+ }
141+ }
142+ // [END android_compose_accessibility_scalable_content_font_scaling]
143+
144+ // [START android_compose_accessibility_scalable_content_demo_card]
145+ @Composable
146+ private fun DemoCard () {
147+ Card (
148+ modifier = Modifier
149+ .width(360 .dp)
150+ .padding(16 .dp),
151+ shape = RoundedCornerShape (12 .dp)
152+ ) {
153+ Column (
154+ modifier = Modifier .padding(16 .dp),
155+ verticalArrangement = Arrangement .spacedBy(16 .dp)
156+ ) {
157+ Text (" Demo Card" , style = MaterialTheme .typography.headlineMedium)
158+ var isChecked by remember { mutableStateOf(true ) }
159+ Row (verticalAlignment = Alignment .CenterVertically ) {
160+ Text (" Demo Switch" , Modifier .weight(1f ), style = MaterialTheme .typography.bodyLarge)
161+ Switch (checked = isChecked, onCheckedChange = { isChecked = it })
162+ }
163+ Row (verticalAlignment = Alignment .CenterVertically ) {
164+ Icon (Icons .Filled .Person , " Icon" , Modifier .size(32 .dp))
165+ Spacer (Modifier .width(8 .dp))
166+ Text (" Demo Icon" , style = MaterialTheme .typography.bodyLarge)
167+ }
168+ Row (
169+ Modifier .fillMaxWidth(),
170+ horizontalArrangement = Arrangement .SpaceBetween
171+ ) {
172+ Box (
173+ Modifier
174+ .width(100 .dp)
175+ .weight(1f )
176+ .height(80 .dp)
177+ .background(Color .Blue )
178+ )
179+ Box (
180+ Modifier
181+ .width(100 .dp)
182+ .weight(1f )
183+ .height(80 .dp)
184+ .background(Color .Red )
185+ )
186+ }
187+ Text (
188+ " Demo Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit," +
189+ " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." ,
190+ style = MaterialTheme .typography.bodyMedium,
191+ textAlign = TextAlign .Justify
192+ )
193+ }
194+ }
195+ }
196+ // [END android_compose_accessibility_scalable_content_demo_card]
0 commit comments