diff --git a/README.md b/README.md index 0e348b3..833a734 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ ```gradle // 极致体验的Compose刷新组件 (*必须) - implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh:1.2.0' + implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh:1.3.0' // 经典样式的指示器 (可选) - implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-classic:1.2.0' + implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-classic:1.3.0' // Lottie动画指示器 (可选) - implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-lottie:1.2.0' + implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-lottie:1.3.0' // 进度条样式的指示器 (可选) - implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-progress:1.2.0' + implementation 'com.github.jenly1314.UltraSwipeRefresh:refresh-indicator-progress:1.3.0' ``` ## 使用 @@ -74,6 +74,7 @@ * @param dragMultiplier 触发下拉刷新或上拉加载时的阻力系数;值越小,阻力越大;默认为:0.5 * @param finishDelayMillis 完成时延时时间;让完成时的中间状态[UltraSwipeRefreshState.isFinishing]停留一会儿,定格的展示提示内容;默认:500毫秒 * @param vibrateEnabled 是否启用振动,如果启用则当滑动偏移量满足触发刷新或触发加载更多时,会有振动效果;默认为:false + * @param alwaysScrollable 是否始终可以滚动;当为true时,则会忽略刷新中或加载中的状态限制,始终可以进行滚动;默认为:false * @param headerIndicator 下拉刷新时顶部显示的Header指示器 * @param footerIndicator 上拉加载更多时底部显示的Footer指示器 * @param contentContainer 内容的父容器,便于统一管理 @@ -91,34 +92,28 @@ fun UltraSwipeRefreshSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 - delay(2000) - itemCount = 20 - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 - delay(2000) - itemCount += 20 - state.isLoading = false - } - } - + val coroutineScope = rememberCoroutineScope() + UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + state.isRefreshing = false + } }, onLoadMore = { - state.isLoading = true + coroutineScope.launch { + state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount += 20 + state.isLoading = false + } }, modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerScrollMode = NestedScrollMode.Translate, @@ -200,8 +195,10 @@ UltraSwipeRefreshTheme.config = UltraSwipeRefreshTheme.config.copy( ## 版本记录 -#### 待发布版本([提前体验](test.md)) +#### v1.3.0 :2024-7-20 * 更新compose至v1.6.0 (v1.5.0 -> v1.6.0) ([#13](https://github.com/jenly1314/UltraSwipeRefresh/issues/13)) +* 新增参数`alwaysScrollable`:是否始终可以滚动 +* 优化一些细节 #### v1.2.0 :2024-7-1 * 新增参数`contentContainer`:内容的父容器,便于统一管理 diff --git a/app/build.gradle b/app/build.gradle index da66b14..610ca48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,7 +37,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.3' + kotlinCompilerExtensionVersion versions.compose_compiler } lint { abortOnError false @@ -61,7 +61,6 @@ dependencies { implementation "androidx.compose.material:material" implementation "androidx.compose.material3:material3:$versions.material3" implementation "androidx.navigation:navigation-compose:$versions.navigation_compose" - implementation "androidx.navigation:navigation-compose:$versions.navigation_compose" implementation "com.google.accompanist:accompanist-swiperefresh:$versions.accompanist" implementation "com.airbnb.android:lottie-compose:$versions.lottie_compose" diff --git a/app/release/app-release.apk b/app/release/app-release.apk index c54cc50..2878bcf 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 9bafc94..8e11a97 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 6, - "versionName": "1.2.0", + "versionCode": 7, + "versionName": "1.3.0", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/MainActivity.kt b/app/src/main/java/com/king/ultraswiperefresh/app/MainActivity.kt index 8ef6ec8..143a7a6 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/MainActivity.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/MainActivity.kt @@ -5,8 +5,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/component/ColumnItem.kt b/app/src/main/java/com/king/ultraswiperefresh/app/component/ColumnItem.kt index 51f41d6..638ddcb 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/component/ColumnItem.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/component/ColumnItem.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/ClassicRefreshIndicatorSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/ClassicRefreshIndicatorSample.kt index c6edfee..2696aa7 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/ClassicRefreshIndicatorSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/ClassicRefreshIndicatorSample.kt @@ -1,17 +1,21 @@ package com.king.ultraswiperefresh.app.sample import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -22,6 +26,7 @@ import com.king.ultraswiperefresh.indicator.classic.ClassicRefreshFooter import com.king.ultraswiperefresh.indicator.classic.ClassicRefreshHeader import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * 经典的刷新样式示例 @@ -34,30 +39,12 @@ import kotlinx.coroutines.delay fun ClassicRefreshIndicatorSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - var hasMoreData by remember { mutableStateOf(true) } - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - itemCount = 20 - hasMoreData = true - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - itemCount += 20 - state.isLoading = false - } - } + val coroutineScope = rememberCoroutineScope() LaunchedEffect(state.isFinishing) { - if (itemCount > 50 && !state.isFinishing) { + if (itemCount >= 60 && !state.isFinishing) { hasMoreData = false } } @@ -65,28 +52,31 @@ fun ClassicRefreshIndicatorSample() { UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + hasMoreData = true + state.isRefreshing = false + } }, onLoadMore = { - if (hasMoreData) { + coroutineScope.launch { state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount += 20 + state.isLoading = false } }, modifier = Modifier.background(color = Color(0x7FEEEEEE)), + loadMoreEnabled = hasMoreData, headerIndicator = { ClassicRefreshHeader(it) }, footerIndicator = { - if (hasMoreData) { - ClassicRefreshFooter(it) - } else { - Text( - text = "———— 我也是有底线的 ————", - color = Color(0xFF999999), - fontSize = 15.sp, - modifier = Modifier.padding(vertical = 16.dp) - ) - } + ClassicRefreshFooter(it) } ) { LazyColumn(Modifier.background(color = Color.White)) { @@ -95,12 +85,25 @@ fun ClassicRefreshIndicatorSample() { val title = "UltraSwipeRefresh列表标题${it + 1}" val content = "UltraSwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) } } + + if (!hasMoreData) { + item { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + text = "没有更多数据了", + color = Color(0xFF999999), + fontSize = 15.sp, + modifier = Modifier.padding(vertical = 16.dp) + ) + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/CustomLottieRefreshIndicatorSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/CustomLottieRefreshIndicatorSample.kt index 682c9c1..91401c0 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/CustomLottieRefreshIndicatorSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/CustomLottieRefreshIndicatorSample.kt @@ -5,14 +5,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,6 +31,7 @@ import com.king.ultraswiperefresh.indicator.lottie.LottieRefreshFooter import com.king.ultraswiperefresh.indicator.lottie.LottieRefreshHeader import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * 自定义Lottie动画刷新样式示例 @@ -42,55 +44,50 @@ import kotlinx.coroutines.delay fun CustomLottieRefreshIndicatorSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - var hasMoreData by remember { mutableStateOf(true) } + val coroutineScope = rememberCoroutineScope() - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - itemCount = 20 - hasMoreData = true - state.isRefreshing = false - } + var headerScrollMode by remember { + mutableStateOf(NestedScrollMode.FixedBehind) } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - itemCount += 20 - state.isLoading = false - } + var footerScrollMode by remember { + mutableStateOf(NestedScrollMode.FixedBehind) } LaunchedEffect(state.isFinishing) { - if (itemCount > 50 && !state.isFinishing) { + if (itemCount >= 60 && !state.isFinishing) { hasMoreData = false } } - var headerScrollMode by remember { - mutableStateOf(NestedScrollMode.FixedBehind) - } - var footerScrollMode by remember { - mutableStateOf(NestedScrollMode.FixedBehind) - } - val context = LocalContext.current UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + hasMoreData = true + state.isRefreshing = false + } }, onLoadMore = { - state.isLoading = true + coroutineScope.launch { + state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount += 20 + state.isLoading = false + } }, + modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerScrollMode = headerScrollMode, footerScrollMode = footerScrollMode, loadMoreEnabled = hasMoreData, - modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerIndicator = { LottieRefreshHeader( state = it, @@ -117,14 +114,17 @@ fun CustomLottieRefreshIndicatorSample() { footerScrollMode = nestedScrollModes.random() context.showToast("滑动模式已随机") } - Divider(modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6)) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color(0xFFF2F3F6) + ) } repeat(itemCount) { item { val title = "UltraSwipeRefresh列表标题${it + 1}" val content = "UltraSwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/LottieRefreshIndicatorSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/LottieRefreshIndicatorSample.kt index 44793d5..b9e62fd 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/LottieRefreshIndicatorSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/LottieRefreshIndicatorSample.kt @@ -5,14 +5,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,10 +23,12 @@ import androidx.compose.ui.unit.sp import com.king.ultraswiperefresh.NestedScrollMode import com.king.ultraswiperefresh.UltraSwipeRefresh import com.king.ultraswiperefresh.app.component.ColumnItem +import com.king.ultraswiperefresh.indicator.classic.ClassicRefreshFooter import com.king.ultraswiperefresh.indicator.lottie.LottieRefreshFooter import com.king.ultraswiperefresh.indicator.lottie.LottieRefreshHeader import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * Lottie动画刷新样式示例 @@ -38,30 +41,12 @@ import kotlinx.coroutines.delay fun LottieRefreshIndicatorSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - var hasMoreData by remember { mutableStateOf(true) } - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - itemCount = 20 - hasMoreData = true - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - itemCount += 20 - state.isLoading = false - } - } + val coroutineScope = rememberCoroutineScope() LaunchedEffect(state.isFinishing) { - if (itemCount > 50 && !state.isFinishing) { + if (itemCount >= 60 && !state.isFinishing) { hasMoreData = false } } @@ -69,20 +54,45 @@ fun LottieRefreshIndicatorSample() { UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + hasMoreData = true + state.isRefreshing = false + } }, onLoadMore = { - state.isLoading = true + if (hasMoreData) { + coroutineScope.launch { + state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + if (itemCount < 60) { + itemCount += 20 + } + state.isLoading = false + } + } }, + modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerScrollMode = NestedScrollMode.FixedBehind, footerScrollMode = NestedScrollMode.FixedBehind, - loadMoreEnabled = hasMoreData, - modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerIndicator = { LottieRefreshHeader(it) }, footerIndicator = { - LottieRefreshFooter(it) + if (hasMoreData) { + LottieRefreshFooter(it) + } else { + Text( + text = "———— 我是有底线的 ————", + color = Color(0xFF999999), + fontSize = 15.sp, + modifier = Modifier.padding(vertical = 16.dp) + ) + } } ) { LazyColumn(Modifier.background(color = Color.White)) { @@ -91,25 +101,12 @@ fun LottieRefreshIndicatorSample() { val title = "UltraSwipeRefresh列表标题${it + 1}" val content = "UltraSwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) } } - - if (!hasMoreData) { - item { - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - text = "没有更多数据了", - color = Color(0xFF999999), - fontSize = 15.sp, - modifier = Modifier.padding(vertical = 16.dp) - ) - } - } - } } } } \ No newline at end of file diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/ProgressRefreshIndicatorSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/ProgressRefreshIndicatorSample.kt index e551358..6cdf6c0 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/ProgressRefreshIndicatorSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/ProgressRefreshIndicatorSample.kt @@ -5,14 +5,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,6 +26,7 @@ import com.king.ultraswiperefresh.indicator.progress.ProgressRefreshFooter import com.king.ultraswiperefresh.indicator.progress.ProgressRefreshHeader import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * 进度条刷新样式示例 @@ -38,45 +39,39 @@ import kotlinx.coroutines.delay fun ProgressRefreshIndicatorSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - var hasMoreData by remember { mutableStateOf(true) } - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - itemCount = 20 - hasMoreData = true - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - itemCount += 20 - state.isLoading = false - } - } - - LaunchedEffect(state.isFinishing) { - if (itemCount > 50 && !state.isFinishing) { - hasMoreData = false - } - } + val coroutineScope = rememberCoroutineScope() UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + hasMoreData = true + state.isRefreshing = false + } }, onLoadMore = { - state.isLoading = true + coroutineScope.launch { + state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + if (itemCount >= 60) { + hasMoreData = false + } else { + itemCount += 20 + } + state.isLoading = false + } }, headerScrollMode = NestedScrollMode.FixedFront, footerScrollMode = NestedScrollMode.FixedFront, loadMoreEnabled = hasMoreData, + alwaysScrollable = true, headerIndicator = { ProgressRefreshHeader(it) }, @@ -90,7 +85,10 @@ fun ProgressRefreshIndicatorSample() { val title = "UltraSwipeRefresh列表标题${it + 1}" val content = "UltraSwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider(modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6)) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color(0xFFF2F3F6) + ) } } diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/PullRefreshSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/PullRefreshSample.kt index 159d6a1..308f99c 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/PullRefreshSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/PullRefreshSample.kt @@ -4,17 +4,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.king.ultraswiperefresh.app.component.ColumnItem import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * PullRefresh示例 @@ -35,21 +36,19 @@ import kotlinx.coroutines.delay fun PullRefreshSample() { var isRefreshing by remember { mutableStateOf(false) } - var itemCount by remember { mutableIntStateOf(20) } - - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - delay(2000) - itemCount = 20 - isRefreshing = false - } - } + val coroutineScope = rememberCoroutineScope() val state = rememberPullRefreshState( refreshing = isRefreshing, onRefresh = { - isRefreshing = true + coroutineScope.launch { + isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + isRefreshing = false + } } ) @@ -60,7 +59,7 @@ fun PullRefreshSample() { val title = "PullRefresh列表标题${it + 1}" val content = "PullRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshIndicatorSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshIndicatorSample.kt index b71125c..4c25d56 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshIndicatorSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshIndicatorSample.kt @@ -5,14 +5,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,6 +26,7 @@ import com.king.ultraswiperefresh.indicator.SwipeRefreshFooter import com.king.ultraswiperefresh.indicator.SwipeRefreshHeader import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * 官方默认的刷新样式示例 @@ -38,46 +39,40 @@ import kotlinx.coroutines.delay fun SwipeRefreshIndicatorSample() { val state = rememberUltraSwipeRefreshState() - var itemCount by remember { mutableIntStateOf(20) } - var hasMoreData by remember { mutableStateOf(true) } - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - itemCount = 20 - hasMoreData = true - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - itemCount += 20 - state.isLoading = false - } - } - - LaunchedEffect(state.isFinishing) { - if (itemCount > 50 && !state.isFinishing) { - hasMoreData = false - } - } + val coroutineScope = rememberCoroutineScope() UltraSwipeRefresh( state = state, onRefresh = { - state.isRefreshing = true + coroutineScope.launch { + state.isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + hasMoreData = true + state.isRefreshing = false + } }, onLoadMore = { - state.isLoading = true + coroutineScope.launch { + state.isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + if (itemCount >= 60) { + hasMoreData = false + } else { + itemCount += 20 + } + state.isLoading = false + } }, + modifier = Modifier.background(color = Color(0x7FEEEEEE)), headerScrollMode = NestedScrollMode.FixedContent, footerScrollMode = NestedScrollMode.FixedContent, loadMoreEnabled = hasMoreData, - modifier = Modifier.background(color = Color(0x7FEEEEEE)), + alwaysScrollable = true, headerIndicator = { SwipeRefreshHeader(it) }, @@ -91,7 +86,10 @@ fun SwipeRefreshIndicatorSample() { val title = "UltraSwipeRefresh列表标题${it + 1}" val content = "UltraSwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider(modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6)) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color(0xFFF2F3F6) + ) } } diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshSample.kt index acfb577..fb23019 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/SwipeRefreshSample.kt @@ -3,13 +3,13 @@ package com.king.ultraswiperefresh.app.sample import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -18,6 +18,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.king.ultraswiperefresh.app.component.ColumnItem import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * SwipeRefresh示例 @@ -31,21 +32,19 @@ import kotlinx.coroutines.delay fun SwipeRefreshSample() { var isRefreshing by remember { mutableStateOf(false) } - var itemCount by remember { mutableIntStateOf(20) } - - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - delay(2000) - itemCount = 20 - isRefreshing = false - } - } + val coroutineScope = rememberCoroutineScope() SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = { - isRefreshing = true + coroutineScope.launch { + isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + itemCount = 20 + isRefreshing = false + } } ) { LazyColumn(Modifier.background(color = Color.White)) { @@ -54,7 +53,7 @@ fun SwipeRefreshSample() { val title = "SwipeRefresh列表标题${it + 1}" val content = "SwipeRefresh列表内容${it + 1}" ColumnItem(title = title, content = content) - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) diff --git a/app/src/main/java/com/king/ultraswiperefresh/app/sample/UltraSwipeRefreshSample.kt b/app/src/main/java/com/king/ultraswiperefresh/app/sample/UltraSwipeRefreshSample.kt index d3a9dd1..2d2ca2f 100644 --- a/app/src/main/java/com/king/ultraswiperefresh/app/sample/UltraSwipeRefreshSample.kt +++ b/app/src/main/java/com/king/ultraswiperefresh/app/sample/UltraSwipeRefreshSample.kt @@ -4,12 +4,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -23,9 +23,9 @@ import com.king.ultraswiperefresh.app.ext.showToast import com.king.ultraswiperefresh.app.navigation.NavRoute import com.king.ultraswiperefresh.indicator.SwipeRefreshFooter import com.king.ultraswiperefresh.indicator.SwipeRefreshHeader -import com.king.ultraswiperefresh.rememberUltraSwipeRefreshState import com.king.ultraswiperefresh.theme.UltraSwipeRefreshTheme import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * UltraSwipeRefresh 示例 @@ -37,21 +37,9 @@ import kotlinx.coroutines.delay @Composable fun UltraSwipeRefreshSample(navController: NavController) { - val state = rememberUltraSwipeRefreshState() - - LaunchedEffect(state.isRefreshing) { - if (state.isRefreshing) { - delay(2000) - state.isRefreshing = false - } - } - - LaunchedEffect(state.isLoading) { - if (state.isLoading) { - delay(2000) - state.isLoading = false - } - } + var isRefreshing by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() var headerScrollMode by remember { mutableStateOf(NestedScrollMode.FixedContent) @@ -62,25 +50,8 @@ fun UltraSwipeRefreshSample(navController: NavController) { val context = LocalContext.current - UltraSwipeRefresh( - state = state, - onRefresh = { - state.isRefreshing = true - }, - onLoadMore = { - state.isLoading = true - }, - headerScrollMode = headerScrollMode, - footerScrollMode = footerScrollMode, - modifier = Modifier.background(color = Color(0x7FEEEEEE)), - headerIndicator = { - SwipeRefreshHeader(it) - }, - footerIndicator = { - SwipeRefreshFooter(it) - } - ) { - val map = mutableMapOf>().apply { + val map = remember { + mutableMapOf>().apply { put( NavRoute.SwipeRefreshIndicatorSample, "默认刷新样式示例" to "使用NestedScrollMode.FixedContent;特点:固定内容;即:内容固定,Header或 Footer进行滑动" @@ -110,7 +81,37 @@ fun UltraSwipeRefreshSample(navController: NavController) { "Material中的Modifier.pullRefresh示例" to "只支持下拉刷新,此示例主要用于与UltraSwipeRefresh进行效果对比(后续可能会移除)" ) } + } + UltraSwipeRefresh( + isRefreshing = isRefreshing, + isLoading = isLoading, + onRefresh = { + coroutineScope.launch { + isRefreshing = true + // TODO 刷新的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + isRefreshing = false + } + }, + onLoadMore = { + coroutineScope.launch { + isLoading = true + // TODO 加载更多的逻辑处理,此处的延时只是为了演示效果 + delay(2000) + isLoading = false + } + }, + modifier = Modifier.background(color = Color(0x7FEEEEEE)), + headerScrollMode = headerScrollMode, + footerScrollMode = footerScrollMode, + headerIndicator = { + SwipeRefreshHeader(it) + }, + footerIndicator = { + SwipeRefreshFooter(it) + } + ) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -123,18 +124,22 @@ fun UltraSwipeRefreshSample(navController: NavController) { content = "[headerIndicator] 和 [footerIndicator]可随意定制,并且[Header]和[Footer]样式与滑动模式可随意组合。" ) { val vibrateEnabled = !UltraSwipeRefreshTheme.config.vibrateEnabled - UltraSwipeRefreshTheme.config = UltraSwipeRefreshTheme.config.copy(vibrateEnabled = vibrateEnabled) - if(vibrateEnabled) { + UltraSwipeRefreshTheme.config = + UltraSwipeRefreshTheme.config.copy(vibrateEnabled = vibrateEnabled) + if (vibrateEnabled) { context.showToast("已全局启用振动效果") } else { context.showToast("已全局关闭振动效果") } } - Divider(modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6)) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color(0xFFF2F3F6) + ) } item { - val nestedScrollModes = remember { NestedScrollMode.values() } + val nestedScrollModes = remember { NestedScrollMode.entries } ColumnItem( title = "默认刷新样式 + 随机滑动模式", content = "\n当前页所选的滑动模式\n" + @@ -146,7 +151,10 @@ fun UltraSwipeRefreshSample(navController: NavController) { footerScrollMode = nestedScrollModes.random() context.showToast("滑动模式已随机") } - Divider(modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6)) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color(0xFFF2F3F6) + ) } map.forEach { (key, value) -> @@ -154,7 +162,7 @@ fun UltraSwipeRefreshSample(navController: NavController) { ColumnItem(title = value.first, content = value.second) { navController.navigate(route = key.name) } - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFF2F3F6) ) diff --git a/build.gradle b/build.gradle index 6eb034b..4db4e87 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,6 @@ buildscript { plugins { id 'com.android.application' version '8.1.2' apply false id 'com.android.library' version '8.1.2' apply false - id 'org.jetbrains.kotlin.android' version '1.8.10' apply false + id 'org.jetbrains.kotlin.android' version '1.9.0' apply false id 'com.vanniktech.maven.publish' version '0.25.3' apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f4d7ef3..4727f15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,8 +22,8 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -VERSION_NAME=1.2.0 -VERSION_CODE=6 +VERSION_NAME=1.3.0 +VERSION_CODE=7 GROUP=com.github.jenly1314.UltraSwipeRefresh POM_DESCRIPTION=UltraSwipeRefresh for Android diff --git a/refresh-indicator-classic/build.gradle b/refresh-indicator-classic/build.gradle index 1790702..5709e18 100644 --- a/refresh-indicator-classic/build.gradle +++ b/refresh-indicator-classic/build.gradle @@ -33,7 +33,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.3' + kotlinCompilerExtensionVersion versions.compose_compiler } lint { abortOnError false diff --git a/refresh-indicator-classic/src/main/java/com/king/ultraswiperefresh/indicator/classic/ClassicRefreshIndicator.kt b/refresh-indicator-classic/src/main/java/com/king/ultraswiperefresh/indicator/classic/ClassicRefreshIndicator.kt index 1599bc5..27beb0a 100644 --- a/refresh-indicator-classic/src/main/java/com/king/ultraswiperefresh/indicator/classic/ClassicRefreshIndicator.kt +++ b/refresh-indicator-classic/src/main/java/com/king/ultraswiperefresh/indicator/classic/ClassicRefreshIndicator.kt @@ -1,5 +1,6 @@ package com.king.ultraswiperefresh.indicator.classic +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat @@ -37,6 +38,7 @@ import androidx.compose.ui.unit.sp import com.king.ultraswiperefresh.UltraSwipeFooterState import com.king.ultraswiperefresh.UltraSwipeHeaderState import com.king.ultraswiperefresh.UltraSwipeRefreshState +import com.king.ultraswiperefresh.indicator.CrossFadeDurationMs /** * 经典样式的指示器 @@ -112,37 +114,43 @@ internal fun ClassicRefreshIndicator( modifier = Modifier.alpha(alphaState.value), verticalAlignment = Alignment.CenterVertically ) { - if (if (isFooter) state.isLoading else state.isRefreshing) { - val transition = rememberInfiniteTransition(label) - val rotate by transition.animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, - easing = LinearEasing - ) - ), - label = label - ) - Image( - painter = loadingIconPainter, - contentDescription = null, - modifier = Modifier - .size(iconSize) - .rotate(rotate), - colorFilter = iconColorFilter, - ) + Crossfade( + targetState = if (isFooter) state.isLoading else state.isRefreshing, + animationSpec = tween(durationMillis = CrossFadeDurationMs), + label = label + ) { + if (it) { + val transition = rememberInfiniteTransition(label = "InfiniteTransition") + val rotate by transition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ), + label = "RotateAnimation" + ) + Image( + painter = loadingIconPainter, + contentDescription = null, + modifier = Modifier + .size(iconSize) + .rotate(rotate), + colorFilter = iconColorFilter, + ) - } else { - Image( - painter = arrowIconPainter, - contentDescription = null, - modifier = Modifier - .size(iconSize) - .rotate(arrowDegrees.value), - colorFilter = iconColorFilter, - ) + } else { + Image( + painter = arrowIconPainter, + contentDescription = null, + modifier = Modifier + .size(iconSize) + .rotate(arrowDegrees.value), + colorFilter = iconColorFilter, + ) + } } Column( modifier = Modifier.padding(horizontal = 6.dp), diff --git a/refresh-indicator-lottie/build.gradle b/refresh-indicator-lottie/build.gradle index 8924bf9..f8d8abc 100644 --- a/refresh-indicator-lottie/build.gradle +++ b/refresh-indicator-lottie/build.gradle @@ -33,7 +33,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.3' + kotlinCompilerExtensionVersion versions.compose_compiler } lint { abortOnError false diff --git a/refresh-indicator-progress/build.gradle b/refresh-indicator-progress/build.gradle index b152bd2..ffe8e54 100644 --- a/refresh-indicator-progress/build.gradle +++ b/refresh-indicator-progress/build.gradle @@ -33,7 +33,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.3' + kotlinCompilerExtensionVersion versions.compose_compiler } lint { abortOnError false diff --git a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshFooter.kt b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshFooter.kt index 3d848e3..043af95 100644 --- a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshFooter.kt +++ b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshFooter.kt @@ -26,6 +26,7 @@ fun ProgressRefreshFooter( isFooter = true, modifier = modifier, height = height, - color = color + color = color, + label = "FooterIndicator" ) } \ No newline at end of file diff --git a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshHeader.kt b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshHeader.kt index 15a1b88..94eb987 100644 --- a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshHeader.kt +++ b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshHeader.kt @@ -26,6 +26,7 @@ fun ProgressRefreshHeader( isFooter = false, modifier = modifier, height = height, - color = color + color = color, + label = "HeaderIndicator" ) } \ No newline at end of file diff --git a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshIndicator.kt b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshIndicator.kt index b0028d8..a120f50 100644 --- a/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshIndicator.kt +++ b/refresh-indicator-progress/src/main/java/com/king/ultraswiperefresh/indicator/progress/ProgressRefreshIndicator.kt @@ -1,6 +1,8 @@ package com.king.ultraswiperefresh.indicator.progress +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -18,6 +20,7 @@ import androidx.compose.ui.unit.dp import com.king.ultraswiperefresh.UltraSwipeFooterState import com.king.ultraswiperefresh.UltraSwipeHeaderState import com.king.ultraswiperefresh.UltraSwipeRefreshState +import com.king.ultraswiperefresh.indicator.CrossFadeDurationMs import com.king.ultraswiperefresh.indicator.LinearProgressIndicator /** @@ -34,8 +37,8 @@ internal fun ProgressRefreshIndicator( modifier: Modifier = Modifier, height: Dp = 60.dp, color: Color = Color(0xFF00CCFF), + label: String = "Indicator" ) { - val progressState = remember { derivedStateOf { when { @@ -88,22 +91,27 @@ internal fun ProgressRefreshIndicator( }, contentAlignment = if (isFooter) Alignment.BottomCenter else Alignment.TopCenter ) { - if (if (isFooter) { + Crossfade( + targetState = if (isFooter) { state.footerState == UltraSwipeFooterState.Loading } else { state.headerState == UltraSwipeHeaderState.Refreshing - } + }, + animationSpec = tween(durationMillis = CrossFadeDurationMs), + label = label ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = color - ) - } else { - LinearProgressIndicator( - progress = progressState.value, - modifier = Modifier.fillMaxWidth(), - color = color, - ) + if (it) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = color + ) + } else { + LinearProgressIndicator( + progress = progressState.value, + modifier = Modifier.fillMaxWidth(), + color = color, + ) + } } } } diff --git a/refresh/build.gradle b/refresh/build.gradle index 5ea0e09..98696ff 100644 --- a/refresh/build.gradle +++ b/refresh/build.gradle @@ -33,7 +33,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.3' + kotlinCompilerExtensionVersion versions.compose_compiler } lint { abortOnError false diff --git a/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefresh.kt b/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefresh.kt index 5c0c201..89a1a3b 100644 --- a/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefresh.kt +++ b/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefresh.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.delay * @param dragMultiplier 触发下拉刷新或上拉加载时的阻力系数;值越小,阻力越大;默认为:0.5 * @param finishDelayMillis 完成时延时时间;让完成时的中间状态[UltraSwipeRefreshState.isFinishing]停留一会儿,定格的展示提示内容;默认:500毫秒 * @param vibrateEnabled 是否启用振动,如果启用则当滑动偏移量满足触发刷新或触发加载更多时,会有振动效果;默认为:false + * @param alwaysScrollable 是否始终可以滚动;当为true时,则会忽略刷新中或加载中的状态限制,始终可以进行滚动;默认为:false * @param headerIndicator 下拉刷新时顶部显示的Header指示器 * @param footerIndicator 上拉加载更多时底部显示的Footer指示器 * @param contentContainer [content]的父容器,便于统一管理 @@ -78,6 +79,7 @@ fun UltraSwipeRefresh( @IntRange(from = 0, to = 2000) finishDelayMillis: Long = UltraSwipeRefreshTheme.config.finishDelayMillis, vibrateEnabled: Boolean = UltraSwipeRefreshTheme.config.vibrateEnabled, + alwaysScrollable: Boolean = UltraSwipeRefreshTheme.config.alwaysScrollable, headerIndicator: @Composable (UltraSwipeRefreshState) -> Unit = UltraSwipeRefreshTheme.config.headerIndicator, footerIndicator: @Composable (UltraSwipeRefreshState) -> Unit = UltraSwipeRefreshTheme.config.footerIndicator, contentContainer: @Composable (@Composable () -> Unit) -> Unit = UltraSwipeRefreshTheme.config.contentContainer, @@ -109,9 +111,10 @@ fun UltraSwipeRefresh( updateOnLoadMore.value.invoke() }) }.apply { + this.dragMultiplier = dragMultiplier this.refreshEnabled = refreshEnabled this.loadMoreEnabled = loadMoreEnabled - this.dragMultiplier = dragMultiplier + this.alwaysScrollable = alwaysScrollable } LaunchedEffect(headerHeight, footerHeight, refreshTriggerRate, loadMoreTriggerRate) { @@ -145,23 +148,7 @@ fun UltraSwipeRefresh( } } - if (vibrateEnabled) { - val vibrator = rememberVibrator() - if (vibrator.hasVibrator()) { - val vibrateState = remember { - derivedStateOf { - state.headerState == UltraSwipeHeaderState.ReleaseToRefresh || state.footerState == UltraSwipeFooterState.ReleaseToLoad - } - } - LaunchedEffect(vibrateState.value) { - if (vibrateState.value) { - vibrator.vibrate() - } - } - } else { - Log.w(TAG, "hasVibrator: false") - } - } + VibrationLaunchedEffect(vibrateEnabled, state) Box( modifier = Modifier @@ -220,6 +207,7 @@ fun UltraSwipeRefresh( * @param dragMultiplier 触发下拉刷新或上拉加载时的阻力系数;值越小,阻力越大;默认为:0.5 * @param finishDelayMillis 完成时延时时间;让完成时的中间状态[UltraSwipeRefreshState.isFinishing]停留一会儿,定格的展示提示内容;默认:500毫秒 * @param vibrateEnabled 是否启用振动,如果启用则当滑动偏移量满足触发刷新或触发加载更多时,会有振动效果;默认为:false + * @param alwaysScrollable 是否始终可以滚动;当为true时,则会忽略刷新中或加载中的状态限制,始终可以进行滚动;默认为:false * @param headerIndicator 下拉刷新时顶部显示的Header指示器 * @param footerIndicator 上拉加载更多时底部显示的Footer指示器 * @param contentContainer [content]的父容器,便于统一管理 @@ -249,6 +237,7 @@ fun UltraSwipeRefresh( @IntRange(from = 0, to = 2000) finishDelayMillis: Long = UltraSwipeRefreshTheme.config.finishDelayMillis, vibrateEnabled: Boolean = UltraSwipeRefreshTheme.config.vibrateEnabled, + alwaysScrollable: Boolean = UltraSwipeRefreshTheme.config.alwaysScrollable, headerIndicator: @Composable (UltraSwipeRefreshState) -> Unit = UltraSwipeRefreshTheme.config.headerIndicator, footerIndicator: @Composable (UltraSwipeRefreshState) -> Unit = UltraSwipeRefreshTheme.config.footerIndicator, contentContainer: @Composable (@Composable () -> Unit) -> Unit = UltraSwipeRefreshTheme.config.contentContainer, @@ -271,6 +260,7 @@ fun UltraSwipeRefresh( dragMultiplier = dragMultiplier, finishDelayMillis = finishDelayMillis, vibrateEnabled = vibrateEnabled, + alwaysScrollable = alwaysScrollable, headerIndicator = headerIndicator, footerIndicator = footerIndicator, contentContainer = contentContainer, @@ -375,6 +365,30 @@ private fun RefreshSubComposeLayout( } } +/** + * 振动效果反馈 + */ +@Composable +private fun VibrationLaunchedEffect(vibrateEnabled: Boolean, state: UltraSwipeRefreshState) { + if (vibrateEnabled) { + val vibrator = rememberVibrator() + if (vibrator.hasVibrator()) { + val vibrateState = remember { + derivedStateOf { + state.headerState == UltraSwipeHeaderState.ReleaseToRefresh || state.footerState == UltraSwipeFooterState.ReleaseToLoad + } + } + LaunchedEffect(vibrateState.value) { + if (vibrateState.value) { + vibrator.vibrate() + } + } + } else { + Log.w(TAG, "hasVibrator: false") + } + } +} + /** * Vibrator */ @@ -397,7 +411,12 @@ private fun rememberVibrator(): Vibrator { @Suppress("DEPRECATION") private fun Vibrator.vibrate() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrate(VibrationEffect.createOneShot(VibrationDurationMs, VibrationEffect.DEFAULT_AMPLITUDE)) + vibrate( + VibrationEffect.createOneShot( + VibrationDurationMs, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) } else { vibrate(VibrationDurationMs) } diff --git a/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefreshNestedScrollConnection.kt b/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefreshNestedScrollConnection.kt index 508ae5f..0cbb06f 100644 --- a/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefreshNestedScrollConnection.kt +++ b/refresh/src/main/java/com/king/ultraswiperefresh/UltraSwipeRefreshNestedScrollConnection.kt @@ -25,12 +25,13 @@ internal class UltraSwipeRefreshNestedScrollConnection( var dragMultiplier = 0.5f var refreshEnabled: Boolean = false var loadMoreEnabled: Boolean = false + var alwaysScrollable: Boolean = false override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = when { // 当下拉刷新和上拉加载都未启用时,则直接返回:Offset.Zero !(refreshEnabled || loadMoreEnabled) -> Offset.Zero - // 当正在刷新或正在加载或处理正在完成时,则直接返回:available(禁止滚动) - state.isRefreshing || state.isLoading || state.isFinishing -> available + // 当正在刷新或正在加载或处理正在完成时,交由 [obtainAvailable] 处理 + state.isRefreshing || state.isLoading || state.isFinishing -> obtainAvailable(available) // 当都Header和Footer都未显示时,则直接返回:Offset.Zero state.indicatorOffset == 0f -> Offset.Zero // 当正在滑动时,则进行处理 @@ -45,13 +46,16 @@ internal class UltraSwipeRefreshNestedScrollConnection( ): Offset = when { // 当下拉刷新和上拉加载都未启用时,则直接返回:Offset.Zero !(refreshEnabled || loadMoreEnabled) -> Offset.Zero - // 当正在刷新或正在加载或处理正在完成时,则直接返回:available(禁止滚动) - state.isRefreshing || state.isLoading || state.isFinishing -> available + // 当正在刷新或正在加载或处理正在完成时,交由 [obtainAvailable] 处理 + state.isRefreshing || state.isLoading || state.isFinishing -> obtainAvailable(available) // 当正在滑动时,则进行处理 source == NestedScrollSource.Drag -> onScroll(available) else -> Offset.Zero } + /** + * 处理可用的滚动偏移量 + */ private fun onScroll(available: Offset): Offset { if (available.y != 0f) { if (state.indicatorOffset <= 0f && available.y < 0f && !loadMoreEnabled) { @@ -81,7 +85,7 @@ internal class UltraSwipeRefreshNestedScrollConnection( override suspend fun onPreFling(available: Velocity): Velocity { if (state.isRefreshing || state.isLoading || state.isFinishing) { state.isSwipeInProgress = false - return available + return obtainAvailable(available) } when { refreshEnabled && state.isExceededRefreshTrigger() -> onRefresh() @@ -94,8 +98,27 @@ internal class UltraSwipeRefreshNestedScrollConnection( override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { if (state.isRefreshing || state.isLoading || state.isFinishing) { - return available + return obtainAvailable(available) } return Velocity.Zero } + + /** + * 根据 [alwaysScrollable] 来决定可用的 [Offset] + */ + private fun obtainAvailable(available: Offset) = if (alwaysScrollable) { + Offset.Zero + } else { + available + } + + /** + * 根据 [alwaysScrollable] 来决定可用的 [Velocity] + */ + private fun obtainAvailable(available: Velocity) = if (alwaysScrollable) { + Velocity.Zero + } else { + available + } + } \ No newline at end of file diff --git a/refresh/src/main/java/com/king/ultraswiperefresh/indicator/ProgressIndicator.kt b/refresh/src/main/java/com/king/ultraswiperefresh/indicator/ProgressIndicator.kt index cdaf3eb..1497c0f 100644 --- a/refresh/src/main/java/com/king/ultraswiperefresh/indicator/ProgressIndicator.kt +++ b/refresh/src/main/java/com/king/ultraswiperefresh/indicator/ProgressIndicator.kt @@ -15,6 +15,7 @@ */ package com.king.ultraswiperefresh.indicator +import androidx.annotation.FloatRange import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.Spring @@ -27,6 +28,7 @@ import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable @@ -38,134 +40,178 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import com.king.ultraswiperefresh.indicator.ProgressIndicatorDefaults.IndicatorBackgroundOpacity import kotlin.math.PI import kotlin.math.abs import kotlin.math.max +internal fun Modifier.increaseSemanticsBounds(): Modifier { + val padding = 10.dp + return this.layout { measurable, constraints -> + val paddingPx = padding.roundToPx() + // We need to add vertical padding to the semantics bounds in other to meet + // screenreader green box minimum size, but we also want to + // preserve a visual appearance and layout size below that minimum + // in order to maintain backwards compatibility. This custom + // layout effectively implements "negative padding". + val newConstraint = constraints.offset(0, paddingPx * 2) + val placeable = measurable.measure(newConstraint) + + // But when actually placing the placeable, create the layout without additional + // space. Place the placeable where it would've been without any extra padding. + val height = placeable.height - paddingPx * 2 + val width = placeable.width + layout(width, height) { placeable.place(0, -paddingPx) } + } + .semantics(mergeDescendants = true) {} + .padding(vertical = padding) +} + /** - * Determinate Material Design linear progress indicator. + * Determinate Material Design linear progress indicator. * * Progress indicators express an unspecified wait time or display the length of a process. * - * ![Linear progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/linear-progress-indicator.png) + * ![Linear progress indicator + * image](https://developer.android.com/images/reference/androidx/compose/material/linear-progress-indicator.png) * * By default there is no animation between [progress] values. You can use - * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended - * [AnimationSpec] when animating progress, such as in the following example: + * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when + * animating progress, such as in the following example: * * @sample androidx.compose.material.samples.LinearProgressIndicatorSample - * * @param progress The progress of this progress indicator, where 0.0 represents no progress and 1.0 - * represents full progress. Values outside of this range are coerced into the range. + * represents full progress. Values outside of this range are coerced into the range. + * @param modifier the [Modifier] to be applied to this progress indicator * @param color The color of the progress indicator. * @param backgroundColor The color of the background behind the indicator, visible when the - * progress has not reached that area of the overall indicator yet. + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator */ @Composable fun LinearProgressIndicator( - /*@FloatRange(from = 0.0, to = 1.0)*/ - progress: Float, + @FloatRange(from = 0.0, to = 1.0) progress: Float, modifier: Modifier = Modifier, color: Color = Color.Black, - backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity) + backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity), + strokeCap: StrokeCap = StrokeCap.Butt, ) { + val coercedProgress = progress.coerceIn(0f, 1f) Canvas( modifier - .progressSemantics(progress) + .increaseSemanticsBounds() + .progressSemantics(coercedProgress) .size(LinearIndicatorWidth, LinearIndicatorHeight) ) { val strokeWidth = size.height - drawLinearIndicatorBackground(backgroundColor, strokeWidth) - drawLinearIndicator(0f, progress, color, strokeWidth) + drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap) + drawLinearIndicator(0f, coercedProgress, color, strokeWidth, strokeCap) } } /** - * Indeterminate Material Design linear progress indicator. + * Indeterminate Material Design linear progress indicator. * * Progress indicators express an unspecified wait time or display the length of a process. * - * ![Linear progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/linear-progress-indicator.png) + * ![Linear progress indicator + * image](https://developer.android.com/images/reference/androidx/compose/material/linear-progress-indicator.png) * + * @param modifier the [Modifier] to be applied to this progress indicator * @param color The color of the progress indicator. * @param backgroundColor The color of the background behind the indicator, visible when the - * progress has not reached that area of the overall indicator yet. + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator */ @Composable fun LinearProgressIndicator( modifier: Modifier = Modifier, color: Color = Color.Black, - backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity) + backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity), + strokeCap: StrokeCap = StrokeCap.Butt, ) { val infiniteTransition = rememberInfiniteTransition() // Fractional position of the 'head' and 'tail' of the two lines drawn. I.e if the head is 0.8 // and the tail is 0.2, there is a line drawn from between 20% along to 80% along the total // width. - val firstLineHead by infiniteTransition.animateFloat( + val firstLineHead by + infiniteTransition.animateFloat( 0f, 1f, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = LinearAnimationDuration - 0f at FirstLineHeadDelay with FirstLineHeadEasing + 0f at FirstLineHeadDelay using FirstLineHeadEasing 1f at FirstLineHeadDuration + FirstLineHeadDelay } - ), - label = "firstLineHead" + ) ) - val firstLineTail by infiniteTransition.animateFloat( + val firstLineTail by + infiniteTransition.animateFloat( 0f, 1f, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = LinearAnimationDuration - 0f at FirstLineTailDelay with FirstLineTailEasing + 0f at FirstLineTailDelay using FirstLineTailEasing 1f at FirstLineTailDuration + FirstLineTailDelay } - ), - label = "firstLineTail" + ) ) - val secondLineHead by infiniteTransition.animateFloat( + val secondLineHead by + infiniteTransition.animateFloat( 0f, 1f, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = LinearAnimationDuration - 0f at SecondLineHeadDelay with SecondLineHeadEasing + 0f at SecondLineHeadDelay using SecondLineHeadEasing 1f at SecondLineHeadDuration + SecondLineHeadDelay } - ), - label = "secondLineHead" + ) ) - val secondLineTail by infiniteTransition.animateFloat( + val secondLineTail by + infiniteTransition.animateFloat( 0f, 1f, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = LinearAnimationDuration - 0f at SecondLineTailDelay with SecondLineTailEasing + 0f at SecondLineTailDelay using SecondLineTailEasing 1f at SecondLineTailDuration + SecondLineTailDelay } - ), - label = "secondLineTail" + ) ) Canvas( modifier + .increaseSemanticsBounds() .progressSemantics() .size(LinearIndicatorWidth, LinearIndicatorHeight) ) { val strokeWidth = size.height - drawLinearIndicatorBackground(backgroundColor, strokeWidth) + drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap) if (firstLineHead - firstLineTail > 0) { drawLinearIndicator( firstLineHead, firstLineTail, color, - strokeWidth + strokeWidth, + strokeCap, ) } if ((secondLineHead - secondLineTail) > 0) { @@ -173,17 +219,49 @@ fun LinearProgressIndicator( secondLineHead, secondLineTail, color, - strokeWidth + strokeWidth, + strokeCap, ) } } } +@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) +@Composable +fun LinearProgressIndicator( + progress: Float, + modifier: Modifier = Modifier, + color: Color = Color.Black, + backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity) +) = + LinearProgressIndicator( + progress, + modifier, + color, + backgroundColor, + strokeCap = StrokeCap.Butt, + ) + +@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) +@Composable +fun LinearProgressIndicator( + modifier: Modifier = Modifier, + color: Color = Color.Black, + backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity) +) = + LinearProgressIndicator( + modifier, + color, + backgroundColor, + strokeCap = StrokeCap.Butt, + ) + private fun DrawScope.drawLinearIndicator( startFraction: Float, endFraction: Float, color: Color, - strokeWidth: Float + strokeWidth: Float, + strokeCap: StrokeCap, ) { val width = size.width val height = size.height @@ -194,129 +272,161 @@ private fun DrawScope.drawLinearIndicator( val barStart = (if (isLtr) startFraction else 1f - endFraction) * width val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width - // Progress line - drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth) + // if there isn't enough space to draw the stroke caps, fall back to StrokeCap.Butt + if (strokeCap == StrokeCap.Butt || height > width) { + // Progress line + drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth) + } else { + // need to adjust barStart and barEnd for the stroke caps + val strokeCapOffset = strokeWidth / 2 + val coerceRange = strokeCapOffset..(width - strokeCapOffset) + val adjustedBarStart = barStart.coerceIn(coerceRange) + val adjustedBarEnd = barEnd.coerceIn(coerceRange) + + if (abs(endFraction - startFraction) > 0) { + // Progress line + drawLine( + color, + Offset(adjustedBarStart, yOffset), + Offset(adjustedBarEnd, yOffset), + strokeWidth, + strokeCap, + ) + } + } } private fun DrawScope.drawLinearIndicatorBackground( color: Color, - strokeWidth: Float -) = drawLinearIndicator(0f, 1f, color, strokeWidth) + strokeWidth: Float, + strokeCap: StrokeCap, +) = drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap) /** - * Determinate Material Design circular progress indicator. + * Determinate Material Design circular progress indicator. * * Progress indicators express an unspecified wait time or display the length of a process. * - * ![Circular progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) + * ![Circular progress indicator + * image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) * * By default there is no animation between [progress] values. You can use - * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended - * [AnimationSpec] when animating progress, such as in the following example: + * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when + * animating progress, such as in the following example: * * @sample androidx.compose.material.samples.CircularProgressIndicatorSample - * * @param progress The progress of this progress indicator, where 0.0 represents no progress and 1.0 - * represents full progress. Values outside of this range are coerced into the range. + * represents full progress. Values outside of this range are coerced into the range. + * @param modifier the [Modifier] to be applied to this progress indicator * @param color The color of the progress indicator. * @param strokeWidth The stroke width for the progress indicator. + * @param backgroundColor The color of the background behind the indicator, visible when the + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator */ @Composable fun CircularProgressIndicator( - /*@FloatRange(from = 0.0, to = 1.0)*/ - progress: Float, + @FloatRange(from = 0.0, to = 1.0) progress: Float, modifier: Modifier = Modifier, color: Color = Color.Black, - strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth + strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, + backgroundColor: Color = Color.Transparent, + strokeCap: StrokeCap = StrokeCap.Butt, ) { - val stroke = with(LocalDensity.current) { - Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Butt) - } - Canvas( - modifier - .progressSemantics(progress) - .size(CircularIndicatorDiameter) - ) { + val coercedProgress = progress.coerceIn(0f, 1f) + val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = strokeCap) } + Canvas(modifier.progressSemantics(coercedProgress).size(CircularIndicatorDiameter)) { // Start at 12 O'clock val startAngle = 270f - val sweep = progress * 360f + val sweep = coercedProgress * 360f + drawCircularIndicatorBackground(backgroundColor, stroke) drawDeterminateCircularIndicator(startAngle, sweep, color, stroke) } } /** - * Indeterminate Material Design circular progress indicator. + * Indeterminate Material Design circular progress indicator. * * Progress indicators express an unspecified wait time or display the length of a process. * - * ![Circular progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) + * ![Circular progress indicator + * image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) * + * @param modifier the [Modifier] to be applied to this progress indicator * @param color The color of the progress indicator. * @param strokeWidth The stroke width for the progress indicator. + * @param backgroundColor The color of the background behind the indicator, visible when the + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator */ @Composable fun CircularProgressIndicator( modifier: Modifier = Modifier, color: Color = Color.Black, - strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth + strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, + backgroundColor: Color = Color.Transparent, + strokeCap: StrokeCap = StrokeCap.Square, ) { - val stroke = with(LocalDensity.current) { - Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square) - } + val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = strokeCap) } val transition = rememberInfiniteTransition() // The current rotation around the circle, so we know where to start the rotation from - val currentRotation by transition.animateValue( + val currentRotation by + transition.animateValue( 0, RotationsPerCycle, Int.VectorConverter, infiniteRepeatable( - animation = tween( + animation = + tween( durationMillis = RotationDuration * RotationsPerCycle, easing = LinearEasing ) ) ) // How far forward (degrees) the base point should be from the start point - val baseRotation by transition.animateFloat( + val baseRotation by + transition.animateFloat( 0f, BaseRotationAngle, infiniteRepeatable( - animation = tween( - durationMillis = RotationDuration, - easing = LinearEasing - ) + animation = tween(durationMillis = RotationDuration, easing = LinearEasing) ) ) // How far forward (degrees) both the head and tail should be from the base point - val endAngle by transition.animateFloat( + val endAngle by + transition.animateFloat( 0f, JumpRotationAngle, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration - 0f at 0 with CircularEasing + 0f at 0 using CircularEasing JumpRotationAngle at HeadAndTailAnimationDuration } ) ) - val startAngle by transition.animateFloat( + val startAngle by + transition.animateFloat( 0f, JumpRotationAngle, infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration - 0f at HeadAndTailDelayDuration with CircularEasing + 0f at HeadAndTailDelayDuration using CircularEasing JumpRotationAngle at durationMillis } ) ) - Canvas( - modifier - .progressSemantics() - .size(CircularIndicatorDiameter) - ) { + Canvas(modifier.progressSemantics().size(CircularIndicatorDiameter)) { + drawCircularIndicatorBackground(backgroundColor, stroke) val currentRotationAngleOffset = (currentRotation * RotationAngleOffset) % 360f @@ -329,6 +439,38 @@ fun CircularProgressIndicator( } } +@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) +@Composable +fun CircularProgressIndicator( + progress: Float, + modifier: Modifier = Modifier, + color: Color = Color.Black, + strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth +) = + CircularProgressIndicator( + progress, + modifier, + color, + strokeWidth, + backgroundColor = Color.Transparent, + strokeCap = StrokeCap.Butt, + ) + +@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) +@Composable +fun CircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = Color.Black, + strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth +) = + CircularProgressIndicator( + modifier, + color, + strokeWidth, + backgroundColor = Color.Transparent, + strokeCap = StrokeCap.Square, + ) + private fun DrawScope.drawCircularIndicator( startAngle: Float, sweep: Float, @@ -350,6 +492,9 @@ private fun DrawScope.drawCircularIndicator( ) } +private fun DrawScope.drawCircularIndicatorBackground(color: Color, stroke: Stroke) = + drawCircularIndicator(0f, 360f, color, stroke) + /** * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator]. */ @@ -358,8 +503,8 @@ object ProgressIndicatorDefaults { * Default stroke width for [CircularProgressIndicator], and default height for * [LinearProgressIndicator]. * - * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator], - * and by passing a layout modifier setting the height for [LinearProgressIndicator]. + * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator], and + * by passing a layout modifier setting the height for [LinearProgressIndicator]. */ val StrokeWidth = 4.dp @@ -373,13 +518,14 @@ object ProgressIndicatorDefaults { * The default [AnimationSpec] that should be used when animating between progress in a * determinate progress indicator. */ - val ProgressAnimationSpec = SpringSpec( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessVeryLow, - // The default threshold is 0.01, or 1% of the overall progress range, which is quite - // large and noticeable. - visibilityThreshold = 1 / 1000f - ) + val ProgressAnimationSpec = + SpringSpec( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessVeryLow, + // The default threshold is 0.01, or 1% of the overall progress range, which is quite + // large and noticeable. + visibilityThreshold = 1 / 1000f + ) } private fun DrawScope.drawDeterminateCircularIndicator( @@ -396,15 +542,19 @@ private fun DrawScope.drawIndeterminateCircularIndicator( color: Color, stroke: Stroke ) { - // Length of arc is angle * radius - // Angle (radians) is length / radius - // The length should be the same as the stroke width for calculating the min angle - val squareStrokeCapOffset = - (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f + val strokeCapOffset = + if (stroke.cap == StrokeCap.Butt) { + 0f + } else { + // Length of arc is angle * radius + // Angle (radians) is length / radius + // The length should be the same as the stroke width for calculating the min angle + (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f + } - // Adding a square stroke cap draws half the stroke width behind the start point, so we want to + // Adding a stroke cap draws half the stroke width behind the start point, so we want to // move it forward by that amount so the arc visually appears in the correct place - val adjustedStartAngle = startAngle + squareStrokeCapOffset + val adjustedStartAngle = startAngle + strokeCapOffset // When the start and end angles are in the same place, we still want to draw a small sweep, so // the stroke caps get added on both ends and we draw the correct minimum length arc @@ -473,6 +623,4 @@ private const val HeadAndTailAnimationDuration = (RotationDuration * 0.5).toInt( private const val HeadAndTailDelayDuration = HeadAndTailAnimationDuration // The easing for the head and tail jump -private val CircularEasing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f) - -private const val IndicatorBackgroundOpacity = 0.24f \ No newline at end of file +private val CircularEasing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f) \ No newline at end of file diff --git a/refresh/src/main/java/com/king/ultraswiperefresh/indicator/SwipeRefreshIndicator.kt b/refresh/src/main/java/com/king/ultraswiperefresh/indicator/SwipeRefreshIndicator.kt index e68d755..6a28e54 100644 --- a/refresh/src/main/java/com/king/ultraswiperefresh/indicator/SwipeRefreshIndicator.kt +++ b/refresh/src/main/java/com/king/ultraswiperefresh/indicator/SwipeRefreshIndicator.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -124,17 +125,13 @@ internal fun SwipeRefreshIndicator( elevation: Dp = 6.dp, label: String = "Indicator" ) { - val adjustedElevation = if (isFooter) { - when { - state.isLoading -> elevation - state.indicatorOffset < -0.5f -> elevation - else -> 0.dp - } - } else { - when { - state.isRefreshing -> elevation - state.indicatorOffset > 0.5f -> elevation - else -> 0.dp + val showElevation by remember(isFooter, state) { + derivedStateOf { + if(isFooter) { + state.isLoading || state.indicatorOffset < 0f + } else { + state.isRefreshing || state.indicatorOffset > 0f + } } } val sizes = if (largeIndication) largeSizes else defaultSizes @@ -201,7 +198,7 @@ internal fun SwipeRefreshIndicator( }, shape = shape, color = backgroundColor, - elevation = adjustedElevation + elevation = if(showElevation) elevation else 0.dp ) { val painter = remember { CircularProgressPainter() } painter.arcRadius = sizes.arcRadius @@ -242,7 +239,7 @@ internal fun SwipeRefreshIndicator( } else { state.headerState == UltraSwipeHeaderState.Refreshing }, - animationSpec = tween(durationMillis = CrossfadeDurationMs), + animationSpec = tween(durationMillis = CrossFadeDurationMs), label = label ) { refreshing -> Box( @@ -301,4 +298,4 @@ private fun Modifier.surface( .background(color = backgroundColor, shape = shape) .clip(shape) -private const val CrossfadeDurationMs = 100 +const val CrossFadeDurationMs = 100 diff --git a/refresh/src/main/java/com/king/ultraswiperefresh/theme/UltraSwipeRefreshTheme.kt b/refresh/src/main/java/com/king/ultraswiperefresh/theme/UltraSwipeRefreshTheme.kt index c94f6f5..0237f94 100644 --- a/refresh/src/main/java/com/king/ultraswiperefresh/theme/UltraSwipeRefreshTheme.kt +++ b/refresh/src/main/java/com/king/ultraswiperefresh/theme/UltraSwipeRefreshTheme.kt @@ -46,6 +46,7 @@ object UltraSwipeRefreshTheme { * @param dragMultiplier 触发下拉刷新或上拉加载时的阻力系数;值越小,阻力越大;默认为:0.5 * @param finishDelayMillis 完成时延时时间;让完成时的中间状态[UltraSwipeRefreshState.isFinishing]停留一会儿,定格的展示提示内容;默认:500毫秒 * @param vibrateEnabled 是否启用振动,如果启用则当滑动偏移量满足触发刷新或触发加载更多时,会有振动效果;默认为:false + * @param alwaysScrollable 是否始终可以滚动;当为true时,则会忽略刷新中或加载中的状态限制,始终可以进行滚动;默认为:false * @param headerIndicator 下拉刷新时顶部显示的Header指示器 * @param footerIndicator 上拉加载更多时底部显示的Footer指示器 * @param contentContainer 内容的父容器,便于统一管理 @@ -62,6 +63,7 @@ data class UltraSwipeRefreshConfig( @FloatRange(from = 0.0, to = 1.0, fromInclusive = false) val dragMultiplier: Float = 0.5f, @IntRange(from = 0, to = 2000) val finishDelayMillis: Long = 500, val vibrateEnabled: Boolean = false, + val alwaysScrollable: Boolean = false, val headerIndicator: @Composable (UltraSwipeRefreshState) -> Unit = { SwipeRefreshHeader(it) }, diff --git a/test.md b/test.md index 89ac96a..1a5beac 100644 --- a/test.md +++ b/test.md @@ -1,5 +1,5 @@ -## 待发布版本 +## ~待发布版本~ (已发布v1.3.0) 待发布版本暂使用 **Jit Pack** 仓库;待收集的一些问题测试稳定后,再统一发布正式版本至 **Maven Central** 仓库。 diff --git a/versions.gradle b/versions.gradle index 7ff8705..4600e40 100644 --- a/versions.gradle +++ b/versions.gradle @@ -1,7 +1,7 @@ // App def app_version = [:] -app_version.versionCode = 6 -app_version.versionName = "1.2.0" +app_version.versionCode = 7 +app_version.versionName = "1.3.0" ext.app_version = app_version // build version @@ -17,13 +17,14 @@ ext.deps = [:] // App dependencies def versions = [:] // AndroidX -versions.core_ktx = "1.10.1" +versions.core_ktx = "1.12.0" +versions.compose_compiler = "1.5.2" versions.compose_bom = "2024.01.00" -versions.activity_compose = "1.7.2" +versions.activity_compose = "1.8.2" versions.foundation = "1.6.0" versions.material3 = "1.2.0" -versions.lifecycle_runtime_ktx = "2.6.2" -versions.navigation_compose = "2.6.0" +versions.lifecycle_runtime_ktx = "2.7.0" +versions.navigation_compose = "2.7.0" versions.lottie_compose = "6.1.0" versions.accompanist = "0.34.0"