Skip to content

Commit

Permalink
Merge pull request #350 from google/cb/swipe-refresh-offset
Browse files Browse the repository at this point in the history
[SwipeRefresh] Make indicator more flexible
  • Loading branch information
chrisbanes authored Apr 20, 2021
2 parents ae26aeb + ffced72 commit 117a9fe
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 229 deletions.
5 changes: 3 additions & 2 deletions docs/swiperefresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ To customize the default indicator, we can provide our own `indicator` content b
SwipeRefresh(
state = /* ... */,
onRefresh = /* ... */,
indicator = { state ->
indicator = { state, trigger ->
SwipeRefreshIndicator(
// Pass the SwipeRefreshState through
// Pass the SwipeRefreshState + trigger through
state = state,
refreshTriggerDistance = trigger,
// Enable the scale animation
scale = true,
// Change the color and shape
Expand Down
9 changes: 9 additions & 0 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@
</intent-filter>
</activity>

<activity
android:name=".swiperefresh.SwipeRefreshContentPaddingSample"
android:label="@string/swiperefresh_title_content_padding">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.google.accompanist.sample.SAMPLE_CODE" />
</intent-filter>
</activity>

<activity
android:name=".swiperefresh.SwipeRefreshTweakedIndicatorSample"
android:label="@string/swiperefresh_title_tweaked">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.accompanist.sample.swiperefresh

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.toPaddingValues
import com.google.accompanist.sample.AccompanistSampleTheme
import com.google.accompanist.sample.R
import com.google.accompanist.sample.insets.InsetAwareTopAppBar
import com.google.accompanist.sample.insets.ListItem
import com.google.accompanist.sample.randomSampleImageUrl
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.SwipeRefreshIndicator
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay

class SwipeRefreshContentPaddingSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
AccompanistSampleTheme {
Sample()
}
}
}
}

private val listItems = List(40) { randomSampleImageUrl(it) }

@Composable
private fun Sample() {
val systemUiController = rememberSystemUiController()
val useDarkIcons = MaterialTheme.colors.isLight
SideEffect {
systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = useDarkIcons)
}

Surface {
Box(Modifier.fillMaxSize()) {
// A state instance which allows us to track the size of the top app bar
var topAppBarSize by remember { mutableStateOf(0) }

// Simulate a fake 2-second 'load'. Ideally this 'refreshing' value would
// come from a ViewModel or similar
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
refreshing = false
}
}

val contentPadding = LocalWindowInsets.current.systemBars.toPaddingValues(
top = false,
additionalTop = with(LocalDensity.current) { topAppBarSize.toDp() }
)

SwipeRefresh(
state = rememberSwipeRefreshState(refreshing),
onRefresh = { refreshing = true },
// Shift the indicator to match the list content padding
indicatorPadding = contentPadding,
// Tweak the indicator to scale up/down
indicator = { state, refreshTriggerDistance ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = refreshTriggerDistance,
scale = true
)
}
) {
LazyColumn(contentPadding = contentPadding) {
items(items = listItems) { imageUrl ->
ListItem(imageUrl, Modifier.fillMaxWidth())
}
}
}

