本篇首要介绍微信查看大图时渐入、渐出和下拉回来列表的作用,旨在经过此进程加深对于Compose动画手势理解和使用。

下面咱们先看下完成的详细作用:

在开端之前先理一下需求完成此作用的几个进程:

  • 正常的宫格形式展现小图
  • 正常的全屏展现大图
  • 点击小图逐步扩大直到显现彻底大图
  • 点击大图逐步缩放直到大图彻底消失
  • 下拉大图开端缩放位移直到大图彻底消失

那么下面就按照上述五个进程详细介绍下此功能全体的完成流程。

宫格形式

宫格形式比较简单,直接选用Compose供给的LazyVerticalGrid可组合项即可完成,图片来历IconFont❤️

val iconList = listOf(
    R.mipmap.icon1,
    R.mipmap.icon2,
    R.mipmap.icon3,
    R.mipmap.icon4,
    R.mipmap.icon5,
    R.mipmap.icon6,
    R.mipmap.icon7,
    R.mipmap.icon8,
    R.mipmap.icon9,
    R.mipmap.icon10,
    R.mipmap.icon11,
    R.mipmap.icon12,
    R.mipmap.icon13,
    R.mipmap.icon14
)
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(iconList.size) { index ->
        Image(
            painter = painterResource(id = iconList[index]),
            contentDescription = "",
            contentScale = ContentScale.FillWidth
        )
    }
}

显现宫格的代码就不必多加解释了,信任小伙伴们现已很熟悉了,此代码运转的作用如下:

【Jetpack Compose】仿微信查看大图渐入渐出效果

完成好宫格之后,下面开端完成点击展现大图的逻辑

大图形式

大图形式是经过点击宫格的item才会显现,所以咱们直接在上面item的点击事情中处理。

@Composable
fun BigImage() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0F, 0F, 0F, 1F))
    ) {
        Image(
            painter = painterResource(id = iconList[BigImageManager.currentClickCellIndex]),
            contentDescription = "",
            modifier = Modifier.fillMaxSize()
        )
    }
}

大图形式选用的是外层包裹一个充溢全屏布景色为全黑的Box,这儿布景色选用的是Color的RGBA形式,方便后续给透明度设置动画,Box内部便是一个Image,暂时设置充溢全屏,后续需求依据动画逐步改变宽高,它的paintResource从iconList中获取,index保存在BigImageManager中。

接着再看下宫格item的点击处理逻辑:

// 记载是否展现大图的状况
val showBigImageStatus = remember {
    mutableStateOf(false)
}
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(iconList.size) { index ->
        Image(
            painter = painterResource(id = iconList[index]),
            contentDescription = "",
            contentScale = ContentScale.FillWidth,
            modifier = Modifier.clickable {
                if (showBigImageStatus.value) {
                    return@clickable
                }
                BigImageManager.currentClickCellIndex = index
                showBigImageStatus.value = true
            }
        )
    }
}
if (showBigImageStatus.value) {
    BigImage()
}

上面代码需求注意的几点是:

  • 定义showBigImageStatus的State变量来保存当时是否展现大图形式的状况;
  • 在Image的点击事情中先记载点击的index,存入BigImageManager.currentClickCellIndex中,然后将showBigImageStatus置为true;
  • 最终依据showBigImageStatus状况展现BigImage大图。

此刻完成的作用见下图GIF

【Jetpack Compose】仿微信查看大图渐入渐出效果

完成到这一步时,仅仅简单的将小图点击后展现大图的逻辑给处理完了,并没有任何的动画完成,接下来将动画的部分弥补完好。

小图动画至大图

从小图至大图的动画的完成思路为:

  • 点击时先获取点击小图的坐标和巨细;
  • 点击之后将大图呈现在小图坐标方位,并且巨细和小图共同,大图的布景为彻底透明;
  • 动画进程中,大图逐步的从小图方位变为居中显现,巨细也变为全屏,布景从全透明变为黑色。

依据以上思路开端编码完成作用。

在Compose中想获取可组合项的坐标方位和巨细,能够经过onGloballyPositioned办法实时获取

// 记载列表item的巨细
val cellSize = remember {
    mutableStateOf(IntSize(0, 0))
}
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(iconList.size) { index ->
        Image(
            painter = painterResource(id = iconList[index]),
            contentDescription = "",
            contentScale = ContentScale.FillWidth,
            modifier = Modifier
                .clickable {
                    if (showBigImageStatus.value) {
                        return@clickable
                    }
                    BigImageManager.currentClickCellIndex = index
                    showBigImageStatus.value = true
                }
                .onGloballyPositioned {
                    val rect = it.boundsInRoot()
                    val offset = Offset(rect.left, rect.top)
                    BigImageManager.cellOffsetMap[index] = offset
                    cellSize.value = it.size
                }
        )
    }
}

这儿在Modifier.onGloballyPositioned办法中经过boundsInRoot()获取了Image的坐标方位,然后经过Size获取Image的巨细,将巨细保存在cellSize中,记载每个Image的方位保存在BigImageManager.cellOffsetMap中。

当咱们获取到item的巨细和详细方位之后,由小变大的动画就显得十分简单了:

  • 先处理下布景透明度的改变,由小变大时,透明度应该是从0变成1,由大变小时则相反,这样咱们就能够经过animateFloatAsState直接完成透明度的改变;
  • 再处理图片方位的动画,履行动画前图片应该是在小格口方位,履行完动画之后图片左上角坐标便是(0,0)方位,这样就能够选用特点动画结合State完成坐标方位的改变;
  • 最终处理图片巨细的动画,巨细动画和方位类似,最开端巨细为小格口的巨细,最终是屏幕的巨细。

下面看详细代码完成:

// 透明度是否逐步增大
val alphaIncrease = remember {
    mutableStateOf(false)
}
// 透明度动画,当showBigImageStatus为true也便是由小变大时,targetValue应该是1,反之则为0
// animationSpec设置的是2s的时长
val alpha = animateFloatAsState(
    targetValue = if (alphaIncrease.value) 1F else 0F,
    label = "",
    animationSpec = tween(BigImageManager.animatorDuration.toInt())
)
// 大图x轴的偏移量
val bigImageOffsetX = remember {
    mutableStateOf(0F)
}
// 大图y轴的偏移量
val bigImageOffsetY = remember {
    mutableStateOf(0F)
}
// 大图宽度
val bigImageSizeWidth = remember {
    mutableStateOf(0)
}
// 大图高度
val bigImageSizeHeight = remember {
    mutableStateOf(0)
}
items(iconList.size) { index ->
    Image(
        painter = painterResource(id = iconList[index]),
        contentDescription = "",
        contentScale = ContentScale.FillWidth,
        modifier = Modifier
            .clickable {
                if (showBigImageStatus.value) {
                    return@clickable
                }
                BigImageManager.currentClickCellIndex = index
                showBigImageStatus.value = true
              	alphaIncrease.value = true
                val currentOffset = BigImageManager.cellOffsetMap[index] ?: Offset(0F, 0F)
                animatorOfFloat(state = bigImageOffsetX, currentOffset.x, 0F)
                animatorOfFloat(state = bigImageOffsetY, currentOffset.y, 0F)
                animatorOfInt(
                    state = bigImageSizeWidth, cellSize.value.width,
                    getScreenWidth(context)
                )
                animatorOfInt(
                    state = bigImageSizeHeight, cellSize.value.height,
                    getScreenHeight(context)
                )
            }
            .onGloballyPositioned {
                val rect = it.boundsInRoot()
                val offset = Offset(rect.left, rect.top)
                BigImageManager.cellOffsetMap[index] = offset
                cellSize.value = it.size
            }
    )
}
fun animatorOfFloat(state: MutableState<Float>, vararg offset: Float, onEnd: () -> Unit = {}) {
    val valueAnimator = ValueAnimator.ofFloat(*offset)
    valueAnimator.duration = BigImageManager.animatorDuration
    valueAnimator.addUpdateListener {
        state.value = it.animatedValue as Float
    }
    valueAnimator.addListener(onEnd = { onEnd() })
    valueAnimator.start()
}
fun animatorOfInt(state: MutableState<Int>, vararg offset: Int, onEnd: () -> Unit = {}) {
    val valueAnimator = ValueAnimator.ofInt(*offset)
    valueAnimator.duration = BigImageManager.animatorDuration
    valueAnimator.addUpdateListener {
        state.value = it.animatedValue as Int
    }
    valueAnimator.addListener(onEnd = { onEnd() })
    valueAnimator.start()
}

