• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2021 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 @file:Suppress("DEPRECATION")
18 
19 package com.google.accompanist.swiperefresh
20 
21 import androidx.compose.animation.Crossfade
22 import androidx.compose.animation.core.LinearOutSlowInEasing
23 import androidx.compose.animation.core.animate
24 import androidx.compose.animation.core.tween
25 import androidx.compose.foundation.Image
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.foundation.layout.fillMaxSize
28 import androidx.compose.foundation.layout.size
29 import androidx.compose.foundation.shape.CircleShape
30 import androidx.compose.foundation.shape.CornerSize
31 import androidx.compose.material.CircularProgressIndicator
32 import androidx.compose.material.MaterialTheme
33 import androidx.compose.material.Surface
34 import androidx.compose.material.contentColorFor
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.Immutable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.getValue
39 import androidx.compose.runtime.mutableStateOf
40 import androidx.compose.runtime.remember
41 import androidx.compose.runtime.setValue
42 import androidx.compose.ui.Alignment
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.graphics.Color
45 import androidx.compose.ui.graphics.Shape
46 import androidx.compose.ui.graphics.graphicsLayer
47 import androidx.compose.ui.platform.LocalDensity
48 import androidx.compose.ui.unit.Dp
49 import androidx.compose.ui.unit.dp
50 
51 /**
52  * A class to encapsulate details of different indicator sizes.
53  *
54  * @param size The overall size of the indicator.
55  * @param arcRadius The radius of the arc.
56  * @param strokeWidth The width of the arc stroke.
57  * @param arrowWidth The width of the arrow.
58  * @param arrowHeight The height of the arrow.
59  */
60 @Immutable
61 private data class SwipeRefreshIndicatorSizes(
62     val size: Dp,
63     val arcRadius: Dp,
64     val strokeWidth: Dp,
65     val arrowWidth: Dp,
66     val arrowHeight: Dp,
67 )
68 
69 /**
70  * The default/normal size values for [SwipeRefreshIndicator].
71  */
72 private val DefaultSizes = SwipeRefreshIndicatorSizes(
73     size = 40.dp,
74     arcRadius = 7.5.dp,
75     strokeWidth = 2.5.dp,
76     arrowWidth = 10.dp,
77     arrowHeight = 5.dp,
78 )
79 
80 /**
81  * The 'large' size values for [SwipeRefreshIndicator].
82  */
83 private val LargeSizes = SwipeRefreshIndicatorSizes(
84     size = 56.dp,
85     arcRadius = 11.dp,
86     strokeWidth = 3.dp,
87     arrowWidth = 12.dp,
88     arrowHeight = 6.dp,
89 )
90 
91 /**
92  * Indicator composable which is typically used in conjunction with [SwipeRefresh].
93  *
94  * @param state The [SwipeRefreshState] passed into the [SwipeRefresh] `indicator` block.
95  * @param modifier The modifier to apply to this layout.
96  * @param fade Whether the arrow should fade in/out as it is scrolled in. Defaults to true.
97  * @param scale Whether the indicator should scale up/down as it is scrolled in. Defaults to false.
98  * @param arrowEnabled Whether an arrow should be drawn on the indicator. Defaults to true.
99  * @param backgroundColor The color of the indicator background surface.
100  * @param contentColor The color for the indicator's contents.
101  * @param shape The shape of the indicator background surface. Defaults to [CircleShape].
102  * @param largeIndication Whether the indicator should be 'large' or not. Defaults to false.
103  * @param elevation The size of the shadow below the indicator.
104  */
105 @Deprecated(
106     """
107      accompanist/swiperefresh is deprecated.
108      The androidx.compose equivalent of SwipeRefreshIndicator() is PullRefreshIndicator().
109      For more migration information, please visit https://google.github.io/accompanist/swiperefresh/#migration
110     """
111 )
112 @Composable
113 public fun SwipeRefreshIndicator(
114     state: SwipeRefreshState,
115     refreshTriggerDistance: Dp,
116     modifier: Modifier = Modifier,
117     fade: Boolean = true,
118     scale: Boolean = false,
119     arrowEnabled: Boolean = true,
120     backgroundColor: Color = MaterialTheme.colors.surface,
121     contentColor: Color = contentColorFor(backgroundColor),
122     shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
123     refreshingOffset: Dp = 16.dp,
124     largeIndication: Boolean = false,
125     elevation: Dp = 6.dp,
126 ) {
127     val sizes = if (largeIndication) LargeSizes else DefaultSizes
128 
129     val indicatorRefreshTrigger = with(LocalDensity.current) { refreshTriggerDistance.toPx() }
130 
131     val indicatorHeight = with(LocalDensity.current) { sizes.size.roundToPx() }
132     val refreshingOffsetPx = with(LocalDensity.current) { refreshingOffset.toPx() }
133 
134     val slingshot = rememberUpdatedSlingshot(
135         offsetY = state.indicatorOffset,
136         maxOffsetY = indicatorRefreshTrigger,
137         height = indicatorHeight,
138     )
139 
140     var offset by remember { mutableStateOf(0f) }
141 
142     if (state.isSwipeInProgress) {
143         // If the user is currently swiping, we use the 'slingshot' offset directly
144         offset = slingshot.offset.toFloat()
145     } else {
146         // If there's no swipe currently in progress, animate to the correct resting position
147         LaunchedEffect(state.isRefreshing) {
148             animate(
149                 initialValue = offset,
150                 targetValue = when {
151                     state.isRefreshing -> indicatorHeight + refreshingOffsetPx
152                     else -> 0f
153                 }
154             ) { value, _ ->
155                 offset = value
156             }
157         }
158     }
159 
160     val adjustedElevation = when {
161         state.isRefreshing -> elevation
162         offset > 0.5f -> elevation
163         else -> 0.dp
164     }
165 
166     Surface(
167         modifier = modifier
168             .size(size = sizes.size)
169             .graphicsLayer {
170                 // Translate the indicator according to the slingshot
171                 translationY = offset - indicatorHeight
172 
173                 val scaleFraction = if (scale && !state.isRefreshing) {
174                     val progress = offset / indicatorRefreshTrigger.coerceAtLeast(1f)
175 
176                     // We use LinearOutSlowInEasing to speed up the scale in
177                     LinearOutSlowInEasing
178                         .transform(progress)
179                         .coerceIn(0f, 1f)
180                 } else 1f
181 
182                 scaleX = scaleFraction
183                 scaleY = scaleFraction
184             },
185         shape = shape,
186         color = backgroundColor,
187         elevation = adjustedElevation
188     ) {
189         val painter = remember { CircularProgressPainter() }
190         painter.arcRadius = sizes.arcRadius
191         painter.strokeWidth = sizes.strokeWidth
192         painter.arrowWidth = sizes.arrowWidth
193         painter.arrowHeight = sizes.arrowHeight
194         painter.arrowEnabled = arrowEnabled && !state.isRefreshing
195         painter.color = contentColor
196         val alpha = if (fade) {
197             (state.indicatorOffset / indicatorRefreshTrigger).coerceIn(0f, 1f)
198         } else {
199             1f
200         }
201         painter.alpha = alpha
202 
203         painter.startTrim = slingshot.startTrim
204         painter.endTrim = slingshot.endTrim
205         painter.rotation = slingshot.rotation
206         painter.arrowScale = slingshot.arrowScale
207 
208         // This shows either an Image with CircularProgressPainter or a CircularProgressIndicator,
209         // depending on refresh state
210         Crossfade(
211             targetState = state.isRefreshing,
212             animationSpec = tween(durationMillis = CrossfadeDurationMs)
213         ) { refreshing ->
214             Box(
215                 modifier = Modifier.fillMaxSize(),
216                 contentAlignment = Alignment.Center
217             ) {
218                 if (refreshing) {
219                     val circleSize = (sizes.arcRadius + sizes.strokeWidth) * 2
220                     CircularProgressIndicator(
221                         color = contentColor,
222                         strokeWidth = sizes.strokeWidth,
223                         modifier = Modifier.size(circleSize),
224                     )
225                 } else {
226                     Image(
227                         painter = painter,
228                         contentDescription = "Refreshing"
229                     )
230                 }
231             }
232         }
233     }
234 }
235 
236 private const val CrossfadeDurationMs = 100
237