Reverts to the non-Google kotlin style.
This commit is contained in:
@@ -42,19 +42,18 @@ import androidx.compose.ui.unit.Velocity
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*/
|
||||
fun Modifier.pullRefresh(
|
||||
state: PullRefreshState,
|
||||
enabled: Boolean = true,
|
||||
) =
|
||||
inspectable(
|
||||
state: PullRefreshState,
|
||||
enabled: Boolean = true,
|
||||
) = inspectable(
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["state"] = state
|
||||
properties["enabled"] = enabled
|
||||
},
|
||||
) {
|
||||
debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["state"] = state
|
||||
properties["enabled"] = enabled
|
||||
},
|
||||
) {
|
||||
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
|
||||
@@ -78,49 +77,48 @@ fun Modifier.pullRefresh(
|
||||
* @sample androidx.compose.material.samples.CustomPullRefreshSample
|
||||
*/
|
||||
fun Modifier.pullRefresh(
|
||||
onPull: (pullDelta: Float) -> Float,
|
||||
onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
enabled: Boolean = true,
|
||||
) =
|
||||
inspectable(
|
||||
onPull: (pullDelta: Float) -> Float,
|
||||
onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
enabled: Boolean = true,
|
||||
) = inspectable(
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["onPull"] = onPull
|
||||
properties["onRelease"] = onRelease
|
||||
properties["enabled"] = enabled
|
||||
},
|
||||
) {
|
||||
debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["onPull"] = onPull
|
||||
properties["onRelease"] = onRelease
|
||||
properties["enabled"] = enabled
|
||||
},
|
||||
) {
|
||||
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
|
||||
}
|
||||
}
|
||||
|
||||
private class PullRefreshNestedScrollConnection(
|
||||
private val onPull: (pullDelta: Float) -> Float,
|
||||
private val onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
private val enabled: Boolean,
|
||||
private val onPull: (pullDelta: Float) -> Float,
|
||||
private val onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
private val enabled: Boolean,
|
||||
) : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset =
|
||||
when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
|
||||
else -> Offset.Zero
|
||||
}
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset =
|
||||
when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset =
|
||||
when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
|
||||
else -> Offset.Zero
|
||||
}
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset =
|
||||
when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
return Velocity(0f, onRelease(available.y))
|
||||
}
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
return Velocity(0f, onRelease(available.y))
|
||||
}
|
||||
}
|
||||
|
||||
+111
-111
@@ -70,148 +70,148 @@ import kotlin.math.pow
|
||||
*/
|
||||
@Composable
|
||||
fun PullRefreshIndicator(
|
||||
refreshing: Boolean,
|
||||
state: PullRefreshState,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
scale: Boolean = false,
|
||||
refreshing: Boolean,
|
||||
state: PullRefreshState,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
scale: Boolean = false,
|
||||
) {
|
||||
val showElevation by
|
||||
remember(refreshing, state) { derivedStateOf { refreshing || state.position > 0.5f } }
|
||||
val showElevation by
|
||||
remember(refreshing, state) { derivedStateOf { refreshing || state.position > 0.5f } }
|
||||
|
||||
Surface(
|
||||
modifier = modifier.size(IndicatorSize).pullRefreshIndicatorTransform(state, scale),
|
||||
shape = SpinnerShape,
|
||||
color = backgroundColor,
|
||||
shadowElevation = if (showElevation) Elevation else 0.dp,
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = refreshing,
|
||||
animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS),
|
||||
) { refreshing ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
|
||||
Surface(
|
||||
modifier = modifier.size(IndicatorSize).pullRefreshIndicatorTransform(state, scale),
|
||||
shape = SpinnerShape,
|
||||
color = backgroundColor,
|
||||
shadowElevation = if (showElevation) Elevation else 0.dp,
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = refreshing,
|
||||
animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS),
|
||||
) { refreshing ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
|
||||
|
||||
if (refreshing) {
|
||||
CircularProgressIndicator(
|
||||
color = contentColor,
|
||||
strokeWidth = StrokeWidth,
|
||||
modifier = Modifier.size(spinnerSize),
|
||||
)
|
||||
} else {
|
||||
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
|
||||
if (refreshing) {
|
||||
CircularProgressIndicator(
|
||||
color = contentColor,
|
||||
strokeWidth = StrokeWidth,
|
||||
modifier = Modifier.size(spinnerSize),
|
||||
)
|
||||
} else {
|
||||
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Modifier.size MUST be specified. */
|
||||
@Composable
|
||||
private fun CircularArrowIndicator(
|
||||
state: PullRefreshState,
|
||||
color: Color,
|
||||
modifier: Modifier,
|
||||
state: PullRefreshState,
|
||||
color: Color,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
|
||||
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
|
||||
|
||||
val targetAlpha by
|
||||
remember(state) { derivedStateOf { if (state.progress >= 1f) MAX_ALPHA else MIN_ALPHA } }
|
||||
val targetAlpha by
|
||||
remember(state) { derivedStateOf { if (state.progress >= 1f) MAX_ALPHA else MIN_ALPHA } }
|
||||
|
||||
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
|
||||
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
|
||||
|
||||
// Empty semantics for tests
|
||||
Canvas(modifier.semantics {}) {
|
||||
val values = ArrowValues(state.progress)
|
||||
val alpha = alphaState.value
|
||||
// Empty semantics for tests
|
||||
Canvas(modifier.semantics {}) {
|
||||
val values = ArrowValues(state.progress)
|
||||
val alpha = alphaState.value
|
||||
|
||||
rotate(degrees = values.rotation) {
|
||||
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
|
||||
val arcBounds =
|
||||
Rect(
|
||||
size.center.x - arcRadius,
|
||||
size.center.y - arcRadius,
|
||||
size.center.x + arcRadius,
|
||||
size.center.y + arcRadius,
|
||||
)
|
||||
drawArc(
|
||||
color = color,
|
||||
alpha = alpha,
|
||||
startAngle = values.startAngle,
|
||||
sweepAngle = values.endAngle - values.startAngle,
|
||||
useCenter = false,
|
||||
topLeft = arcBounds.topLeft,
|
||||
size = arcBounds.size,
|
||||
style =
|
||||
Stroke(
|
||||
width = StrokeWidth.toPx(),
|
||||
cap = StrokeCap.Square,
|
||||
),
|
||||
)
|
||||
drawArrow(path, arcBounds, color, alpha, values)
|
||||
rotate(degrees = values.rotation) {
|
||||
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
|
||||
val arcBounds =
|
||||
Rect(
|
||||
size.center.x - arcRadius,
|
||||
size.center.y - arcRadius,
|
||||
size.center.x + arcRadius,
|
||||
size.center.y + arcRadius,
|
||||
)
|
||||
drawArc(
|
||||
color = color,
|
||||
alpha = alpha,
|
||||
startAngle = values.startAngle,
|
||||
sweepAngle = values.endAngle - values.startAngle,
|
||||
useCenter = false,
|
||||
topLeft = arcBounds.topLeft,
|
||||
size = arcBounds.size,
|
||||
style =
|
||||
Stroke(
|
||||
width = StrokeWidth.toPx(),
|
||||
cap = StrokeCap.Square,
|
||||
),
|
||||
)
|
||||
drawArrow(path, arcBounds, color, alpha, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private class ArrowValues(
|
||||
val rotation: Float,
|
||||
val startAngle: Float,
|
||||
val endAngle: Float,
|
||||
val scale: Float,
|
||||
val rotation: Float,
|
||||
val startAngle: Float,
|
||||
val endAngle: Float,
|
||||
val scale: Float,
|
||||
)
|
||||
|
||||
private fun ArrowValues(progress: Float): ArrowValues {
|
||||
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
|
||||
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
|
||||
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
|
||||
// Calculations based on SwipeRefreshLayout specification.
|
||||
val endTrim = adjustedPercent * MAX_PROGRESS_ARC
|
||||
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
|
||||
val startAngle = rotation * 360
|
||||
val endAngle = (rotation + endTrim) * 360
|
||||
val scale = min(1f, adjustedPercent)
|
||||
// Calculations based on SwipeRefreshLayout specification.
|
||||
val endTrim = adjustedPercent * MAX_PROGRESS_ARC
|
||||
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
|
||||
val startAngle = rotation * 360
|
||||
val endAngle = (rotation + endTrim) * 360
|
||||
val scale = min(1f, adjustedPercent)
|
||||
|
||||
return ArrowValues(rotation, startAngle, endAngle, scale)
|
||||
return ArrowValues(rotation, startAngle, endAngle, scale)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawArrow(
|
||||
arrow: Path,
|
||||
bounds: Rect,
|
||||
color: Color,
|
||||
alpha: Float,
|
||||
values: ArrowValues,
|
||||
arrow: Path,
|
||||
bounds: Rect,
|
||||
color: Color,
|
||||
alpha: Float,
|
||||
values: ArrowValues,
|
||||
) {
|
||||
arrow.reset()
|
||||
arrow.moveTo(0f, 0f) // Move to left corner
|
||||
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
|
||||
arrow.reset()
|
||||
arrow.moveTo(0f, 0f) // Move to left corner
|
||||
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
|
||||
|
||||
// Line to tip of arrow
|
||||
arrow.lineTo(
|
||||
x = ArrowWidth.toPx() * values.scale / 2,
|
||||
y = ArrowHeight.toPx() * values.scale,
|
||||
)
|
||||
// Line to tip of arrow
|
||||
arrow.lineTo(
|
||||
x = ArrowWidth.toPx() * values.scale / 2,
|
||||
y = ArrowHeight.toPx() * values.scale,
|
||||
)
|
||||
|
||||
val radius = min(bounds.width, bounds.height) / 2f
|
||||
val inset = ArrowWidth.toPx() * values.scale / 2f
|
||||
arrow.translate(
|
||||
Offset(
|
||||
x = radius + bounds.center.x - inset,
|
||||
y = bounds.center.y + StrokeWidth.toPx() / 2f,
|
||||
),
|
||||
)
|
||||
arrow.close()
|
||||
rotate(degrees = values.endAngle) { drawPath(path = arrow, color = color, alpha = alpha) }
|
||||
val radius = min(bounds.width, bounds.height) / 2f
|
||||
val inset = ArrowWidth.toPx() * values.scale / 2f
|
||||
arrow.translate(
|
||||
Offset(
|
||||
x = radius + bounds.center.x - inset,
|
||||
y = bounds.center.y + StrokeWidth.toPx() / 2f,
|
||||
),
|
||||
)
|
||||
arrow.close()
|
||||
rotate(degrees = values.endAngle) { drawPath(path = arrow, color = color, alpha = alpha) }
|
||||
}
|
||||
|
||||
private const val CROSSFADE_DURATION_MS = 100
|
||||
|
||||
+33
-34
@@ -37,42 +37,41 @@ import androidx.compose.ui.platform.inspectable
|
||||
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
|
||||
*/
|
||||
fun Modifier.pullRefreshIndicatorTransform(
|
||||
state: PullRefreshState,
|
||||
scale: Boolean = false,
|
||||
) =
|
||||
inspectable(
|
||||
state: PullRefreshState,
|
||||
scale: Boolean = false,
|
||||
) = inspectable(
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "pullRefreshIndicatorTransform"
|
||||
properties["state"] = state
|
||||
properties["scale"] = scale
|
||||
},
|
||||
) {
|
||||
debugInspectorInfo {
|
||||
name = "pullRefreshIndicatorTransform"
|
||||
properties["state"] = state
|
||||
properties["scale"] = scale
|
||||
},
|
||||
) {
|
||||
Modifier
|
||||
// Essentially we only want to clip the at the top, so the indicator will not appear when
|
||||
// the position is 0. It is preferable to clip the indicator as opposed to the layout that
|
||||
// contains the indicator, as this would also end up clipping shadows drawn by items in a
|
||||
// list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
|
||||
// for the other dimensions to allow for more room for elevation / arbitrary indicators - we
|
||||
// only ever really want to clip at the top edge.
|
||||
.drawWithContent {
|
||||
clipRect(
|
||||
top = 0f,
|
||||
left = -Float.MAX_VALUE,
|
||||
right = Float.MAX_VALUE,
|
||||
bottom = Float.MAX_VALUE,
|
||||
) {
|
||||
this@drawWithContent.drawContent()
|
||||
// Essentially we only want to clip the at the top, so the indicator will not appear when
|
||||
// the position is 0. It is preferable to clip the indicator as opposed to the layout that
|
||||
// contains the indicator, as this would also end up clipping shadows drawn by items in a
|
||||
// list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
|
||||
// for the other dimensions to allow for more room for elevation / arbitrary indicators - we
|
||||
// only ever really want to clip at the top edge.
|
||||
.drawWithContent {
|
||||
clipRect(
|
||||
top = 0f,
|
||||
left = -Float.MAX_VALUE,
|
||||
right = Float.MAX_VALUE,
|
||||
bottom = Float.MAX_VALUE,
|
||||
) {
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
translationY = state.position - size.height
|
||||
.graphicsLayer {
|
||||
translationY = state.position - size.height
|
||||
|
||||
if (scale && !state.refreshing) {
|
||||
val scaleFraction =
|
||||
LinearOutSlowInEasing.transform(state.position / state.threshold).coerceIn(0f, 1f)
|
||||
scaleX = scaleFraction
|
||||
scaleY = scaleFraction
|
||||
if (scale && !state.refreshing) {
|
||||
val scaleFraction =
|
||||
LinearOutSlowInEasing.transform(state.position / state.threshold).coerceIn(0f, 1f)
|
||||
scaleX = scaleFraction
|
||||
scaleY = scaleFraction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Creates a [PullRefreshState] that is remembered across compositions.
|
||||
@@ -55,33 +55,33 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPullRefreshState(
|
||||
refreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
|
||||
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
|
||||
refreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
|
||||
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
|
||||
): PullRefreshState {
|
||||
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
|
||||
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val onRefreshState = rememberUpdatedState(onRefresh)
|
||||
val thresholdPx: Float
|
||||
val refreshingOffsetPx: Float
|
||||
val scope = rememberCoroutineScope()
|
||||
val onRefreshState = rememberUpdatedState(onRefresh)
|
||||
val thresholdPx: Float
|
||||
val refreshingOffsetPx: Float
|
||||
|
||||
with(LocalDensity.current) {
|
||||
thresholdPx = refreshThreshold.toPx()
|
||||
refreshingOffsetPx = refreshingOffset.toPx()
|
||||
}
|
||||
with(LocalDensity.current) {
|
||||
thresholdPx = refreshThreshold.toPx()
|
||||
refreshingOffsetPx = refreshingOffset.toPx()
|
||||
}
|
||||
|
||||
val state =
|
||||
remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) }
|
||||
val state =
|
||||
remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) }
|
||||
|
||||
SideEffect {
|
||||
state.setRefreshing(refreshing)
|
||||
state.setThreshold(thresholdPx)
|
||||
state.setRefreshingOffset(refreshingOffsetPx)
|
||||
}
|
||||
SideEffect {
|
||||
state.setRefreshing(refreshing)
|
||||
state.setThreshold(thresholdPx)
|
||||
state.setRefreshingOffset(refreshingOffsetPx)
|
||||
}
|
||||
|
||||
return state
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,131 +98,131 @@ fun rememberPullRefreshState(
|
||||
* Should be created using [rememberPullRefreshState].
|
||||
*/
|
||||
class PullRefreshState
|
||||
internal constructor(
|
||||
private val animationScope: CoroutineScope,
|
||||
private val onRefreshState: State<() -> Unit>,
|
||||
refreshingOffset: Float,
|
||||
threshold: Float,
|
||||
) {
|
||||
/**
|
||||
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
|
||||
*
|
||||
* If the component has not been pulled at all, progress is zero. If the pull has reached halfway
|
||||
* to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has gone beyond
|
||||
* the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to two times the
|
||||
* refreshThreshold.
|
||||
*/
|
||||
val progress
|
||||
get() = adjustedDistancePulled / threshold
|
||||
internal constructor(
|
||||
private val animationScope: CoroutineScope,
|
||||
private val onRefreshState: State<() -> Unit>,
|
||||
refreshingOffset: Float,
|
||||
threshold: Float,
|
||||
) {
|
||||
/**
|
||||
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
|
||||
*
|
||||
* If the component has not been pulled at all, progress is zero. If the pull has reached halfway
|
||||
* to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has gone beyond
|
||||
* the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to two times the
|
||||
* refreshThreshold.
|
||||
*/
|
||||
val progress
|
||||
get() = adjustedDistancePulled / threshold
|
||||
|
||||
val refreshing
|
||||
get() = _refreshing
|
||||
val refreshing
|
||||
get() = _refreshing
|
||||
|
||||
val position
|
||||
get() = _position
|
||||
val position
|
||||
get() = _position
|
||||
|
||||
val threshold
|
||||
get() = _threshold
|
||||
val threshold
|
||||
get() = _threshold
|
||||
|
||||
private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER }
|
||||
private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER }
|
||||
|
||||
private var _refreshing by mutableStateOf(false)
|
||||
private var _position by mutableStateOf(0f)
|
||||
private var distancePulled by mutableStateOf(0f)
|
||||
private var _threshold by mutableStateOf(threshold)
|
||||
private var refreshingOffsetState by mutableStateOf(refreshingOffset)
|
||||
private var _refreshing by mutableStateOf(false)
|
||||
private var _position by mutableStateOf(0f)
|
||||
private var distancePulled by mutableStateOf(0f)
|
||||
private var _threshold by mutableStateOf(threshold)
|
||||
private var refreshingOffsetState by mutableStateOf(refreshingOffset)
|
||||
|
||||
internal fun onPull(pullDelta: Float): Float {
|
||||
if (_refreshing) return 0f // Already refreshing, do nothing.
|
||||
internal fun onPull(pullDelta: Float): Float {
|
||||
if (_refreshing) return 0f // Already refreshing, do nothing.
|
||||
|
||||
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
|
||||
val dragConsumed = newOffset - distancePulled
|
||||
distancePulled = newOffset
|
||||
_position = calculateIndicatorPosition()
|
||||
return dragConsumed
|
||||
}
|
||||
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
|
||||
val dragConsumed = newOffset - distancePulled
|
||||
distancePulled = newOffset
|
||||
_position = calculateIndicatorPosition()
|
||||
return dragConsumed
|
||||
}
|
||||
|
||||
internal fun onRelease(velocity: Float): Float {
|
||||
if (refreshing) return 0f // Already refreshing, do nothing
|
||||
internal fun onRelease(velocity: Float): Float {
|
||||
if (refreshing) return 0f // Already refreshing, do nothing
|
||||
|
||||
if (adjustedDistancePulled > threshold) {
|
||||
onRefreshState.value()
|
||||
if (adjustedDistancePulled > threshold) {
|
||||
onRefreshState.value()
|
||||
}
|
||||
animateIndicatorTo(0f)
|
||||
val consumed =
|
||||
when {
|
||||
// We are flinging without having dragged the pull refresh (for example a fling inside
|
||||
// a list) - don't consume
|
||||
distancePulled == 0f -> 0f
|
||||
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
|
||||
// the list from scrolling
|
||||
velocity < 0f -> 0f
|
||||
// We are showing the indicator, and the fling is downwards - consume everything
|
||||
else -> velocity
|
||||
}
|
||||
distancePulled = 0f
|
||||
return consumed
|
||||
}
|
||||
|
||||
internal fun setRefreshing(refreshing: Boolean) {
|
||||
if (_refreshing != refreshing) {
|
||||
_refreshing = refreshing
|
||||
distancePulled = 0f
|
||||
animateIndicatorTo(if (refreshing) refreshingOffsetState else 0f)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setThreshold(threshold: Float) {
|
||||
_threshold = threshold
|
||||
}
|
||||
|
||||
internal fun setRefreshingOffset(refreshingOffset: Float) {
|
||||
if (refreshingOffsetState != refreshingOffset) {
|
||||
refreshingOffsetState = refreshingOffset
|
||||
if (refreshing) animateIndicatorTo(refreshingOffset)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
|
||||
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
|
||||
// overhead of running through the animation pipeline instead of directly mutating the state.
|
||||
private val mutatorMutex = MutatorMutex()
|
||||
|
||||
private fun animateIndicatorTo(offset: Float) =
|
||||
animationScope.launch {
|
||||
mutatorMutex.mutate {
|
||||
animate(initialValue = _position, targetValue = offset) { value, _ -> _position = value }
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateIndicatorPosition(): Float =
|
||||
when {
|
||||
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
|
||||
adjustedDistancePulled <= threshold -> adjustedDistancePulled
|
||||
else -> {
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
// The additional offset beyond the threshold.
|
||||
val extraOffset = threshold * tensionPercent
|
||||
threshold + extraOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
animateIndicatorTo(0f)
|
||||
val consumed =
|
||||
when {
|
||||
// We are flinging without having dragged the pull refresh (for example a fling inside
|
||||
// a list) - don't consume
|
||||
distancePulled == 0f -> 0f
|
||||
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
|
||||
// the list from scrolling
|
||||
velocity < 0f -> 0f
|
||||
// We are showing the indicator, and the fling is downwards - consume everything
|
||||
else -> velocity
|
||||
}
|
||||
distancePulled = 0f
|
||||
return consumed
|
||||
}
|
||||
|
||||
internal fun setRefreshing(refreshing: Boolean) {
|
||||
if (_refreshing != refreshing) {
|
||||
_refreshing = refreshing
|
||||
distancePulled = 0f
|
||||
animateIndicatorTo(if (refreshing) refreshingOffsetState else 0f)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setThreshold(threshold: Float) {
|
||||
_threshold = threshold
|
||||
}
|
||||
|
||||
internal fun setRefreshingOffset(refreshingOffset: Float) {
|
||||
if (refreshingOffsetState != refreshingOffset) {
|
||||
refreshingOffsetState = refreshingOffset
|
||||
if (refreshing) animateIndicatorTo(refreshingOffset)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
|
||||
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
|
||||
// overhead of running through the animation pipeline instead of directly mutating the state.
|
||||
private val mutatorMutex = MutatorMutex()
|
||||
|
||||
private fun animateIndicatorTo(offset: Float) =
|
||||
animationScope.launch {
|
||||
mutatorMutex.mutate {
|
||||
animate(initialValue = _position, targetValue = offset) { value, _ -> _position = value }
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateIndicatorPosition(): Float =
|
||||
when {
|
||||
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
|
||||
adjustedDistancePulled <= threshold -> adjustedDistancePulled
|
||||
else -> {
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
// The additional offset beyond the threshold.
|
||||
val extraOffset = threshold * tensionPercent
|
||||
threshold + extraOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Default parameter values for [rememberPullRefreshState]. */
|
||||
object PullRefreshDefaults {
|
||||
/**
|
||||
* If the indicator is below this threshold offset when it is released, a refresh will be
|
||||
* triggered.
|
||||
*/
|
||||
val RefreshThreshold = 80.dp
|
||||
/**
|
||||
* If the indicator is below this threshold offset when it is released, a refresh will be
|
||||
* triggered.
|
||||
*/
|
||||
val RefreshThreshold = 80.dp
|
||||
|
||||
/** The offset at which the indicator should be rendered whilst a refresh is occurring. */
|
||||
val RefreshingOffset = 56.dp
|
||||
/** The offset at which the indicator should be rendered whilst a refresh is occurring. */
|
||||
val RefreshingOffset = 56.dp
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user