上面逻辑就将大图的透明度、巨细和方位动画现已处理完成,下面再来看看大图中对这些值的引用。

@Composable
fun BigImage(
    bigImageSizeWidth: Int,
    bigImageSizeHeight: Int,
    bigImageOffsetX: Float,
    bigImageOffsetY: Float,
    alpha: Float,
    click: () -> Unit
) {
    val context = LocalContext.current
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0F, 0F, 0F, alpha))
    ) {
        Image(
            painter = painterResource(id = iconList[BigImageManager.currentClickCellIndex]),
            contentDescription = "",
            modifier = Modifier
                .size(
                    bigImageSizeWidth.toDp(context).dp,
                    bigImageSizeHeight.toDp(context).dp
                )
                .offset(bigImageOffsetX.toDp(context).dp, bigImageOffsetY.toDp(context).dp)
                .clickable { click() }
        )
    }
}

透明度值alpha直接赋给Box的background即可,然后将对应的巨细、宽高别离赋给Modifier.size()和Modifier.offset()。这儿要注意的一点便是,咱们动画履行进程中产生的值都是float和int型的px值,可是size和offset传入的都是Compose中Dp目标,所以咱们需求先将px转为dp值,再转成Dp目标。

这时候咱们再来看看由小格口变成大图的作用:

大图动画至小图

大图动画至小图的动画其实便是小图至大图相反的进程,只需求将透明度、巨细和方位动画反过来履行即可,这儿就不多解释详细的进程改变了,直接上代码片段。

if (showBigImageStatus.value) {
    BigImage(
        bigImageSizeWidth.value, bigImageSizeHeight.value, bigImageOffsetX.value,
        bigImageOffsetY.value, alpha.value, click = {
            if (!showBigImageStatus.value) {
                return@BigImage
            }
            alphaIncrease.value = false
            val currentCellOffset =
                BigImageManager.cellOffsetMap[BigImageManager.currentClickCellIndex]
                    ?: Offset(0F, 0F)
            animatorOfFloat(state = bigImageOffsetX, bigImageOffsetX.value, currentCellOffset.x)
            animatorOfFloat(state = bigImageOffsetY, bigImageOffsetY.value, currentCellOffset.y)
            animatorOfInt(
                state = bigImageSizeWidth,
                getScreenWidth(context),
                cellSize.value.width,
                onEnd = { showBigImageStatus.value = false })
            animatorOfInt(
                state = bigImageSizeHeight,
                getScreenHeight(context),
                cellSize.value.height,
                onEnd = { showBigImageStatus.value = false })
        }
    )
}

BigImage中Image的点击事情说到click参数中,然后在click中先将alpha标志方位为false,此刻透明度就会从1变为0,然后从BigImageManager.cellOffsetMap中取出对应小图的方位,最终经过特点动画和State的结合将巨细和方位的动画开端履行。

到这方位点击小图动画至大图和点击大图动画至小图作用都现已成型,再看下完成的作用:

【Jetpack Compose】仿微信查看大图渐入渐出效果

下拉缩放

下拉事情可经过Modifier.pointerInput()办法监听,还是和点击事情相同,将下拉事情说到参数中,整个进程中只需求重视下拉开端、下拉完毕和下拉的进程,撤销事情暂时忽略不计。

.pointerInput(null) {
    detectDragGestures(
        onDragStart = onDraStart,
        onDragEnd = onDragEnd,
        onDrag = onDrag
    )
}

下拉进程中要记载两个状况,一是下拉的状况表示当时是否处于下拉状况,二是下拉进程中的透明度,都是经过State来记载

// 下拉状况
val dragStatus = remember {
    mutableStateOf(false)
}
// 下拉时透明度
val dragAlpha = remember {
    mutableStateOf(1F)
}

