• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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  *      http://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.android.systemui.common.ui.compose
18 
19 import androidx.compose.animation.core.animateDpAsState
20 import androidx.compose.foundation.Canvas
21 import androidx.compose.foundation.layout.Arrangement.spacedBy
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.size
24 import androidx.compose.foundation.layout.wrapContentWidth
25 import androidx.compose.foundation.pager.PagerState
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.derivedStateOf
28 import androidx.compose.runtime.getValue
29 import androidx.compose.runtime.remember
30 import androidx.compose.runtime.rememberCoroutineScope
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.geometry.CornerRadius
34 import androidx.compose.ui.geometry.Offset
35 import androidx.compose.ui.geometry.Size
36 import androidx.compose.ui.graphics.Color
37 import androidx.compose.ui.graphics.drawscope.DrawScope
38 import androidx.compose.ui.graphics.drawscope.scale
39 import androidx.compose.ui.semantics.pageLeft
40 import androidx.compose.ui.semantics.pageRight
41 import androidx.compose.ui.semantics.semantics
42 import androidx.compose.ui.semantics.stateDescription
43 import androidx.compose.ui.unit.Dp
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.dp
46 import com.android.app.tracing.coroutines.launchTraced as launch
47 import kotlin.math.absoluteValue
48 import kotlinx.coroutines.CoroutineScope
49 import platform.test.motion.compose.values.MotionTestValueKey
50 import platform.test.motion.compose.values.motionTestValues
51 
52 @Composable
53 fun PagerDots(
54     pagerState: PagerState,
55     activeColor: Color,
56     nonActiveColor: Color,
57     modifier: Modifier = Modifier,
58     dotSize: Dp = 6.dp,
59     spaceSize: Dp = 4.dp,
60 ) {
61     if (pagerState.pageCount < 2) {
62         return
63     }
64     val inPageTransition by
65         remember(pagerState) {
66             derivedStateOf {
67                 pagerState.currentPageOffsetFraction.absoluteValue > 0.05 &&
68                     !pagerState.isOverscrolling()
69             }
70         }
71     val coroutineScope = rememberCoroutineScope()
72     val doubleDotWidth = dotSize * 2 + spaceSize
73     val activeMarkerWidth by
74         animateDpAsState(
75             targetValue = if (inPageTransition) doubleDotWidth else dotSize,
76             label = "PagerDotsTransitionAnimation",
77         )
78     val cornerRadius = dotSize / 2
79 
80     fun DrawScope.drawDoubleRect(withPrevious: Boolean, width: Dp) {
81         drawRoundRect(
82             topLeft =
83                 Offset(
84                     if (withPrevious) {
85                         dotSize.toPx() - width.toPx()
86                     } else {
87                         -(dotSize.toPx() + spaceSize.toPx())
88                     },
89                     0f,
90                 ),
91             color = activeColor,
92             size = Size(width.toPx(), dotSize.toPx()),
93             cornerRadius = CornerRadius(cornerRadius.toPx()),
94         )
95     }
96 
97     Row(
98         modifier =
99             modifier
100                 .motionTestValues { activeMarkerWidth exportAs PagerDotsMotionKeys.indicatorWidth }
101                 .wrapContentWidth()
102                 .pagerDotsSemantics(pagerState, coroutineScope),
103         horizontalArrangement = spacedBy(spaceSize),
104         verticalAlignment = Alignment.CenterVertically,
105     ) {
106         // This means that the active rounded rect has to be drawn between the current page
107         // and the previous one (as we are animating back), or the current one if not transitioning
108         val withPrevious by
109             remember(pagerState) {
110                 derivedStateOf {
111                     pagerState.currentPageOffsetFraction <= 0 || pagerState.isOverscrolling()
112                 }
113             }
114         repeat(pagerState.pageCount) { page ->
115             Canvas(Modifier.size(dotSize)) {
116                 val rtl = layoutDirection == LayoutDirection.Rtl
117                 scale(if (rtl) -1f else 1f, 1f, Offset(0f, center.y)) {
118                     drawCircle(nonActiveColor)
119                     // We always want to draw the rounded rect on the rightmost dot iteration, so
120                     // the inactive dot is always drawn behind.
121                     // This means that:
122                     // * if we are scrolling back, we draw it when we are in the current page (so it
123                     //   extends between this page and the previous one).
124                     // * if we are scrolling forward, we draw it when we are in the next page (so it
125                     //   extends between the next page and the current one).
126                     // * if we are not scrolling, withPrevious is true (pageOffset 0) and we
127                     //   draw in the current page.
128                     // drawDoubleRect calculates the offset based on the above.
129                     if (
130                         withPrevious && page == pagerState.currentPage ||
131                             (!withPrevious && page == pagerState.currentPage + 1)
132                     ) {
133                         drawDoubleRect(withPrevious, activeMarkerWidth)
134                     }
135                 }
136             }
137         }
138     }
139 }
140 
141 object PagerDotsMotionKeys {
142     val indicatorWidth = MotionTestValueKey<Dp>("indicatorWidth")
143 }
144 
pagerDotsSemanticsnull145 private fun Modifier.pagerDotsSemantics(
146     pagerState: PagerState,
147     coroutineScope: CoroutineScope,
148 ): Modifier {
149     return then(
150         Modifier.semantics {
151             pageLeft {
152                 if (pagerState.canScrollBackward) {
153                     coroutineScope.launch {
154                         pagerState.animateScrollToPage(pagerState.currentPage - 1)
155                     }
156                     true
157                 } else {
158                     false
159                 }
160             }
161             pageRight {
162                 if (pagerState.canScrollForward) {
163                     coroutineScope.launch {
164                         pagerState.animateScrollToPage(pagerState.currentPage + 1)
165                     }
166                     true
167                 } else {
168                     false
169                 }
170             }
171             stateDescription = "Page ${pagerState.settledPage + 1} of ${pagerState.pageCount}"
172         }
173     )
174 }
175 
isOverscrollingnull176 private fun PagerState.isOverscrolling(): Boolean {
177     val position = currentPage + currentPageOffsetFraction
178     return position < 0 || position > pageCount - 1
179 }
180