mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 23:47:02 +05:30
app: vendor PullToRefresh for Compose beta03 ABI compatibility
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
5c7f38c2dc
commit
48f45beeeb
3 changed files with 343 additions and 1 deletions
|
@ -45,7 +45,6 @@ dependencies {
|
||||||
implementation(Dependencies.ThirdParty.accompanistCoil)
|
implementation(Dependencies.ThirdParty.accompanistCoil)
|
||||||
implementation(Dependencies.ThirdParty.accompanistFlow)
|
implementation(Dependencies.ThirdParty.accompanistFlow)
|
||||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||||
implementation(Dependencies.ThirdParty.pullToRefresh)
|
|
||||||
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
||||||
implementation(Dependencies.ThirdParty.SQLDelight.androidDriver)
|
implementation(Dependencies.ThirdParty.SQLDelight.androidDriver)
|
||||||
testImplementation(kotlin("test-junit"))
|
testImplementation(kotlin("test-junit"))
|
||||||
|
|
214
app/src/main/java/com/puculek/pulltorefresh/PullToRefresh.kt
Normal file
214
app/src/main/java/com/puculek/pulltorefresh/PullToRefresh.kt
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
package com.puculek.pulltorefresh
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.absoluteOffset
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.CombinedModifier
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
||||||
|
private const val MAX_OFFSET = 400f
|
||||||
|
private const val MIN_REFRESH_OFFSET = 250f
|
||||||
|
private const val PERCENT_INDICATOR_PROGRESS_ON_DRAG = 0.85f
|
||||||
|
private const val BASE_OFFSET = -48
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A layout composable with [content].
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* @sample com.puculek.pulltorefresh.samples.PullToRefreshWithColumn
|
||||||
|
* @sample com.puculek.pulltorefresh.samples.PullToRefreshWithLazyColumn
|
||||||
|
*
|
||||||
|
* @param modifier The modifier to be applied to the layout.
|
||||||
|
* @param maxOffset How many pixels can the progress indicator can be dragged down
|
||||||
|
* @param minRefreshOffset Minimum drag value to trigger [onRefresh]
|
||||||
|
* @param isRefreshing Flag describing if [PullToRefresh] is refreshing.
|
||||||
|
* @param progressColor Color of progress drawable.
|
||||||
|
* @param backgroundColor Background color of progress indicator.
|
||||||
|
* @param onRefresh Callback to be called if layout is pulled to refresh.
|
||||||
|
* @param content The content of the [PullToRefresh].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PullToRefresh(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
maxOffset: Float = MAX_OFFSET,
|
||||||
|
minRefreshOffset: Float = MIN_REFRESH_OFFSET,
|
||||||
|
isRefreshing: Boolean,
|
||||||
|
progressColor: Color = MaterialTheme.colors.primary,
|
||||||
|
backgroundColor: Color = MaterialTheme.colors.surface,
|
||||||
|
onRefresh: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
var indicatorOffset by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
// check if isRefreshing has been changed
|
||||||
|
var isRefreshingInternal by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// User cancelled dragging before reaching MAX_REFRESH_OFFSET
|
||||||
|
var isResettingScroll by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// How much should indicator scroll to reset its position
|
||||||
|
var scrollToReset by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
// Trigger for scaling animation
|
||||||
|
var isFinishingRefresh by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scaleAnimation by animateFloatAsState(
|
||||||
|
targetValue = if (isFinishingRefresh) 0f else 1f,
|
||||||
|
finishedListener = {
|
||||||
|
indicatorOffset = 0f
|
||||||
|
isFinishingRefresh = false
|
||||||
|
})
|
||||||
|
val offsetAnimation by animateFloatAsState(
|
||||||
|
targetValue = if (isRefreshing || isFinishingRefresh) {
|
||||||
|
indicatorOffset - minRefreshOffset
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val resettingScrollOffsetAnimation by animateFloatAsState(
|
||||||
|
targetValue = if (isResettingScroll) {
|
||||||
|
scrollToReset
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
},
|
||||||
|
finishedListener = {
|
||||||
|
if (isResettingScroll) {
|
||||||
|
indicatorOffset = 0f
|
||||||
|
isResettingScroll = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isResettingScroll) {
|
||||||
|
indicatorOffset -= resettingScrollOffsetAnimation
|
||||||
|
}
|
||||||
|
if (!isRefreshing && isRefreshingInternal) {
|
||||||
|
isFinishingRefresh = true
|
||||||
|
isRefreshingInternal = false
|
||||||
|
}
|
||||||
|
if (isRefreshing && !isRefreshingInternal) {
|
||||||
|
isRefreshingInternal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val nestedScrollConnection = object : NestedScrollConnection {
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (!isRefreshing && source == NestedScrollSource.Drag) {
|
||||||
|
val diff = if (indicatorOffset + available.y > maxOffset) {
|
||||||
|
available.y - (indicatorOffset + available.y - maxOffset)
|
||||||
|
} else if (indicatorOffset + available.y < 0) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
available.y
|
||||||
|
}
|
||||||
|
indicatorOffset += diff
|
||||||
|
return Offset(0f, diff)
|
||||||
|
}
|
||||||
|
return super.onPostScroll(consumed, available, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (!isRefreshing && source == NestedScrollSource.Drag) {
|
||||||
|
if (available.y < 0 && indicatorOffset > 0) {
|
||||||
|
val diff = if (indicatorOffset + available.y < 0) {
|
||||||
|
indicatorOffset = 0f
|
||||||
|
indicatorOffset
|
||||||
|
} else {
|
||||||
|
indicatorOffset += available.y
|
||||||
|
available.y
|
||||||
|
}
|
||||||
|
isFinishingRefresh = false
|
||||||
|
return Offset.Zero.copy(y = diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onPreScroll(available, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(
|
||||||
|
consumed: Velocity,
|
||||||
|
available: Velocity
|
||||||
|
): Velocity {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
if (indicatorOffset > minRefreshOffset) {
|
||||||
|
onRefresh()
|
||||||
|
isRefreshingInternal = true
|
||||||
|
} else {
|
||||||
|
isResettingScroll = true
|
||||||
|
scrollToReset = indicatorOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onPostFling(consumed, available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = CombinedModifier(
|
||||||
|
inner = Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
.clip(RectangleShape),
|
||||||
|
outer = modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
|
||||||
|
val offsetPx = if (isRefreshing || isFinishingRefresh) {
|
||||||
|
offsetAnimation
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
val absoluteOffset = BASE_OFFSET.dp + with(LocalDensity.current) {
|
||||||
|
val diffedOffset = indicatorOffset - offsetPx
|
||||||
|
val calculated = calculateAbsoluteOffset(diffedOffset, MAX_OFFSET)
|
||||||
|
calculated.toDp()
|
||||||
|
}
|
||||||
|
val progressFromOffset = with(LocalDensity.current) {
|
||||||
|
val coeff = MAX_OFFSET / (MAX_OFFSET - BASE_OFFSET)
|
||||||
|
(indicatorOffset - BASE_OFFSET) / maxOffset * coeff
|
||||||
|
}
|
||||||
|
PullToRefreshProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.absoluteOffset(y = absoluteOffset)
|
||||||
|
.scale(scaleAnimation)
|
||||||
|
.rotate(indicatorOffset / MAX_OFFSET * 180 + 110),
|
||||||
|
progressColor = progressColor,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
progress = when {
|
||||||
|
!isRefreshing && !isFinishingRefresh -> progressFromOffset * PERCENT_INDICATOR_PROGRESS_ON_DRAG
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateAbsoluteOffset(absoluteOffset: Float, maxOffset: Float): Float {
|
||||||
|
// TODO: Match best function to imitate physics
|
||||||
|
return absoluteOffset
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.puculek.pulltorefresh
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.ProgressIndicatorDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
||||||
|
private val CircularIndicatorDiameter = 40.dp
|
||||||
|
private const val strokeWidthPx = 2.5f
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PullToRefreshProgressIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
progressColor: Color,
|
||||||
|
backgroundColor: Color,
|
||||||
|
progress: Float? = null
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(CircularIndicatorDiameter)
|
||||||
|
.height(CircularIndicatorDiameter),
|
||||||
|
shape = CircleShape,
|
||||||
|
elevation = 6.dp,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
) {
|
||||||
|
val padding = Modifier.padding(8.dp)
|
||||||
|
val strokeWidth = with(LocalDensity.current) {
|
||||||
|
(strokeWidthPx * this.density).toDp()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress == null) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = padding,
|
||||||
|
color = progressColor,
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ProgressIndicatorWithArrow(
|
||||||
|
modifier = padding,
|
||||||
|
progress = progress,
|
||||||
|
color = progressColor,
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProgressIndicatorWithArrow(
|
||||||
|
progress: Float,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colors.primary,
|
||||||
|
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val strokeWidthPx = with(LocalDensity.current) {
|
||||||
|
strokeWidth.toPx()
|
||||||
|
}
|
||||||
|
val arrowWidth = 2.5f * strokeWidthPx * (0.5f + progress * 0.5f)
|
||||||
|
val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Butt)
|
||||||
|
val diameterOffset = stroke.width / 2
|
||||||
|
|
||||||
|
val arrowPath = Path().apply {
|
||||||
|
moveTo(0f, -arrowWidth)
|
||||||
|
lineTo(arrowWidth, 0f)
|
||||||
|
lineTo(0f, arrowWidth)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Canvas(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(), onDraw = {
|
||||||
|
val arcDimen = size.width - 2 * diameterOffset
|
||||||
|
withTransform({
|
||||||
|
translate(top = strokeWidthPx / 2, left = size.width / 2)
|
||||||
|
rotate(
|
||||||
|
degrees = progress * 360,
|
||||||
|
pivot = Offset(x = 0f, y = size.height / 2 - diameterOffset)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
drawPath(
|
||||||
|
path = arrowPath,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawArc(
|
||||||
|
color = color,
|
||||||
|
startAngle = -90f,
|
||||||
|
sweepAngle = 360 * progress,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = Offset(diameterOffset, diameterOffset),
|
||||||
|
size = Size(arcDimen, arcDimen),
|
||||||
|
style = stroke
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue