app: vendor PullToRefresh for Compose beta03 ABI compatibility

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-03-29 12:16:11 +05:30
parent 5c7f38c2dc
commit 48f45beeeb
3 changed files with 343 additions and 1 deletions

View file

@ -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"))

View 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
}

View file

@ -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
)
})
}
}