最近在学习Jetpack Compose
,想着能否用Jetpack Compose
实现微信一些重要界面以及功能。好消息是已经实现了微信聊天界面相关功能以及交互,最近又搞了搞朋友圈的整体交互,网上看了看,关于compose动画相关知识比较少,所以打算通过最近学习的compose手势动画相关知识实现该功能。
本文主要讲述如何通过compose手势动画实现微信大图缩放、切换、预览功能。
先上动图
在实现上述功能时首先我们需要了解一下 Compose
为我们公积金提供的一手势舞教程视频慢动作些手势动画。
使用P公司让员工下班发手机电量截图ointerInput Modifier
对于所有手势操作的处理都需要封装到这个 Modifier
中,我们知google服务框架道 Modifier 时用来修饰 UI 组件的,Go所以将手势操作的处理封装在 Modifier 符合开发者设计直觉,这同时也做到了手势处理逻辑与 UI 视图的解耦,从而提高复用性。
Modifier 为我们提供了很多手势事件,比如:Transformer Mogithub官网登陆入口difier
、Dr工商银行aggable Modifier
、Ro枸杞tation Modifier
以及滚动手势舞视频事件
、点击事件
等等都能看到PointerInput Modifier动画专业
的身影。因为这类上层的手势处理 Modifier 都是基于基础Modifier.p动画ointInput()
来实现的,所以自定义手势必然要在这个 Modifier 中进行。手势
//Transformer Modifier fun Modifier.transformable( state: TransformableState, lockRotationOnZoomPan: Boolean = false, enabled: Boolean = true ) = composed( factory = { ... if (enabled) Modifier.pointerInput(Unit, block) else Modifier }, ) //Draggable Modifier internal fun Modifier.draggable( stateFactory: @Composable () -> PointerAwareDraggableState, canDrag: (PointerInputChange) -> Boolean, orientation: Orientation, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, startDragImmediately: () -> Boolean, onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}, onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}, reverseDirection: Boolean = false ): Modifier = composed( ) { ... Modifier.pointerInput(orientation, enabled, reverseDirection) { ... } }
通过动画片少儿小猪佩奇 PointerInput Modifier
实现我们可以看出,我们所定义的自定义手势处理流程均发生在 PointerInputScope
中,suspend 关键字也告知我们自定义手势处理流程是发生在协程中。这其实是无可厚非的,在探索重组工作原理的过程中我们也经常能够看到协程的身影。
fun Modifier.pointerInput( key1: Any?, block: suspend PointerInputScope.() -> Unit ): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "pointerInput" properties["key1"] = key1 properties["block"] = block } ) { val density = LocalDensity.current val viewConfiguration = LocalViewConfiguration.current remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply { val filter = this LaunchedEffect(this, key1) { filter.coroutineScope = this block() } } }
接下来我们重点看看 PointerI手势密码nputScope
作用域,本文将着重解释一下部分我们用到的API,有想了解更全的可以移步大神文章:# 使用Jetpack C手势密码ompose完成自定义手势处理。
点击类型基础 API
API介绍
API名称 | 作用 |
---|---|
detectTapGestures | 监听点击手势 |
我们知道,Clickable Modifier
是c手势舞教程视频慢动作ompose给我们提供的单击事件,
与Clickable Modifier
不同的是,detectTapGestures
可以监听更多的点击事件。作为手机监听的动画头像基础 API,必然不会存在Clickable Modifier
所拓展的涟漪效果。
detectTapGestures包括四个函数回调,分别为:
-
onDoubleTap (可选):双击时回调
-
onLongPress (可选):长google谷歌搜索主页按时回调
-
onPress (可选):按下时回调
-
onTap (可选):轻触时回调手势识别
suspend fun PointerInputScope.detectTapGestures( onDoubleTap: ((Offset) -> Unit)? = null, onLongPress: ((Offset) -> Unit)? = null, onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture, onTap: ((Offset) -> Unit)? = null ) = coroutineScope { ... }
Tips
onPress 普通按下事件
onDoubleTap 前必定会动画先回调 2 次 Press
onLongPress 前必定会先github中文社区回调 1 次 Press(时间长)googleplay安卓版下载
onTap 前必定会先回调 1 次 Press(时间github开放私库短)
例子如下:
@Composable fun TapGestureSample() { var boxSize = 100.dp Box( Modifier.fillMaxSize() ) { Text(text = "detectTapGesturest监听点击手势", fontSize = 30.sp) Text( text = "", fontSize = 16.sp, modifier = Modifier.align(Alignment.BottomCenter) ) Box( Modifier .size(boxSize) .align(Alignment.Center) .background(Color.Green) .pointerInput(Unit) { detectTapGestures( onDoubleTap = { offset: Offset -> //双击时回调 println("detectTapGestures obDoubleTap[双击时回调] offset:$offset") }, onLongPress = { offset: Offset -> //长按时回调 println("detectTapGestures onLongPress[长按时回调] offset:$offset") }, onPress = { offset: Offset -> //按下时回调 println("detectTapGestures onPress[按下时回调] offset:$offset") }, onTap = { offset: Offset -> //轻触时回调 println("detectTapGestures onTap[轻触时回调] offset:$offset") } ) } ) } }
将上述例子运行一下就明白了,此处就github下载不录gif了。
手势检测
tr公积金ansformable 修饰符
接下来我们通过rememberTransformableState
检测用于平移、缩放和旋转的多点触控手势,我们可以使用transformable
修饰符。此修google服务框架饰符本身不会转换元素,只会检测手势。
remgithub是什么emberTransformableSgoogletate
内部是通过协程作用域来事实检测触控手势改变的。
例子如下:
@Composable fun TransformableSample() { // set up all transformation states var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange rotation += rotationChange offset += offsetChange } Box( Modifier // apply other transformations like rotation and zoom // on the pizza slice emoji .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) // add transformable to listen to multitouch transformation events // after offset .transformable(state = state) .background(Color.Blue) .fillMaxSize() ) }
需要部分如下依赖
def accompanist_version = "0.24.3-alpha" //compose的viewpager库 implementation "com.google.accompanist:accompanist-pager:$accompanist_version" //指示器 implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" //CoilImage是google推荐我们去使用的加载网络图片的开源库 implementation "com.google.accompanist:accompanist-coil:0.13.0"
功能实现
我们回到我们的项目中, 如上图所示,我们拆分一下该功能的实现。
-
1: 实现图片横向水平滚动
HorizontalPager
-
2: 底部的水平切换的指示器:
HorizontalPagerI手势识别ndicator
-
3: 双击放大和缩小
-
4龚俊: 双指缩放
-
5: 图片如有动画制作软件放大,切换时放大图还原至原始大动画头像小
HorizontalPagergithub官网
H动画电影orizontalPager
是其中一种布局,他将所有子项摆手势舞教程视频慢动作放在一条水平行上,允许用户在子项之间水平滑动。

/** * 界面状态变更 */ val pageState = rememberPagerState(initialPage = currentIndex) HorizontalPager( count = 图片数量, state = pageState, //图片状态 contentPadding = PaddingValues(horizontal = 0.dp), //图片间的间距 modifier = Modifier.fillMaxSize() ) { page -> println("ImageBrowserItem current page: $page") ImageBrowserItem(images[page], page, this) }
如果你想跳转到某一个特定页面,你可工商银行以在 CorouGitHubtineScope
中选择使用
rememberPagerState(initialPage = currentIndex)
或
pagerState.scrollToPage(index)
、 pagerState.animateScrollToPage(index)
选一即可。
HorizontalPagerIndicator
Horizontal工龄差一年工资差多少PagerIndicator
用来标识 HorizontalPager
或 Vegithub开放私库rtigithub中文官网网页calPager
的水平布局指示动画器,表示当前活动页面和使用 Shape 绘制Go的总页面。需要通过pageState绑定。
HorizontalPagerIndicator( pagerState = pageState, //需要通过pageState绑定 activeColor = Color.White, inactiveColor = WeComposeTheme.colors.onBackground, modifier = Modifier .align(Alignment.BottomCenter) .padding(60.dp) )
双击放大和缩小
对于我们要实现的双击事件来说,当双击时获取到已经缩放的scale
,,则将当前图片缩放至原始图的两倍,也就是双击放大两倍,再次双击还原到原图大github开放私库小,并且偏移量O动画ffset
恢复到中心点位置。如下部分代码:
... Modifier.pointerInput(Unit) { detectTapGestures( onDoubleTap = { println("ImageBrowserItem detectTapGestures onDoubleTap offset: $it") scale = if (scale <= 1f) { 2f } else { 1f } offset = Offset.Zero }, onTap = { } )
双指缩手势变化放
对于我们要实现的双指缩放来说,我们只需要处理缩放大小即可。当我们监听remembGitHuberTransformableState
变换时,s手势舞cale
放大的5倍时就停止继续放大。
如下部分代码:
/** * 监听手势状态变换 */ var state = rememberTransformableState(onTransformation = { zoomChange, panChange, rotationChange -> scale = (zoomChange * scale).coerceAtLeast(1f) scale = if (scale > 5f) { 5f } else { scale } println("ImageBrowserItem detectTapGestures rememberTransformableState scale: $scale") }) ... Modifier .transformable(state = state) .graphicsLayer{ //布局缩放、旋转、移动变换 scaleX = scale scaleY = scale translationX = offset.x translationY = offset.y }
切换恢复图片大小
在 pager 组件的 content scope 中允许开发者很轻松地拿到 currentPage
与 currentPageOffset
引用。可以使用这些值来计算效果。我们提供了 calculateCurrentOffsetgithub是什么ForPage()
扩展函数去计算某一个特定页面的动画电影偏移量。
例子如下:
@OptIn(ExperimentalPagerApi::class) @Composable private fun Sample() { Scaffold( topBar = { TopAppBar( title = { Text(stringResource(R.string.horiz_pager_with_transition_title)) }, backgroundColor = MaterialTheme.colors.surface, ) }, modifier = Modifier.fillMaxSize() ) { padding -> HorizontalPagerWithOffsetTransition(Modifier.padding(padding)) } } @OptIn(ExperimentalPagerApi::class, ExperimentalCoilApi::class) @Composable fun HorizontalPagerWithOffsetTransition(modifier: Modifier = Modifier) { HorizontalPager( count = 10, // Add 32.dp horizontal padding to 'center' the pages contentPadding = PaddingValues(horizontal = 32.dp), modifier = modifier.fillMaxSize() ) { page -> Card( 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 // 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) ) } .fillMaxWidth() .aspectRatio(1f) ) { Box { Image( painter = rememberImagePainter( data = rememberRandomSampleImageUrl(width = 600), ), contentDescription = null, modifier = Modifier.fillMaxSize(), ) ProfilePicture( Modifier .align(Alignment.BottomCenter) .padding(16.dp) // We add an offset lambda, to apply a light parallax effect .offset { // Calculate the offset for the current page from the // scroll position val pageOffset = this@HorizontalPager.calculateCurrentOffsetForPage(page) // Then use it as a multiplier to apply an offset IntOffset( x = (36.dp * pageOffset).roundToPx(), y = 0 ) } ) } } } } @OptIn(ExperimentalCoilApi::class) @Composable private fun ProfilePicture(modifier: Modifier = Modifier) { Card( modifier = modifier, shape = CircleShape, border = BorderStroke(4.dp, MaterialTheme.colors.surface) ) { Image( painter = rememberImagePainter(rememberRandomSampleImageUrl()), contentDescription = null, modifier = Modifier.size(72.dp), ) } }
我们可以在pager切换时通过calculateCgoogle服务框架urrentOffsetForPage(GopageGo).absolu手势舞teValue
拿到当前pager的偏移量,当pageOffet =手势舞教学视频简单= 1.0f
时证明动画片熊出没pager已切换至下一页,此时我们恢复scale = 1f
到原始大小即可,部分代码如下:
Modifier .transformable(state = state) .graphicsLayer{ //布局缩放、旋转、移动变换 scaleX = scale scaleY = scale translationX = offset.x translationY = offset.y val pageOffset = pagerScope.calculateCurrentOffsetForPage(page = page).absoluteValue if (pageOffset == 1.0f) { scale = 1.0f } println("ImageBrowserItem pagerScope calculateCurrentOffsetForPage pageOffset: $pageOffset") }
到这里我们就整个实现了大图缩google放、切换、预览功能,完整代码如下:
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.Surface
import androidx.compose.material.swipeable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.eegets.wechatcompose.ui.find.model.ImageBrowserModel
import com.eegets.wechatcompose.ui.theme.WeComposeTheme
import com.google.accompanist.coil.rememberCoilPainter
import com.google.accompanist.pager.*
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlin.math.absoluteValue
/**
* 大图预览
*/
@OptIn(ExperimentalPagerApi::class, InternalCoroutinesApi::class)
@Composable
fun ImageBrowserScreen(images: List<Image>, selectImage: Image) {
var currentIndex = 0
images.forEachIndexed { index, image ->
if (image.url == selectImage.url) {
currentIndex = index
return@forEachIndexed
}
}
/**
* 界面状态变更
*/
val pageState = rememberPagerState(initialPage = currentIndex)
Box {
HorizontalPager(
count = images.size,
state = pageState,
contentPadding = PaddingValues(horizontal = 0.dp),
modifier = Modifier.fillMaxSize()
) { page ->
println("ImageBrowserItem current page: $page")
ImageBrowserItem(images[page], page, this)
}
HorizontalPagerIndicator(
pagerState = pageState,
activeColor = Color.White,
inactiveColor = WeComposeTheme.colors.onBackground,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(60.dp)
)
LaunchedEffect(pageState) {
snapshotFlow { pageState }.collect { pageState ->
println("ImageBrowserItem LaunchedEffect pageState currentPageOffset: $pageState.currentPageOffset")
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun ImageBrowserItem(image: Image, page: Int = 0, pagerScope: PagerScope) {
/**
* 缩放比例
*/
var scale by remember { mutableStateOf(1f) }
/**
* 偏移量
*/
var offset by remember { mutableStateOf(Offset.Zero) }
/**
* 监听手势状态变换
*/
var state =
rememberTransformableState(onTransformation = { zoomChange, panChange, rotationChange ->
scale = (zoomChange * scale).coerceAtLeast(1f)
scale = if (scale > 5f) {
5f
} else {
scale
}
println("ImageBrowserItem detectTapGestures rememberTransformableState scale: $scale")
})
Surface(
modifier = Modifier
.fillMaxSize(),
color = Color.Black,
) {
Image(
painter = rememberCoilPainter(
request = image.url
),
contentDescription = "",
contentScale = ContentScale.Fit,
modifier = Modifier
.transformable(state = state)
.graphicsLayer{ //布局缩放、旋转、移动变换
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
val pageOffset = pagerScope.calculateCurrentOffsetForPage(page = page).absoluteValue
if (pageOffset == 1.0f) {
scale = 1.0f
}
println("ImageBrowserItem pagerScope calculateCurrentOffsetForPage pageOffset: $pageOffset")
}
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
println("ImageBrowserItem detectTapGestures onDoubleTap offset: $it")
scale = if (scale <= 1f) {
2f
} else {
1f
}
offset = Offset.Zero
},
onTap = {
}
)
}
)
}
}
@Preview
@Composable
fun ImageBrowserScreenPreview() {
ImageBrowserScreen(
images = mutableListOf(),
selectImage = Image(url = "https://www.6hu.cc/wp-content/uploads/2022/05/31261-VOT6E7.jpg")
)
}
调github下载用
val images = mutableListOf( Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-BEhBfz.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-ebldS3.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-96qNy9.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-TL1UF1.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-VOT6E7.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-nFgFOw.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-szkyMX.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-vW2DDW.jpg"), Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-el4QgY.jpg") ) val selectImage = Image("https://www.6hu.cc/wp-content/uploads/2022/05/31261-BEhBfz.jpg") ImageBrowserScreen(images = images, selectImage = selectImage)
该功能只是仿Jetpack Compose
微信的小部分功能,所以暂无源码,所后期会将仿微信全部代码上传至Github。
参考资料
# 使用Jetpack Compose完成自定义手势处理
# 多点触控:平移、googleplay安卓版下载缩放、旋转
# Jetpack Navigation Compose Animation
评论(0)