All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.lt.compose_views.compose_pager.ComposePager.kt Maven / Gradle / Ivy

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