/**
* We show a translucent app bar above which floats about the content. Our
* [InsetAwareTopAppBar] below automatically draws behind the status bar too.
*/
InsetAwareTopAppBar(
title = { Text(stringResource(R.string.swiperefresh_title_content_padding)) },
backgroundColor = MaterialTheme.colors.surface.copy(alpha = 0.9f),
modifier = Modifier
.fillMaxWidth()
// We use onSizeChanged to track the app bar height, and update
// our state above
.onSizeChanged { topAppBarSize = it.height }
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,23 @@ package com.google.accompanist.sample.swiperefresh
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand All @@ -49,9 +44,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.sample.AccompanistSampleTheme
import com.google.accompanist.sample.R
Expand Down Expand Up @@ -96,8 +95,11 @@ private fun Sample() {
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = { refreshing = true },
indicator = { state ->
CustomIndicator(swipeRefreshState = state)
indicator = { state, trigger ->
GlowIndicator(
swipeRefreshState = state,
refreshTriggerDistance = trigger
)
},
) {
LazyColumn {
Expand Down Expand Up @@ -125,46 +127,44 @@ private fun Sample() {
}
}

/**
* A custom indicator which displays a glow and progress indicator
*/
@Composable
fun CustomIndicator(swipeRefreshState: SwipeRefreshState) {
// Animates the indicator when refreshing, by rotating it
val rotation = remember { mutableStateOf(0f) }
LaunchedEffect(swipeRefreshState.isRefreshing) {
fun GlowIndicator(
swipeRefreshState: SwipeRefreshState,
refreshTriggerDistance: Dp,
color: Color = MaterialTheme.colors.primary,
) {
Box(
Modifier
.drawWithCache {
onDrawBehind {
val distance = refreshTriggerDistance.toPx()
val progress = (swipeRefreshState.indicatorOffset / distance).coerceIn(0f, 1f)
// We draw a translucent glow
val brush = Brush.verticalGradient(
0f to color.copy(alpha = 0.45f),
1f to color.copy(alpha = 0f)
)
// And fade the glow in/out based on the swipe progress
drawRect(brush = brush, alpha = FastOutSlowInEasing.transform(progress))
}
}
.fillMaxWidth()
.height(72.dp)
) {
if (swipeRefreshState.isRefreshing) {
animate(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
),
block = { value, _ -> rotation.value = value }
)
// If we're refreshing, show an indeterminate progress indicator
LinearProgressIndicator(Modifier.fillMaxWidth())
} else {
rotation.value = 0f
// Otherwise we display a determinate progress indicator with the current swipe progress
val trigger = with(LocalDensity.current) { refreshTriggerDistance.toPx() }
val progress = (swipeRefreshState.indicatorOffset / trigger).coerceIn(0f, 1f)
LinearProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxWidth(),
)
}
}

Surface(
border = BorderStroke(1.dp, MaterialTheme.colors.primary),
elevation = 4.dp,
shape = CircleShape,
modifier = Modifier
.size(48.dp)
.graphicsLayer {
// We rotate the indicator in the X axis as the user swipes
val circumference = 2 * Math.PI.toFloat() * (48.dp.toPx() / 2)
rotationX = (swipeRefreshState.indicatorOffset / circumference) * -360f

// Apply our animated 'refreshing' rotation
rotationZ = rotation.value
}
) {
Image(
imageVector = Icons.Default.Refresh,
contentDescription = "Refresh",
modifier = Modifier
.wrapContentSize()
.size(24.dp)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ private fun Sample() {
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = { refreshing = true },
indicator = {
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = it,
state = state,
refreshTriggerDistance = trigger,
scale = true,
arrowEnabled = false,
backgroundColor = MaterialTheme.colors.primary,
Expand Down
1 change: 1 addition & 0 deletions sample/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<string name="flowlayout_title_row">Flow Layout: Row</string>

<string name="swiperefresh_title_basics">Swipe Refresh: Basic</string>
<string name="swiperefresh_title_content_padding">Swipe Refresh: Edge-to-edge</string>
<string name="swiperefresh_title_tweaked">Swipe Refresh: Tweaked indicator</string>
<string name="swiperefresh_title_custom">Swipe Refresh: Custom indicator</string>
<string name="swiperefresh_title_verticalpager">Swipe Refresh: VerticalPager</string>
Expand Down
18 changes: 4 additions & 14 deletions swiperefresh/api/swiperefresh.api
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
public final class com/google/accompanist/swiperefresh/ComposableSingletons$SwipeRefreshIndicatorKt {
public static final field INSTANCE Lcom/google/accompanist/swiperefresh/ComposableSingletons$SwipeRefreshIndicatorKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$swiperefresh_release ()Lkotlin/jvm/functions/Function2;
}

public final class com/google/accompanist/swiperefresh/ComposableSingletons$SwipeRefreshKt {
public static final field INSTANCE Lcom/google/accompanist/swiperefresh/ComposableSingletons$SwipeRefreshKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-1 Lkotlin/jvm/functions/Function4;
public fun <init> ()V
public final fun getLambda-1$swiperefresh_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-1$swiperefresh_release ()Lkotlin/jvm/functions/Function4;
}

public final class com/google/accompanist/swiperefresh/SwipeRefreshIndicatorKt {
public static final fun PreviewSwipeRefreshIndicator (Landroidx/compose/runtime/Composer;I)V
public static final fun SwipeRefreshIndicator-7eC1SuE (Lcom/google/accompanist/swiperefresh/SwipeRefreshState;Landroidx/compose/ui/Modifier;ZZJJLandroidx/compose/ui/graphics/Shape;ZFLandroidx/compose/runtime/Composer;II)V
public static final fun SwipeRefreshIndicator-YLMMpjw (ZFFLandroidx/compose/ui/Modifier;ZZJJLandroidx/compose/ui/graphics/Shape;ZFLandroidx/compose/runtime/Composer;III)V
public static final fun SwipeRefreshIndicator-fRQ31k0 (Lcom/google/accompanist/swiperefresh/SwipeRefreshState;FLandroidx/compose/ui/Modifier;ZZJJLandroidx/compose/ui/graphics/Shape;FZFLandroidx/compose/runtime/Composer;III)V
}

public final class com/google/accompanist/swiperefresh/SwipeRefreshKt {
public static final fun SwipeRefresh (Lcom/google/accompanist/swiperefresh/SwipeRefreshState;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun SwipeRefresh-gNPyAyM (Lcom/google/accompanist/swiperefresh/SwipeRefreshState;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZFLandroidx/compose/ui/Alignment;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun rememberSwipeRefreshState (ZLandroidx/compose/runtime/Composer;I)Lcom/google/accompanist/swiperefresh/SwipeRefreshState;
}

public final class com/google/accompanist/swiperefresh/SwipeRefreshState {
public fun <init> (Z)V
public final fun getIndicatorOffset ()F
public final fun getIndicatorRefreshOffset ()F
public final fun isRefreshing ()Z
public final fun isSwipeInProgress ()Z
public final fun setRefreshing (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.swipeDown
import com.google.common.truth.Truth.assertThat
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test

Expand All @@ -43,6 +44,7 @@ class SwipeRefreshTest {
val rule = createComposeRule()

@Test
@Ignore("https://issuetracker.google.com/issues/185814751")
fun swipeRefreshes() {
val state = SwipeRefreshState(false)
var refreshCallCount = 0
Expand Down Expand Up @@ -118,8 +120,8 @@ private fun SwipeRefreshTestContent(
state = state,
onRefresh = onRefresh,
modifier = Modifier.testTag(SwipeRefreshTag),
indicator = { state ->
SwipeRefreshIndicator(state, Modifier.testTag(SwipeRefreshIndicatorTag))
indicator = { state, trigger ->
SwipeRefreshIndicator(state, trigger, Modifier.testTag(SwipeRefreshIndicatorTag))
}
) {
LazyColumn(Modifier.fillMaxSize()) {
Expand Down
Loading

0 comments on commit 117a9fe

Please sign in to comment.