接下来处理下拉进程中巨细、方位和透明度的改变

onDrag = { _, dragAmount ->
    val offsetX = bigImageOffsetX.value
    val offsetY = bigImageOffsetY.value
    // 上滑暂时不处理
    if (offsetY < 0) {
        return@BigImage
    }
    bigImageOffsetX.value = offsetX + dragAmount.x
    bigImageOffsetY.value = offsetY + dragAmount.y
    val scale = 1 - (offsetY + dragAmount.y) / (getScreenHeight(context) / 2)
    if (scale > 0.5F) {
        dragAlpha.value = scale
        BigImageManager.dragEndAlpha = scale
        bigImageSizeWidth.value = (getScreenWidth(context) * scale).toInt()
        bigImageSizeHeight.value = (getScreenHeight(context) * scale).toInt()
    }
}
  • 下拉进程中先获取大图当时的方位x和y
  • 然后将x和y方位依据下拉的偏移量作出改变
  • 接着获取下拉的一个系数,这儿将0.5作为最小缩放系数,这儿的系数是依据下拉的偏移量和屏幕全体高度的一半计算得出
  • 依据下拉缩放系数计算出透明度值和宽高的值,每次都将下拉缩放系数保存至BigImageManager.draEndAlpha中,这个值在松手时会使用到。

到这就将下拉进程中大图的改变处理完成了,最终处理下下拉开端和下拉完毕事情就完成整个下拉事情了。

onDraStart = { _ -> dragStatus.value = true },
onDragEnd = {
    val currentCellOffset =
        BigImageManager.cellOffsetMap[BigImageManager.currentClickCellIndex]
            ?: Offset(0F, 0F)
    animatorOfFloat(state = bigImageOffsetX, bigImageOffsetX.value, currentCellOffset.x)
    animatorOfFloat(state = bigImageOffsetY, bigImageOffsetY.value, currentCellOffset.y)
    animatorOfInt(
        state = bigImageSizeWidth,
        bigImageSizeWidth.value,
        cellSize.value.width,
        onEnd = { showBigImageStatus.value = false })
    animatorOfInt(
        state = bigImageSizeHeight,
        bigImageSizeHeight.value,
        cellSize.value.height,
        onEnd = { showBigImageStatus.value = false })
    animatorOfFloat(state = dragAlpha, BigImageManager.dragEndAlpha, 0F, onEnd = {
        dragStatus.value = false
        alphaIncrease.value = false
    })
},

下拉开端事情中只需求将dragStatus置为true,下拉完毕事情的处理逻辑和点击大图的逻辑根本共同,仅仅点击大图时巨细的改变是从充溢全屏的宽高开端,而下拉完毕时巨细是松手时的巨细,松手时的巨细记载在bigImageSizeWidth和bigImageSizeHeight中,所以大图巨细动画的起始点便是这两个值,能够和click事情中动画对比下。

最终咱们看下全体的作用

【Jetpack Compose】仿微信查看大图渐入渐出效果

比较于原生的完成,个人感觉Compose完成查看大图的渐入渐出动画完成愈加简单,得益于Compose的呼应式和animate*AsState,下面是整个作用的代码,感兴趣的小伙伴能够自己运转下代码领会领会全体作用,感谢大家的阅读


