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