diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 9a55c2d..fdf8d99 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/banner/build.gradle b/banner/build.gradle index 663300a..1ae97d0 100644 --- a/banner/build.gradle +++ b/banner/build.gradle @@ -17,7 +17,7 @@ afterEvaluate { // You can then customize attributes of the publication as shown below. groupId = 'com.zj.banner' artifactId = 'banner' - version = '2.5.0' + version = '2.5.2' } // // Creates a Maven publication called “debug”. // debug(MavenPublication) { @@ -71,8 +71,4 @@ dependencies { implementation 'io.coil-kt:coil-compose:2.4.0' - def accompanist_version = "0.31.5-beta" - api "com.google.accompanist:accompanist-pager:$accompanist_version" - api "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" - } \ No newline at end of file diff --git a/banner/src/main/java/com/zj/banner/BannerPager.kt b/banner/src/main/java/com/zj/banner/BannerPager.kt index 685de46..8a53a1f 100644 --- a/banner/src/main/java/com/zj/banner/BannerPager.kt +++ b/banner/src/main/java/com/zj/banner/BannerPager.kt @@ -1,10 +1,16 @@ +@file:OptIn(ExperimentalFoundationApi::class) + package com.zj.banner import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -12,10 +18,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp -import com.google.accompanist.pager.* import com.zj.banner.model.BaseBannerBean import com.zj.banner.ui.BannerCard import com.zj.banner.ui.config.BannerConfig +import com.zj.banner.utils.HorizontalPagerIndicator +import com.zj.banner.utils.VerticalPagerIndicator import kotlinx.coroutines.launch import java.util.* import kotlin.math.absoluteValue @@ -32,7 +39,6 @@ private const val TAG = "BannerPager" * @param indicatorGravity Banner 指示器位置,直接使用 Alignment 即可进行设定 * @param onBannerClick Banner 点击事件的回调 */ -@OptIn(ExperimentalPagerApi::class) @Composable fun BannerPager( modifier: Modifier = Modifier, @@ -46,7 +52,12 @@ fun BannerPager( throw NullPointerException("items is not null") } - val pagerState = rememberPagerState() + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f + ) { + items.size + } if (config.repeat) { StartBanner(pagerState, config.intervalTime) @@ -54,50 +65,55 @@ fun BannerPager( Box(modifier = modifier.height(config.bannerHeight)) { HorizontalPager( - count = items.size, + modifier = Modifier, state = pagerState, - itemSpacing = config.itemSpacing, - contentPadding = config.contentPadding - ) { page -> - val item = items[page] - BannerCard( - bean = item, - modifier = Modifier - .graphicsLayer { - // Calculate the absolute offset for the current page from the - // scroll position. We use the absolute value which allows us to mirror - // any effects for both directions - val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue + key = { items[it].data?:it }, + pageContent = { page -> + val item = items[page] - // We animate the scaleX + scaleY, between 85% and 100% - lerp( - start = 0.85f, - stop = 1f, - fraction = 1f - pageOffset.coerceIn(0f, 1f) - ).also { scale -> - scaleX = scale - scaleY = scale - } + BannerCard( + bean = item, + modifier = Modifier + .graphicsLayer { + // Calculate the absolute offset for the current page from the + // scroll position. We use the absolute value which allows us to mirror + // any effects for both directions + val offset = + (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction + val pageOffset = offset.absoluteValue + + // We animate the scaleX + scaleY, between 85% and 100% + lerp( + start = 0.85f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ).also { scale -> + scaleX = scale + scaleY = scale + } - // We animate the alpha, between 50% and 100% - alpha = lerp( - start = 0.5f, - stop = 1f, - fraction = 1f - pageOffset.coerceIn(0f, 1f) - ) - } - .fillMaxSize() - .padding(config.bannerImagePadding), - shape = config.shape, - contentScale = config.contentScale - ) { - Log.d(TAG, "item is :${item.javaClass}") - onBannerClick(item) + // We animate the alpha, between 50% and 100% + alpha = lerp( + start = 0.5f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + } + .fillMaxSize() + .padding(config.bannerImagePadding), + shape = config.shape, + contentScale = config.contentScale + ) { + Log.d(TAG, "item is :${item.javaClass}") + onBannerClick(item) + } } - } + ) + if (indicatorIsVertical) { VerticalPagerIndicator( pagerState = pagerState, + pageCount = items.size, modifier = Modifier .align(indicatorGravity) .padding(16.dp), @@ -105,6 +121,7 @@ fun BannerPager( } else { HorizontalPagerIndicator( pagerState = pagerState, + pageCount = items.size, modifier = Modifier .align(indicatorGravity) .padding(16.dp), @@ -114,8 +131,6 @@ fun BannerPager( } } - -@ExperimentalPagerApi @Composable fun StartBanner(pagerState: PagerState, intervalTime: Long) { val coroutineScope = rememberCoroutineScope() diff --git a/banner/src/main/java/com/zj/banner/utils/PagerIndicator.kt b/banner/src/main/java/com/zj/banner/utils/PagerIndicator.kt new file mode 100644 index 0000000..6a7d90f --- /dev/null +++ b/banner/src/main/java/com/zj/banner/utils/PagerIndicator.kt @@ -0,0 +1,282 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package com.zj.banner.utils + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.absoluteValue +import kotlin.math.sign + + +/** + * A horizontally laid out indicator for a [androidx.compose.foundation.pager.HorizontalPager] or + * [androidx.compose.foundation.pager.VerticalPager], representing + * the currently active page and total pages drawn using a [Shape]. + * + * This element allows the setting of the [indicatorShape], which defines how the + * indicator is visually represented. + * + * @param pagerState A [androidx.compose.foundation.pager.PagerState] object of your + * [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager]to be used to observe the list's state. + * @param modifier the modifier to apply to this layout. + * @param pageCount the size of indicators should be displayed. + * If you are implementing a looping pager with a much larger [pageCount] + * than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param. + * @param pageIndexMapping describe how to get the position of active indicator by the giving page + * from [androidx.compose.foundation.pager.PagerState.currentPage]. + * @param activeColor the color of the active Page indicator + * @param inactiveColor the color of page indicators that are inactive. This defaults to + * [activeColor] with the alpha component set to the [ContentAlpha.disabled]. + * @param indicatorWidth the width of each indicator in [Dp]. + * @param indicatorHeight the height of each indicator in [Dp]. Defaults to [indicatorWidth]. + * @param spacing the spacing between each indicator in [Dp]. + * @param indicatorShape the shape representing each indicator. This defaults to [CircleShape]. + */ +@Composable +fun HorizontalPagerIndicator( + pagerState: PagerState, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorWidth: Dp = 8.dp, + indicatorHeight: Dp = indicatorWidth, + spacing: Dp = indicatorWidth, + indicatorShape: Shape = CircleShape, +) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + } + + HorizontalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +@Composable +private fun HorizontalPagerIndicator( + pagerState: PagerStateBridge, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorWidth: Dp = 8.dp, + indicatorHeight: Dp = indicatorWidth, + spacing: Dp = indicatorWidth, + indicatorShape: Shape = CircleShape, +) { + + val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() } + val spacingPx = LocalDensity.current.run { spacing.roundToPx() } + + Box( + modifier = modifier, + contentAlignment = Alignment.CenterStart + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(spacing), + verticalAlignment = Alignment.CenterVertically, + ) { + val indicatorModifier = Modifier + .size(width = indicatorWidth, height = indicatorHeight) + .background(color = inactiveColor, shape = indicatorShape) + + repeat(pageCount) { + Box(indicatorModifier) + } + } + + Box( + Modifier + .offset { + val position = pageIndexMapping(pagerState.currentPage) + val offset = pagerState.currentPageOffset + val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt()) + val scrollPosition = ((next - position) * offset.absoluteValue + position) + .coerceIn( + 0f, + (pageCount - 1) + .coerceAtLeast(0) + .toFloat() + ) + + IntOffset( + x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(), + y = 0 + ) + } + .size(width = indicatorWidth, height = indicatorHeight) + .then( + if (pageCount > 0) Modifier.background( + color = activeColor, + shape = indicatorShape, + ) + else Modifier + ) + ) + } +} + +/** + * A vertically laid out indicator for a [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager], representing + * the currently active page and total pages drawn using a [Shape]. + * + * This element allows the setting of the [indicatorShape], which defines how the + * indicator is visually represented. + * + * @param pagerState A [androidx.compose.foundation.pager.PagerState] object of your + * [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager]to be used to observe the list's state. + * @param modifier the modifier to apply to this layout. + * @param pageCount the size of indicators should be displayed. If you are implementing a looping + * pager with a much larger [pageCount] than indicators should displayed, e.g. [Int.MAX_VALUE], + * specify you real size in this param. + * @param pageIndexMapping describe how to get the position of active indicator by the giving page + * from [androidx.compose.foundation.pager.PagerState.currentPage]. + * @param activeColor the color of the active Page indicator + * @param inactiveColor the color of page indicators that are inactive. This defaults to + * [activeColor] with the alpha component set to the [ContentAlpha.disabled]. + * @param indicatorHeight the height of each indicator in [Dp]. + * @param indicatorWidth the width of each indicator in [Dp]. Defaults to [indicatorHeight]. + * @param spacing the spacing between each indicator in [Dp]. + * @param indicatorShape the shape representing each indicator. This defaults to [CircleShape]. + */ +@Composable +fun VerticalPagerIndicator( + pagerState: PagerState, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorHeight: Dp = 8.dp, + indicatorWidth: Dp = indicatorHeight, + spacing: Dp = indicatorHeight, + indicatorShape: Shape = CircleShape, +) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + } + + VerticalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +@Composable +private fun VerticalPagerIndicator( + pagerState: PagerStateBridge, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorHeight: Dp = 8.dp, + indicatorWidth: Dp = indicatorHeight, + spacing: Dp = indicatorHeight, + indicatorShape: Shape = CircleShape, +) { + + val indicatorHeightPx = LocalDensity.current.run { indicatorHeight.roundToPx() } + val spacingPx = LocalDensity.current.run { spacing.roundToPx() } + + Box( + modifier = modifier, + contentAlignment = Alignment.TopCenter + ) { + Column( + verticalArrangement = Arrangement.spacedBy(spacing), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val indicatorModifier = Modifier + .size(width = indicatorWidth, height = indicatorHeight) + .background(color = inactiveColor, shape = indicatorShape) + + repeat(pageCount) { + Box(indicatorModifier) + } + } + + Box( + Modifier + .offset { + val position = pageIndexMapping(pagerState.currentPage) + val offset = pagerState.currentPageOffset + val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt()) + val scrollPosition = ((next - position) * offset.absoluteValue + position) + .coerceIn( + 0f, + (pageCount - 1) + .coerceAtLeast(0) + .toFloat() + ) + + IntOffset( + x = 0, + y = ((spacingPx + indicatorHeightPx) * scrollPosition).toInt(), + ) + } + .size(width = indicatorWidth, height = indicatorHeight) + .then( + if (pageCount > 0) Modifier.background( + color = activeColor, + shape = indicatorShape, + ) + else Modifier + ) + ) + } +} \ No newline at end of file diff --git a/banner/src/main/java/com/zj/banner/utils/PagerTab.kt b/banner/src/main/java/com/zj/banner/utils/PagerTab.kt new file mode 100644 index 0000000..1522e75 --- /dev/null +++ b/banner/src/main/java/com/zj/banner/utils/PagerTab.kt @@ -0,0 +1,83 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package com.zj.banner.utils + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.material.ScrollableTabRow +import androidx.compose.material.TabPosition +import androidx.compose.material.TabRow +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.lerp + + +/** + * This indicator syncs up a [TabRow] or [ScrollableTabRow] tab indicator with a + * [androidx.compose.foundation.pager.HorizontalPager] or + * [androidx.compose.foundation.pager.VerticalPager]. + */ +fun Modifier.pagerTabIndicatorOffset( + pagerState: androidx.compose.foundation.pager.PagerState, + tabPositions: List, + pageIndexMapping: (Int) -> Int = { it }, +): Modifier { + val stateBridge = object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + + return pagerTabIndicatorOffset(stateBridge, tabPositions, pageIndexMapping) +} + +private fun Modifier.pagerTabIndicatorOffset( + pagerState: PagerStateBridge, + tabPositions: List, + pageIndexMapping: (Int) -> Int = { it }, +): Modifier = layout { measurable, constraints -> + if (tabPositions.isEmpty()) { + // If there are no pages, nothing to show + layout(constraints.maxWidth, 0) {} + } else { + val currentPage = minOf(tabPositions.lastIndex, pageIndexMapping(pagerState.currentPage)) + val currentTab = tabPositions[currentPage] + val previousTab = tabPositions.getOrNull(currentPage - 1) + val nextTab = tabPositions.getOrNull(currentPage + 1) + val fraction = pagerState.currentPageOffset + val indicatorWidth = if (fraction > 0 && nextTab != null) { + lerp(currentTab.width, nextTab.width, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.width, previousTab.width, -fraction).roundToPx() + } else { + currentTab.width.roundToPx() + } + val indicatorOffset = if (fraction > 0 && nextTab != null) { + lerp(currentTab.left, nextTab.left, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.left, previousTab.left, -fraction).roundToPx() + } else { + currentTab.left.roundToPx() + } + val placeable = measurable.measure( + Constraints( + minWidth = indicatorWidth, + maxWidth = indicatorWidth, + minHeight = 0, + maxHeight = constraints.maxHeight + ) + ) + layout(constraints.maxWidth, maxOf(placeable.height, constraints.minHeight)) { + placeable.placeRelative( + indicatorOffset, + maxOf(constraints.minHeight - placeable.height, 0) + ) + } + } +} + +internal interface PagerStateBridge { + val currentPage: Int + val currentPageOffset: Float +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2371b8b..5b52081 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { ext { compose_version = '1.5.0-beta03' - compose_compiler_version = '1.4.8' - kotlin_version = '1.8.22' + compose_compiler_version = '1.5.0' + kotlin_version = '1.9.0' } repositories { google()