object BigImageManager {
    // 动画时长
    const val animatorDuration = 2000L
    // 记载当时点击宫格的index
    var currentClickCellIndex = 0
    // 记载所有宫格的方位
    var cellOffsetMap = mutableMapOf<Int, Offset>()
    // 记载下拉松手时大图的透明度值
    var dragEndAlpha = 0F
}
val iconList = listOf(
    R.mipmap.icon1,
    R.mipmap.icon2,
    R.mipmap.icon3,
    R.mipmap.icon4,
    R.mipmap.icon5,
    R.mipmap.icon6,
    R.mipmap.icon7,
    R.mipmap.icon8,
    R.mipmap.icon9,
    R.mipmap.icon10,
    R.mipmap.icon11,
    R.mipmap.icon12,
    R.mipmap.icon13,
    R.mipmap.icon14
)
@Composable
fun BigImageScaffold() {
    val context = LocalContext.current
    // 记载是否展现大图的状况
    val showBigImageStatus = remember {
        mutableStateOf(false)
    }
    // 记载列表item的巨细
    val cellSize = remember {
        mutableStateOf(IntSize(0, 0))
    }
    // 透明度是否逐步增大
    val alphaIncrease = remember {
        mutableStateOf(false)
    }
    // 透明度动画,当showBigImageStatus为true也便是由小变大时,targetValue应该是1,反之则为0
    // animationSpec设置的是1s的时长
    val alpha = animateFloatAsState(
        targetValue = if (alphaIncrease.value) 1F else 0F,
        label = "",
        animationSpec = tween(BigImageManager.animatorDuration.toInt())
    )
    // 大图x轴的偏移量
    val bigImageOffsetX = remember {
        mutableStateOf(0F)
    }
    // 大图y轴的偏移量
    val bigImageOffsetY = remember {
        mutableStateOf(0F)
    }
    // 大图宽度
    val bigImageSizeWidth = remember {
        mutableStateOf(0)
    }
    // 大图高度
    val bigImageSizeHeight = remember {
        mutableStateOf(0)
    }
    // 下拉状况
    val dragStatus = remember {
        mutableStateOf(false)
    }
    // 下拉时透明度
    val dragAlpha = remember {
        mutableStateOf(1F)
    }
    LazyVerticalGrid(columns = GridCells.Fixed(3)) {
        items(iconList.size) { index ->
            Image(
                painter = painterResource(id = iconList[index]),
                contentDescription = "",
                contentScale = ContentScale.FillWidth,
                modifier = Modifier
                    .clickable {
                        if (showBigImageStatus.value) {
                            return@clickable
                        }
                        BigImageManager.currentClickCellIndex = index
                        showBigImageStatus.value = true
                        alphaIncrease.value = true
                        val currentOffset = BigImageManager.cellOffsetMap[index] ?: Offset(0F, 0F)
                        animatorOfFloat(state = bigImageOffsetX, currentOffset.x, 0F)
                        animatorOfFloat(state = bigImageOffsetY, currentOffset.y, 0F)
                        animatorOfInt(
                            state = bigImageSizeWidth, cellSize.value.width,
                            getScreenWidth(context)
                        )
                        animatorOfInt(
                            state = bigImageSizeHeight, cellSize.value.height,
                            getScreenHeight(context)
                        )
                    }
                    .onGloballyPositioned {
                        val rect = it.boundsInRoot()
                        val offset = Offset(rect.left, rect.top)
                        BigImageManager.cellOffsetMap[index] = offset
                        cellSize.value = it.size
                    }
            )
        }
    }
    if (showBigImageStatus.value) {
        BigImage(
            bigImageSizeWidth.value, bigImageSizeHeight.value, bigImageOffsetX.value,
            bigImageOffsetY.value, if (dragStatus.value) dragAlpha.value else alpha.value,
            click = {
                if (!showBigImageStatus.value) {
                    return@BigImage
                }
                alphaIncrease.value = false
                val currentCellOffset =
                    BigImageManager.cellOffsetMap[BigImageManager.currentClickCellIndex]
                        ?: Offset(0F, 0F)
                animatorOfFloat(state = bigImageOffsetX, bigImageOffsetX.value, currentCellOffset.x)
                animatorOfFloat(state = bigImageOffsetY, bigImageOffsetY.value, currentCellOffset.y)
                animatorOfInt(
                    state = bigImageSizeWidth,
                    getScreenWidth(context),
                    cellSize.value.width,
                    onEnd = { showBigImageStatus.value = false })
                animatorOfInt(
                    state = bigImageSizeHeight,
                    getScreenHeight(context),
                    cellSize.value.height,
                    onEnd = { showBigImageStatus.value = false })
            },
            onDraStart = { _ -> dragStatus.value = true },
            onDragEnd = {
                val currentCellOffset =
                    BigImageManager.cellOffsetMap[BigImageManager.currentClickCellIndex]
                        ?: Offset(0F, 0F)
                animatorOfFloat(state = bigImageOffsetX, bigImageOffsetX.value, currentCellOffset.x)
                animatorOfFloat(state = bigImageOffsetY, bigImageOffsetY.value, currentCellOffset.y)
                animatorOfInt(
                    state = bigImageSizeWidth,
                    bigImageSizeWidth.value,
                    cellSize.value.width,
                    onEnd = { showBigImageStatus.value = false })
                animatorOfInt(
                    state = bigImageSizeHeight,
                    bigImageSizeHeight.value,
                    cellSize.value.height,
                    onEnd = { showBigImageStatus.value = false })
                animatorOfFloat(state = dragAlpha, BigImageManager.dragEndAlpha, 0F, onEnd = {
                    dragStatus.value = false
                    alphaIncrease.value = false
                })
            },
            onDrag = { _, dragAmount ->
                val offsetX = bigImageOffsetX.value
                val offsetY = bigImageOffsetY.value
                // 上滑暂时不处理
                if (offsetY < 0) {
                    return@BigImage
                }
                bigImageOffsetX.value = offsetX + dragAmount.x
                bigImageOffsetY.value = offsetY + dragAmount.y
                val scale = 1 - (offsetY + dragAmount.y) / (getScreenHeight(context) / 2)
                if (scale > 0.5F) {
                    dragAlpha.value = scale
                    BigImageManager.dragEndAlpha = scale
                    bigImageSizeWidth.value = (getScreenWidth(context) * scale).toInt()
                    bigImageSizeHeight.value = (getScreenHeight(context) * scale).toInt()
                }
            }
        )
    }
}
@Composable
fun BigImage(
    bigImageSizeWidth: Int,
    bigImageSizeHeight: Int,
    bigImageOffsetX: Float,
    bigImageOffsetY: Float,
    alpha: Float,
    click: () -> Unit,
    onDraStart: (Offset) -> Unit,
    onDragEnd: () -> Unit,
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
    val context = LocalContext.current
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0F, 0F, 0F, alpha))
    ) {
        Image(
            painter = painterResource(id = iconList[BigImageManager.currentClickCellIndex]),
            contentDescription = "",
            modifier = Modifier
                .size(
                    bigImageSizeWidth.toDp(context).dp,
                    bigImageSizeHeight.toDp(context).dp
                )
                .offset(bigImageOffsetX.toDp(context).dp, bigImageOffsetY.toDp(context).dp)
                .clickable { click() }
                .pointerInput(null) {
                    detectDragGestures(
                        onDragStart = onDraStart,
                        onDragEnd = onDragEnd,
                        onDrag = onDrag
                    )
                }
        )
    }
}
fun animatorOfFloat(state: MutableState<Float>, vararg offset: Float, onEnd: () -> Unit = {}) {
    val valueAnimator = ValueAnimator.ofFloat(*offset)
    valueAnimator.duration = BigImageManager.animatorDuration
    valueAnimator.addUpdateListener {
        state.value = it.animatedValue as Float
    }
    valueAnimator.addListener(onEnd = { onEnd() })
    valueAnimator.start()
}
fun animatorOfInt(state: MutableState<Int>, vararg offset: Int, onEnd: () -> Unit = {}) {
    val valueAnimator = ValueAnimator.ofInt(*offset)
    valueAnimator.duration = BigImageManager.animatorDuration
    valueAnimator.addUpdateListener {
        state.value = it.animatedValue as Int
    }
    valueAnimator.addListener(onEnd = { onEnd() })
    valueAnimator.start()
}
fun getScreenWidth(context: Context): Int {
    val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
    return wm?.defaultDisplay?.width ?: 0
}
fun getScreenHeight(context: Context): Int {
    val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
    return wm?.defaultDisplay?.height ?: 0
}
fun Int.toDp(context: Context): Int {
    val density = context.resources.displayMetrics.density
    return (this / density + 0.5).toInt()
}
fun Float.toDp(context: Context): Int {
    val density = context.resources.displayMetrics.density
    return (this / density + 0.5).toInt()
}

关于我

我是Taonce,如果觉得本文对你有所协助,帮助重视、赞或许收藏三连一下,谢谢~