commonMain.com.lt.compose_views.compose_pager.ComposePager.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ComposeViews-desktop Show documentation
Show all versions of ComposeViews-desktop Show documentation
Jatpack(JetBrains) Compose views
The newest version!
/*
* Copyright lt 2023
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lt.compose_views.compose_pager
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
import com.lt.compose_views.banner.BannerScope
import com.lt.compose_views.util.DragInteractionSource
import com.lt.compose_views.util.midOf
import com.lt.compose_views.util.rememberMutableStateOf
import com.lt.compose_views.util.runIf
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
/**
* creator: lt 2022/6/25 [email protected]
* effect : 类似于安卓中的ViewPager
* Equivalent to the ViewPager in android
* warning:
* @param pageCount 一共有多少页
* Sum page count
* @param modifier 修饰
* @param composePagerState ComposePager的状态
* ComposePager's state
* @param orientation 滑动的方向
* Scroll orientation
* @param userEnable 用户是否可以滑动,等于false时用户滑动无反应,但代码可以执行翻页
* Whether the user can scroll
* @param pageCache 左右两边的页面缓存,默认左右各缓存1页,但不能少于1页(不宜过大)
* The number of pagers cached on the left and right sides
* @param scrollableInteractionSource 滚动状态监听,可以用来监听:用户开始(结束,取消)滑动等事件
* Scroll state monitor
* @param pagerKey 使用key来提高性能,减少重组,效果等同于[LazyColumn#items#key]
* Using key to improve performance, reduce recombination, and achieve the same effect as [LazyColumn#items#key]
* @param clip 是否对内容区域进行裁剪
* Whether to crop the content area
* @param contentTransformation 变换ComposePager的Content
* Transform the Content of ComposePager
* @param content compose内容区域
* Content of compose
*/
@Composable
fun ComposePager(
pageCount: Int,
modifier: Modifier = Modifier,
composePagerState: ComposePagerState = rememberComposePagerState(),
orientation: Orientation = Orientation.Horizontal,
userEnable: Boolean = true,
pageCache: Int = 1,
scrollableInteractionSource: DragInteractionSource? = null,
pagerKey: (index: Int) -> Any = { it },
clip: Boolean = true,
contentTransformation: PagerContentTransformation = NoPagerContentTransformation,
content: @Composable ComposePagerScope.() -> Unit
) {
val indexToKey = LocalIndexToKey.current
//key和content的缓存位置
val contentList = remember {
mutableStateListOf()
}
//content最大缓存的数量
val maxContent by remember(key1 = pageCache) {
mutableStateOf(pageCache * 2 + 1)
}
//下一个要被替换的content缓存的索引
var nextContentReplaceIndex by remember(key1 = pageCache, key2 = pageCount) {
mutableStateOf(null)
}
var isNextPage by rememberMutableStateOf { PageChangeAnimFlag.Reduction }
val coroutineScope = rememberCoroutineScope()
//检查索引是否在页数内
remember(key1 = pageCount) {
if (pageCount <= 0) {
} else if (pageCount <= composePagerState.getCurrSelectIndex()) {
composePagerState.currSelectIndex.value = pageCount - 1
composePagerState.setPageIndex(pageCount - 1)
}
0
}
if (pageCount <= composePagerState.getCurrSelectIndex())
return
//初始化content
remember(
key1 = pageCount,
key2 = pageCache,
) {
initContentList(
composePagerState,
pageCache,
indexToKey,
pagerKey,
contentList,
pageCount,
content
)
0
}
//放置的compose元素的content
remember(
key1 = pageCount,
key2 = isNextPage,
key3 = pageCache,
) {
if (isNextPage is PageChangeAnimFlag.GoToPageNotAnim || isNextPage is PageChangeAnimFlag.GoToPageWithAnim) {
initContentList(
composePagerState,
pageCache,
indexToKey,
pagerKey,
contentList,
pageCount,
content
)
nextContentReplaceIndex = null
}
if (isNextPage == PageChangeAnimFlag.Next) {
val currIndex = nextContentReplaceIndex?.let {
if (it >= maxContent - 1)
0
else
it + 1
} ?: 0
val index = composePagerState.getCurrSelectIndex() + pageCache
val key = indexToKey(index)
contentList[currIndex] = ComposePagerContentBean(
key,
getPagerKey(pagerKey, pageCount, key),
Modifier.layoutId(index),
ComposePagerScope(key, index)
) { mModifier, mScope ->
if (key < 0 || key >= pageCount)
Box(modifier = Modifier)
else {
Box(modifier = mModifier) {
mScope.content()
}
}
}
nextContentReplaceIndex = currIndex
} else if (isNextPage == PageChangeAnimFlag.Prev) {
val currIndex = nextContentReplaceIndex ?: (maxContent - 1)
val index = composePagerState.getCurrSelectIndex() - pageCache
val key = indexToKey(index)
contentList[currIndex] = ComposePagerContentBean(
key,
getPagerKey(pagerKey, pageCount, key),
Modifier.layoutId(index),
ComposePagerScope(key, index)
) { mModifier, mScope ->
if (key < 0 || key >= pageCount)
Box(modifier = Modifier)
else {
Box(modifier = mModifier) {
mScope.content()
}
}
}
nextContentReplaceIndex = if (currIndex <= 0)
maxContent - 1
else
currIndex - 1
}
isNextPage = PageChangeAnimFlag.Reduction
0
}
val minOffset = remember(
key1 = composePagerState.mainAxisSize,
key2 = composePagerState.currSelectIndex.value,
key3 = pageCount
) {
val currIndex = composePagerState.currSelectIndex.value
if (currIndex + 1 >= pageCount)
currIndex * -composePagerState.mainAxisSize.toFloat()
else
(currIndex + 1) * -composePagerState.mainAxisSize.toFloat()
}
val maxOffset = remember(
key1 = composePagerState.mainAxisSize,
key2 = composePagerState.currSelectIndex.value,
) {
val currIndex = composePagerState.currSelectIndex.value
if (currIndex <= 0)
0f
else
(currIndex - 1) * -composePagerState.mainAxisSize.toFloat()
}
//滑动监听
val scrollableState = rememberScrollableState {
if (!userEnable) return@rememberScrollableState it//fix desktop bug
//停止之前的动画
composePagerState.pageChangeAnimFlag = null
val lastOffset = composePagerState.offsetAnim.value
val offset = midOf(minOffset, lastOffset + it, maxOffset)
coroutineScope.launch {
composePagerState.offsetAnim.snapTo(offset)
}
offset - lastOffset
}
//处理翻页动画
LaunchedEffect(
key1 = composePagerState.pageChangeAnimFlag,
block = {
val flag = composePagerState.pageChangeAnimFlag
if (flag == null) {
if (composePagerState.offsetAnim.isRunning)
composePagerState.offsetAnim.stop()
return@LaunchedEffect
}
var returnPageChangeAnimFlag: PageChangeAnimFlag? = null
try {
val index = composePagerState.currSelectIndex.value
when (flag) {
PageChangeAnimFlag.Prev -> {
if (index <= 0) {
composePagerState.offsetAnim.animateTo(-index * composePagerState.mainAxisSize.toFloat())
return@LaunchedEffect
}
try {
composePagerState.offsetAnim.animateTo(-(index - 1) * composePagerState.mainAxisSize.toFloat())
} finally {
composePagerState.currSelectIndex.value = index - 1
isNextPage = PageChangeAnimFlag.Prev
}
}
PageChangeAnimFlag.Next -> {
if (index + 1 >= pageCount) {
composePagerState.offsetAnim.animateTo(-index * composePagerState.mainAxisSize.toFloat())
return@LaunchedEffect
}
try {
composePagerState.offsetAnim.animateTo(-(index + 1) * composePagerState.mainAxisSize.toFloat())
} finally {
composePagerState.currSelectIndex.value = index + 1
isNextPage = PageChangeAnimFlag.Next
}
}
PageChangeAnimFlag.Reduction -> {
composePagerState.offsetAnim.animateTo(-index * composePagerState.mainAxisSize.toFloat())
}
is PageChangeAnimFlag.GoToPageNotAnim -> {
composePagerState.currSelectIndex.value = flag.index
composePagerState.offsetAnim.snapTo(-flag.index * composePagerState.mainAxisSize.toFloat())
isNextPage = flag
}
is PageChangeAnimFlag.GoToPageWithAnim -> {
val (goToIndex, pageChangeAnimFlag) = if (flag.index > composePagerState.currSelectIndex.value)
flag.index - 1 to PageChangeAnimFlag.Next
else
flag.index + 1 to PageChangeAnimFlag.Prev
composePagerState.currSelectIndex.value = goToIndex
composePagerState.offsetAnim.snapTo(-goToIndex * composePagerState.mainAxisSize.toFloat())
returnPageChangeAnimFlag = pageChangeAnimFlag
isNextPage = flag
}
}
} finally {
composePagerState.pageChangeAnimFlag = returnPageChangeAnimFlag
}
})
//测量和放置compose元素
Layout(
content = {
contentList.forEach {
key(it.key) {
it.function(
contentTransformation.transformation(composePagerState, it.paramScope, it.paramModifier),
it.paramScope,
)
}
}
},
modifier = modifier
.scrollable(
state = scrollableState,
orientation = orientation,
enabled = userEnable,
interactionSource = scrollableInteractionSource,
flingBehavior = remember(orientation) {
object : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val index = composePagerState.currSelectIndex.value
if (composePagerState.offsetAnim.value + initialVelocity > -(index * composePagerState.mainAxisSize - composePagerState.mainAxisSize / 2)) {
composePagerState.pageChangeAnimFlag = PageChangeAnimFlag.Prev
} else if (composePagerState.offsetAnim.value + initialVelocity < -(index * composePagerState.mainAxisSize + composePagerState.mainAxisSize / 2)) {
composePagerState.pageChangeAnimFlag = PageChangeAnimFlag.Next
} else {
composePagerState.pageChangeAnimFlag = PageChangeAnimFlag.Reduction
}
return 0f//返回剩余的速度
}
}
})
.runIf(clip) { clipScrollableContainer(orientation) }
.onSizeChanged {
if (composePagerState.size != it) {
composePagerState.size = it
//大小变更时,滚动到正确的位置
coroutineScope.launch {
composePagerState.setOffset(0f)
}
}
}
) { measurableList/* 可测量的(子控件) */, constraints/* 约束条件 */ ->
val selectIndex = composePagerState.currSelectIndex.value
var width = 0
var height = 0
//测量子元素,并算出他们的最大宽度
val placeableList = measurableList
.filter {
//只测量有效的布局
val key = it.layoutId
key is Int && abs(key - selectIndex) <= pageCache
}
.map {
val key = it.layoutId as Int
val placeable = it.measure(constraints)
width = maxOf(width, placeable.width)
height = maxOf(height, placeable.height)
key to placeable
}
composePagerState.mainAxisSize =
if (orientation == Orientation.Horizontal) width else height
//设置自身大小,并布局子元素
layout(width, height) {
val animValue = composePagerState.offsetAnim.value.roundToInt()
placeableList.forEach { (index, placeable) ->
val offset = index * composePagerState.mainAxisSize + animValue
//遍历放置子元素
if (orientation == Orientation.Horizontal)
placeable.placeRelative(
x = offset,
y = 0
)//placeRelative可以适配从右到左布局的放置子元素,place只适用于从左到右的布局
else
placeable.placeRelative(
x = 0,
y = offset
)
}
}
}
}
//初始化ContentList
private fun initContentList(
composePagerState: ComposePagerState,
pageCache: Int,
indexToKey: (index: Int) -> Int,
pagerKey: (index: Int) -> Any,
contentList: MutableList,
pageCount: Int,
content: @Composable (ComposePagerScope.() -> Unit)
) {
contentList.clear()
//当前索引
val selectIndex = composePagerState.currSelectIndex.value
//key的集合: key to index
val keyList = (selectIndex - pageCache).rangeTo(selectIndex + pageCache)
.map { indexToKey(it) to it }
//特殊处理一下banner中数量过少的问题,保证至少有三条
when (keyList.size) {
1 -> {
val (key, value) = keyList.first()
repeat(3) {
contentList.add(ComposePagerContentBean(
key,
getPagerKey(pagerKey, pageCount, key),
Modifier.layoutId(value + it - 2),
ComposePagerScope(key, value + it - 2)
) { mModifier, mScope ->
if (key < 0 || key >= pageCount)
Box(modifier = Modifier)
else {
Box(modifier = mModifier) {
mScope.content()
}
}
})
}
}
2 -> {
val cacheList = ArrayList(4)
repeat(2) {
keyList.forEach { (key, value) ->
cacheList.add(ComposePagerContentBean(
key,
getPagerKey(pagerKey, pageCount, key),
Modifier.layoutId((value - it * 2)),
ComposePagerScope(key, (value - it * 2))
) { mModifier, mScope ->
if (key < 0 || key >= pageCount)
Box(modifier = Modifier)
else {
Box(modifier = mModifier) {
mScope.content()
}
}
})
}
}
contentList.addAll(cacheList.reversed())
}
else -> {
//创建或修改缓存
keyList.forEach { (key, value) ->
contentList.add(ComposePagerContentBean(
key,
getPagerKey(pagerKey, pageCount, key),
Modifier.layoutId(value),
ComposePagerScope(key, value)
) { mModifier, mScope ->
if (key < 0 || key >= pageCount)
Box(modifier = Modifier)
else {
Box(modifier = mModifier) {
mScope.content()
}
}
})
}
}
}
}
//由于有pageCache的存在,所以需要处理越界问题
private fun getPagerKey(
pagerKey: (index: Int) -> Any,
pageCount: Int,
index: Int,
): Any {
return if (index < 0 || index >= pageCount)
index
else
pagerKey(index)
}
//通过当前index确定pager的index,用来保存和复用content
internal val LocalIndexToKey = compositionLocalOf<(index: Int) -> Int> { { it } }
//应该不会有人这样用吧...
@Composable
fun BannerScope.InnerComposePager(
pageCount: Int,
modifier: Modifier = Modifier,
composePagerState: ComposePagerState = rememberComposePagerState(),
orientation: Orientation = Orientation.Horizontal,
userEnable: Boolean = true,
pageCache: Int = 1,
pagerKey: (index: Int) -> Any = { it },
content: @Composable ComposePagerScope.() -> Unit
) {
CompositionLocalProvider(LocalIndexToKey provides { it }) {
ComposePager(
pageCount = pageCount,
modifier = modifier,
composePagerState = composePagerState,
orientation = orientation,
userEnable = userEnable,
pageCache = pageCache,
pagerKey = pagerKey,
content = content,
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy