这篇文章中,我们介绍Compose的手势,Compose 提供了很多直接使用的API,能够让我们去很方便的检测到用户的手势。下面我们会一个个介绍。
一:Modifier.clickable 点击
由于clickable我们一直在使用,直接上官网的例子,一个计数的Text。点击数字+1,代码如下:
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
效果如下:
二:Modifier.verticalScroll 跟 Modifier.horizontalScroll (执行滚动)
verticalScroll(竖直滚动) 和 horizontalScroll(水平滚动) 当控件的内容大于控件本身的尺寸的时候。使用这两个修饰符可进行滚动。这两个方法的代码如下:
fun Modifier.verticalScroll(
state: ScrollState,
enabled: Boolean = true,
flingBehavior: FlingBehavior? = null,
reverseScrolling: Boolean = false
) = {...}
fun Modifier.horizontalScroll(
state: ScrollState,
enabled: Boolean = true,
flingBehavior: FlingBehavior? = null,
reverseScrolling: Boolean = false
) {...}
- state 是滚动的状态
- enabled 是否可用
- flingBehavior
- reverseScrolling 是否反着滚动 举例如下:当列表滚动到顶部的时候,我们点击滚动按钮,列表会往下滚动,而当列表滚动到底部的时候,我们点击按钮会往上滚动。
@Preview
@Composable
private fun ScrollBoxesSmooth() {
// Smoothly scroll 100px on first composition
val state = rememberScrollState()
val scope = rememberCoroutineScope()
val isScrollBottom = remember {
mutableStateOf(false)
}
Row() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.padding(horizontal = 8.dp)
.verticalScroll(state)
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
Button(modifier = Modifier.padding(10.dp),onClick = {
scope.launch {
// 当滚动到顶部的时候,点击就往下滚
if(state.value<=0){
isScrollBottom.value = false
}else if(state.value>=state.maxValue){
// 当滚动到底部的时候,点击就往上滚动
isScrollBottom.value = true
}
state.animateScrollBy(if(isScrollBottom.value) -50f else 50f)
}
}) {
Text(text = "滚动")
}
}
}
三:Modifier.scrollable (监听滚动)
scrollable修饰符跟上面verticalScroll和horizontalScroll的区别在于,verticalScroll和horizontalScroll会去执行滚动,滚动内容。而scrollable只是检测监听的作用,监听到用户滚动了。但scrollable并没有任何实际的偏移效果。来看看scrollable的代码:
fun Modifier.scrollable(
state: ScrollableState,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null
){...}
- state 滚动的状态 ScrollableState 具体实现通过 rememberScrollableState
- orientation 滚动的方向 Orientation.Vertical是竖直的方向,Orientation.Horizontal是水平的方向
- enabled 是否可用
- reverseDirection 是否反着滚动
- flingBehavior
- interactionSource 可以获取用户的状态,比如是否是按下,是否获取焦点。以前在介绍Button的时候讲过。Button的讲解 举例如下:我们用一个Text去显示出我们手势滚动的数值,代码如下:
@Preview
@Composable
fun scrollableTest(){
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
state = rememberScrollableState {
offset+=it
it
},
orientation = Orientation.Vertical
)
.background(Color.LightGray),contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
效果如下:
四:Modifier.nestedScroll 嵌套滑动
嵌套滑动一种是比较简单的,Compose会自动的实现嵌套滑动,类似原生view的NestedScrollView。一种是需要自己自定义子控件和父控件的滚动逻辑(类似View系统的NestedScrollingChild跟NestedScrollingParent)。
4.1 自动嵌套滑动
@Preview
@Composable
fun nestedScrollTest(){
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(32.dp)
) {
Column {
repeat(6) {
Box(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"Scroll here",
modifier = Modifier
.border(12.dp, Color.DarkGray)
.background(brush = gradient)
.padding(24.dp)
.height(150.dp)
)
}
}
}
}
}
效果如下:
4.2 自定义嵌套滑动
先来看看nestedScroll的代码
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
){...}
- connection NestedScrollConnection 当滚动可滚动子View时该类可以接收事件,并提供一些时机供我们使用。我们来具体看看这个类。NestedScrollConnection有如下几个方法。
- fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero 子控件调用dispatchPreScroll会回调到该方法,允许父控件提前消耗一部分拖动事件。参数:available-可用于预滚动的增量,source—滚动事件的源. 返回值是:消耗的量。
- fun onPostScroll( consumed: Offset,available: Offset, source: NestedScrollSource): Offset = Offset.Zero 子控件消耗完了自己的滚动,调用dispatchPostScroll,告知父控件子控件已经没法滚动了。父控件可以决定要不要接着处理滚动。consumed—层次结构下所有嵌套滚动节点消耗的量,available—此连接可使用的增量,source—卷轴的来源。返回值是:消耗的量
- suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero 当快速滚动的时候,子控件调用dispatchPreFling,会回调到onPreFling,允许父控件提前拦截消耗一部分的快速滑动。
- suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {return Velocity.Zero} 当子控件完成了快速滑动的消耗,会回调到该方法,父控件可以选择是否继续消耗快速滑动。consumed:子View消耗的速度量。available:父对象在子对象之后抛下的速度。返回:该次快速滚动操作消耗的速度量
- dispatcher NestedScrollDispatcher是嵌套滚动的分发类。主要也是有几个方法
- dispatchPreScroll 通知父控件是否提前消耗一些事件。当子控件调用了该方法后,NestedScrollConnection回接收到onPreScroll的回调。代码如下:
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset { return parent?.onPreScroll(available, source) ?: Offset.Zero }
- dispatchPostScroll 通知父控件,我子控件消耗完滑动了。当子控件调用了该方法后,NestedScrollConnection会接收到onPostScroll回调。代码如下:
fun dispatchPostScroll(consumed: Offset,available: Offset, source: NestedScrollSource): Offset { return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero }
- dispatchPreFling 通知父控件是否提前消耗一些快速滑动事件。当子控件调用了该方法后,NestedScrollConnection会接收到dispatchPreFling回调。代码如下:
suspend fun dispatchPreFling(available: Velocity): Velocity { return parent?.onPreFling(available) ?: Velocity.Zero }
- dispatchPostFling 通知父控件,我子控件快速滑动消耗完了。当子控件调用了该方法后,NestedScrollConnection会接收到onPostFling回调。代码如下:
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity { return parent?.onPostFling(consumed, available) ?: Velocity.Zero }
- dispatchPreScroll 通知父控件是否提前消耗一些事件。当子控件调用了该方法后,NestedScrollConnection回接收到onPreScroll的回调。代码如下:
关于嵌套滑动,我们可以来看看下面两张流程图,能够帮助我们更直观的理解它们。
一张是Android。View系统的嵌套滑动的。感谢傅晨明作者的图
另一张是Compose的。
举个实例:有个标题栏,中间有个图片跟切换tab栏。底部是个列表。滑动的时候,图片跟tab栏往上顶。tab栏置顶效果。代码如下:
@Preview
@Composable
fun nestedScrollTest(){
val imageHeight = 150.dp
val headerHeight = 200.dp
val topBarHeight = 48.dp
val state = rememberLazyListState()
val headerOffsetHeightPx = remember {
mutableStateOf(0f)
}
val headerHeightPx = with(LocalDensity.current){
imageHeight.roundToPx().toFloat()
}
val nestedScrollConnection = remember {
object :NestedScrollConnection{
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 说明是向下滚,需要判断list是否已经滚动完了,滚动完了才去滚动
if(available.y>0){
if(state.firstVisibleItemIndex<=2){
val delta = available.y
val newOffset = headerOffsetHeightPx.value + delta
headerOffsetHeightPx.value = newOffset.coerceIn(-headerHeightPx, 0f)
}
}else{
val delta = available.y
val newOffset = headerOffsetHeightPx.value + delta
headerOffsetHeightPx.value = newOffset.coerceIn(-headerHeightPx, 0f)
}
return Offset.Zero
}
}
}
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection,nestedScrollDispatcher)){
LazyColumn(state=state,contentPadding = PaddingValues(top = headerHeight+topBarHeight)) {
items(100) { index ->
Text("I'm item $index", modifier = Modifier
.fillMaxWidth()
.padding(16.dp))
}
}
headerView(headerOffsetHeightPx.value.roundToInt())
TopAppBar(modifier = Modifier
.height(48.dp)
.background(Color.Blue),contentPadding = PaddingValues(start = 20.dp)) {
Text(text = "标题",fontSize = 17.sp)
}
}
}
@Composable
fun headerView(headerOffsetY:Int){
Column(modifier = Modifier.padding(top = 48.dp).offset {
IntOffset(x = 0, y = headerOffsetY)
}) {
// 一张图片。高度是150dp
Image(modifier = Modifier
.fillMaxWidth()
.size(150.dp),bitmap = ImageBitmap.imageResource(id = R.drawable.icon_head), contentDescription = "图片",contentScale = ContentScale.FillBounds)
// 一个TabRow,高度是50dp
tabRowView()
}
}
@Composable
fun tabRowView(){
val tabIndex = remember {
mutableStateOf(0)
}
val tabDatas = ArrayList<String>().apply {
add("语文")
add("数学")
add("英语")
}
TabRow(
selectedTabIndex = tabIndex.value,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
backgroundColor = Color.Green,
contentColor = Color.Black,
divider = {
TabRowDefaults.Divider()
},
indicator = {
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(it[tabIndex.value]),
color = Color.Blue,
height = 2.dp
)
}
) {
tabDatas.forEachIndexed{
index, s ->
tabView(index,s,tabIndex)
}
}
}
@Composable
fun tabView(index:Int,text:String,tabIndex:MutableState<Int>){
val interactionSource = remember {
MutableInteractionSource()
}
val isPress = interactionSource.collectIsPressedAsState().value
Tab(
selected = index == tabIndex.value,
onClick = {
tabIndex.value = index
},
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight(),
enabled =true,
interactionSource = interactionSource,
selectedContentColor = Color.Red,
unselectedContentColor = Color.Black
) {
Text(text = text,color = if(isPress || index == tabIndex.value) Color.Red else Color.Black)
}
}
效果图如下:
五:Modifier.draggable 拖动
draggable是监听拖动事件,跟scrollable一样,都只是检测监听的作用。没有实际的去修改偏移值。先来具体看看draggable的代码:
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
reverseDirection: Boolean = false
){...}
- state 拖动状态 DraggableState 通过rememberDraggableState创建
- orientation 方向 Orientation.Vertical 竖直,Orientation.Horizontal 水平
- enabled 是否可用
- interactionSource 可以获取用户的状态,比如是否是按下,是否获取焦点。以前在介绍Button的时候讲过。[Button的讲解]
- startDragImmediately 设置为true时,DragTable将立即开始拖动,并防止其他手势检测器对“向下”事件作出反应(以阻止合成的基于按键的手势)
- onDragStarted 开始拖动的回调
- onDragStopped 停止拖动的回调
- reverseDirection 是否反方向 举例:Text 拖动,并通过监听拖动的距离从而来改变Text的水平方向上的Offset,从而来达到拖动的效果。代码如下:
@Preview
@Composable
fun draggableTest(){
var offsetX by remember { mutableStateOf(0f) }
Text(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
},
onDragStarted = {
Log.e("ccm","startDrag")
},
onDragStopped = {
Log.e("ccm","endDrag")
}
),
text = "Drag me!"
)
}
六:Modifier.swipeable 滑动
使用 swipeable 修饰符,您可以拖动元素,释放后,这些元素通常朝一个方向定义的两个或多个锚点呈现动画效果。其常见用途是实现“滑动关闭”模式。 请务必注意,此修饰符不会移动元素,而只检测手势。您需要保存状态并在屏幕上表示,例如通过 offset 修饰符移动元素。先来看看swipeable的代码
@ExperimentalMaterialApi
fun <T> Modifier.swipeable(
state: SwipeableState<T>,
anchors: Map<Float, T>,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null,
thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
velocityThreshold: Dp = VelocityThreshold
){...}
- state 滑动状态 SwipeableState 通过rememberSwipeableState()获取,可以获得当前的偏移量,以及当前值,还可以调用animateTo,或者snapTo去执行滑动到指定的value位置等。
- anchors 锚点。
- orientation 方向
- enabled 是否可用
- reverseDirection 是否反方向
- interactionSource 可以获取用户的状态,比如是否是按下,是否获取焦点。以前在介绍Button的时候讲过。[Button的讲解]
- thresholds 指定状态之间的阈值的位置。比如到这个临界值是从0-1的时候是0.3。那么当我们滑动从开始位置滑动不到0.3的时候放开,那么会自动滑回开始位置。如果超过0.3手放开那么就会自动滑到1的位置。
- resistance 阻力-控制刷过边界时施加的阻力大小
- velocityThreshold 即使未达到位置阈值,为使动画进入下一个状态,结束速度必须超过的阈值(以每秒dp为单位)。 举例,我们使用swipeable去实验开关的控件。开关可以通过滑动开启关闭,也可以通过点击开启关闭。代码如下:
@ExperimentalMaterialApi
@Preview
@Composable
fun SwipeableSample() {
val width = 96.dp
val squareSize = 48.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { from, to ->
if(from==0){
FractionalThreshold(0.3f)
}else{
FractionalThreshold(0.7f)
}},
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
.clickable {
scope.launch {
if(swipeableState.currentValue==0){
swipeableState.animateTo(1)
}else{
swipeableState.animateTo(0)
}
}
}
)
}
}
效果如下:
七:Modifier.transformable 多点触控:平移、缩放、旋转
如需检测用于平移、缩放和旋转的多点触控手势,您可以使用 transformable 修饰符。此修饰符本身不会转换元素,只会检测手势。来看看Modifier.transformable的代码
fun Modifier.transformable(
state: TransformableState,
lockRotationOnZoomPan: Boolean = false,
enabled: Boolean = true
){...}
- state TransformableState状态 获取方式通过rememberTransformableState获取,rememberTransformableState有三个入参,1缩放改变多少,2平移改变多少,3旋转改变多少。state可以通过调用animatePanBy,animateZoomBy,animateRotateBy。panBy,zoomBy,rotateBy手动去进行平移,缩放,旋转。带animate开头是有动画的。
- lockRotationOnZoomPan 为true时候是当处于平移缩放的时候,禁止旋转。
- enabled 是否可用 举例:双指缩放,平移,旋转一个蓝色的Box。代码如下:
@Preview
@Composable
fun transformableTest(){
// 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,lockRotationOnZoomPan = false)
.background(Color.Blue)
.fillMaxSize()
)
}
效果如下:
八:Modifier.pointerInput 手势检测器
Modifier.pointerInput 是手势检测器,先来看看Modifier.pointerInput的代码
fun Modifier.pointerInput(
block: suspend PointerInputScope.() -> Unit
){...}
- block 是PointerInputScope。PointerInputScope主要有如下几个扩展方法跟一个内部方法awaitPointerEventScope:
- detectTapGestures 可以监听长按,点击,双击,按下
- detectDragGestures 可以监听拖动。
- detectHorizontalDragGestures 可以监听水平方向时候的拖动
- detectVerticalDragGestures 可以监听竖直方向时候的拖动
- detectDragGesturesAfterLongPress 可以监听长按之后的拖动
- detectTransformGestures 检测平移,缩放,旋转的
- forEachGesture 遍历每组事件。
- awaitPointerEventScope 我们一个个来讲
8.1 detectTapGestures 可以监听长按,点击,按下,双击
先来看看detectTapGestures的代码
suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
){...}
- onDoubleTap 双击的回调
- onLongPress 长按的回调
- onPress 按下
- onTap 点击 举例:点击的时候Box变成黑色,长按时候变成蓝色,双击变成红色,按下时候变成黄色,代码如下:
@Preview
@Composable
fun detectTapGesturesTest(){
val color = remember {
mutableStateOf(Color.Gray)
}
Box(modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
Log.e("ccm", "=onDoubleTap==")
// 双击变成红色
color.value = Color.Red
},
onLongPress = {
Log.e("ccm", "==onLongPress==")
// 长按变成蓝色
color.value = Color.Blue
},
onPress = {
Log.e("ccm", "==onPress==")
// 按下变成黄色
color.value = Color.Yellow
},
onTap = {
Log.e("ccm", "==onTap==")
// 点击时候变成黑色
color.value = Color.Black
}
)
}
.size(200.dp)
.background(color.value)){
}
}
8.2 detectDragGestures,detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGesturesAfterLongPress。 拖动监听
detectDragGestures 是拖动的监听,detectHorizontalDragGestures是水平方向上的拖动的监听,detectVerticalDragGestures是竖直方向上拖动的监听,detectDragGesturesAfterLongPress是长按之后拖动的监听。来具体看看他们的代码
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
){...}
suspend fun PointerInputScope.detectVerticalDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
){...}
suspend fun PointerInputScope.detectHorizontalDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
){...}
suspend fun PointerInputScope.detectDragGesturesAfterLongPress(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
){...}
- onDragStart 开始拖动
- onDragEnd 结束拖动
- onDragCancel 取消拖动
- onDrag 拖动中
- onVerticalDrag 竖直方向拖动中
- onHorizontalDrag 水平方向拖动中 举例:Box随着手指的拖动移动,代码如下:
@Preview
@Composable
fun detectDragGesturesTest(){
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
Log.e("ccm","开始拖动===")
},
onDragEnd = {
Log.e("ccm","结束===")
},
onDragCancel = {
Log.e("ccm","取消===")
},
onDrag = { change, dragAmount ->
Log.e("ccm","拖动中===")
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
)
}
)
}
}
效果如下:
8.3 detectTransformGestures 检测平移,缩放,旋转的
detectTransformGestures 是用来检测平移,缩放,旋转的。我们来具体看看它的代码:
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
){...}
- panZoomLock 为true是缩放平移的时候禁止旋转。
- onGesture 是平移,缩放,旋转的手势回调监听,centroid是中心点的坐标。pan是平移,zoom是缩放,rotation是旋转 举例:支持单点移动,多点缩放,平移,旋转的例子。代码如下:
@Preview
@Composable
fun detectTransformGesturesTest(){
var scale by remember { mutableStateOf(1f) }
var m_rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = m_rotation,
translationX = offset.x,
translationY = offset.y
)
.pointerInput(Unit) {
detectTransformGestures(
panZoomLock = false,
onGesture = {
center,pan,zoom,rotation->
scale *= zoom
m_rotation += rotation
offset += pan
}
)
}
.background(Color.Blue)
.fillMaxSize()
)
}
效果图如下:
8.4 awaitPointerEventScope(监听每个事件)
awaitPointerEventScope 是用于监听一个事件。先来看看awaitPointerEventScope的代码
suspend fun <R> awaitPointerEventScope(
block: suspend AwaitPointerEventScope.() -> R
): R
- block 是一个AwaitPointerEventScope,而AwaitPointerEventScope主要讲如下几个方法:
- awaitFirstDown() Down事件的监听。
- awaitDragOrCancellation() //拖动取消的回调
- awaitHorizontalDragOrCancellation() //水平拖动取消的回调
- awaitVerticalDragOrCancellation() //竖直拖动取消的回调
- drag() //拖动的监听
- horizontalDrag() //水平拖动的监听
- verticalDrag() //竖直拖动的监听
- awaitTouchSlopOrCancellation() 用于判断是否超过最小滑动距离。
- awaitVerticalTouchSlopOrCancellation() 用于判断竖直方向上是否超过最小滑动距离。
- awaitHorizontalTouchSlopOrCancellation() 用于判断水平方向上是否超过最小滑动距离
8.4.1 awaitFirstDown (Down事件的监听)
awaitFirstDown是Down事件的监听,来看看代码:
suspend fun AwaitPointerEventScope.awaitFirstDown(
requireUnconsumed: Boolean = true
): PointerInputChange {
var event: PointerEvent
do {
event = awaitPointerEvent()
} while (
!event.changes.fastAll {
if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
}
)
return event.changes[0]
}
- requireUnconsumed,如果requireUnconsumed为true,并且在PointerEventPass.Main过程中使用第一个down,则忽略该手势。 返回值是一个PointerInputChange。来看看PointerInputChange的代码:
@Immutable
class PointerInputChange(
val id: PointerId,
val uptimeMillis: Long,
val position: Offset,
val pressed: Boolean,
val previousUptimeMillis: Long,
val previousPosition: Offset,
val previousPressed: Boolean,
val consumed: ConsumedData,
val type: PointerType = PointerType.Touch
){...}
可以看到PointerInputChange包含该事件的id,position位置等信息。接下来我们来举个例子,比如我们在屏幕上画个圆点。我们手指按在哪里,圆点就动画移动过度到我们按下的点。代码如下:
@Preview
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
@Composable
fun Circle(modifier: Modifier){
Canvas(modifier = modifier) {
drawCircle(color = Color.Red,radius = 20f,center=Offset(10f,10f))
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
8.4.2 drag(),horizontalDrag(),verticalDrag(),awaitDragOrCancellation(),awaitHorizontalDragOrCancellation(),awaitVerticalDragOrCancellation()
drag是Move事件的监听,horizontalDrag是水平方向的Move事件的监听,verticalDrag竖直方向Move事件的监听。awaitDragOrCancellation是drag取消的监听。awaitHorizontalDragOrCancellation是横向拖动取消的监听。awaitVerticalDragOrCancellation是竖直拖动取消的监听。来看看他们的代码:
suspend fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
){...}
suspend fun AwaitPointerEventScope.horizontalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
){...}
suspend fun AwaitPointerEventScope.verticalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
){...}
suspend fun AwaitPointerEventScope.awaitDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {...}
suspend fun AwaitPointerEventScope.awaitVerticalDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {...}
suspend fun AwaitPointerEventScope.awaitHorizontalDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {...}
- pointerId 是移动的那个事件的id
- onDrag 是拖动的变化监听。有个参数change是变化后的PointerInputChange值 当awaitDragOrCancellation,awaitVerticalDragOrCancellation,awaitHorizontalDragOrCancellation返回null说明对应的跟踪的id的事件已经抬起。 下面我们来举个例子:比如还是一个Box随着手指去拖动。
@Preview
@Composable
fun dragTest(){
val cacheOffset = remember() {
mutableStateOf(Offset.Zero)
}
val offsetAnimatable = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
Modifier
.pointerInput(Unit) {
coroutineScope {
while (true) {
// down事件
val downPointerInputChange = awaitPointerEventScope {
awaitFirstDown()
}
offsetAnimatable.stop()
// 如果位置不在手指按下的位置,先动画的形式过度到手指按下的位置
if (cacheOffset.value.x != downPointerInputChange.position.x
&& cacheOffset.value.y != downPointerInputChange.position.y
) {
launch {
offsetAnimatable.animateTo(downPointerInputChange.position)
cacheOffset.value = downPointerInputChange.position
}
}
// touch Move事件
// 滑动的时候,box随着手指的移动去移动
awaitPointerEventScope {
drag(downPointerInputChange.id, onDrag = {
launch {
offsetAnimatable.snapTo(it.position)
}
cacheOffset.value = it.position
})
}
// 在手指弹起的时候,才通过动画的形式,回到原点的位置
val dragUpOrCancelPointerInputChange = awaitPointerEventScope {
awaitDragOrCancellation(downPointerInputChange.id)
}
// 等于空,说明已经抬起
if(dragUpOrCancelPointerInputChange==null){
launch {
val result = offsetAnimatable.animateTo(Offset.Zero)
cacheOffset.value = Offset.Zero
}
}
}
}
}
.fillMaxSize()
){
Box(modifier = Modifier.offset{ IntOffset(offsetAnimatable.value.x.roundToInt(), offsetAnimatable.value.y.roundToInt()) }.size(50.dp).background(Color.Blue))
}
}
初始效果如下:
下面再举例一个horizontalDrag的,横向滑动Box的例子。代码如下:
@Preview
@Composable
fun swipeToDismissTest(){
Column() {
Box(modifier=Modifier.swipeToDismiss(onDismissed={
Log.e("ccm","===onDismissed==")
}).background(Color.Blue).fillMaxWidth().height(100.dp)){
}
}
}
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Intercept an ongoing animation (if there's one).
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
初始效果如下:
8.4.3 awaitTouchSlopOrCancellation(),awaitVerticalTouchSlopOrCancellation(),awaitHorizontalTouchSlopOrCancellation()
awaitTouchSlopOrCancellation()是用于判断是否达到了最小滑动距离,awaitVerticalTouchSlopOrCancellation()用于判断是否达到了竖直方向上的最小滑动距离,awaitHorizontalTouchSlopOrCancellation() 用于判断水平方向上是否达到了最小滑动距离。 我们来看看他们的具体应用场景。记得前面讲过一个PointerInputScope.detectDragGestures的方法。具体来看看该方法的源码。
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
do {
drag = awaitTouchSlopOrCancellation(down.id, onDrag)
} while (drag != null && !drag.positionChangeConsumed())
if (drag != null) {
onDragStart.invoke(drag.position)
if (
!drag(drag.id) {
onDrag(it, it.positionChange())
}
) {
onDragCancel()
} else {
onDragEnd()
}
}
}
}
}
我们来分析一下,forEachGesture是反复的处理手势,如果没有添加forEachGesture那么手指检测之后走一遍。后面我们会讲到。val down = awaitFirstDown(requireUnconsumed = false) 这句代码我们可以看到拿到down的事件。drag = awaitTouchSlopOrCancellation(down.id, onDrag) 这边会去拿到这个是否达到最小滑动的事件,如果达到了,那么drag会不为空,如果没达到,drag会为空。所以这里while (drag != null && !drag.positionChangeConsumed())会循环的判断。直到不为空,并且该事件没有被消耗。才去调用onDragStart.invoke(drag.position)开始滑动的回调,以及后面的onDrag,onDragCancel,onDragEnd
8.5 forEachGesture(反复的处理手势)
forEachGesture是反复的处理手势。接下来,我们先来看看forEachGesture的代码
suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {...}
- block 是一个PointerInputScope。也就是说forEachGesture里面可以使用PointerInputScope的所有的方法。 forEachGesture是反复的处理手势是什么意思呢? 举个例子:
@Preview
@Composable
fun test(){
Column(modifier = Modifier.pointerInput(Unit) {
awaitPointerEventScope {
val id = awaitFirstDown().id
Log.e("ccm","==awaitFirstDown==id===${id}===")
drag(id,onDrag = {
Log.e("ccm==onDrag=","====id===${it.id}===position===${it.position}===changedToUp===${it.changedToUp()}==changeToDown==${it.changedToUp()}")
})
}
}.fillMaxSize().background(Color.Red))
}
上面的代码,当我们在Column上滑动的时候,会打出来awaitFirstDown以及onDrag的log。但是当我们抬起手指之后再重新按下去对Column进行滑动,发现不打log了。也就是说这时候的手势监听只有一次。如果我们想要去反复的监听该手势。我们就可以添加forEachGesture。代码修改如下:
@Preview
@Composable
fun forEachGestureTest(){
Column(modifier = Modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
val id = awaitFirstDown().id
Log.e("ccm","==awaitFirstDown==id===${id}===")
drag(id,onDrag = {
Log.e("ccm==onDrag=","====id===${it.id}===position===${it.position}===")
})
}
}
}.fillMaxSize().background(Color.Red))
}
这时候我们再去按下Column并且滑动,会发现打出来log。抬起手指,重新按下滑动还是会打出log。这个就是forEachGesture的作用。