Compose 手勢處理,增進交互體驗
- 概述
- 常用手勢處理Modifier
- clickable()
- combinedClickable()
- draggable()
- swipeable()
- transformable()
- scrollable()
- nestedScroll
- NestedScrollConnection
- NestedScrollDispatcher
- 定制手勢處理
- 使用 PointerInput Modifier
- PointerInputScope
- awaitPointerEventScope
概述
在處理手勢時,應將手勢處理修飾符盡可能放到 Modifier 末尾,從而可以避免產生不可預期的行為。
常用手勢處理Modifier
clickable()
監聽點擊事件,在絕大多數情況下,只需要出入 onClick 回調即可。當然也可以將 enable 參數設置為一個可變狀態,通過狀態來動態控制啟用點擊監聽。
@Composable
private fun GestureOfClick(){var colorState by remember { mutableStateOf(false) }Box(modifier = Modifier.size(60.dp).background(color = if (colorState) Color.LightGray else Color.Gray).clickable { colorState = !colorState },contentAlignment = Alignment.Center){Text(text = "點擊")}
}
combinedClickable()
和 clickable() 類似,但支持長按/雙擊/單擊:
// Clickable.kt
fun Modifier.combinedClickable(enabled: Boolean = true,onClickLabel: String? = null,role: Role? = null,onLongClickLabel: String? = null,onLongClick: (() -> Unit)? = null,onDoubleClick: (() -> Unit)? = null,onClick: () -> Unit
)
draggable()
只支持檢測單一方向的拖動(水平方向或垂直方向),不支持同時監聽兩個方向上的拖動偏移,要實現這種效果,需要使用更底層的 PointerInputModifier。
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//是否反轉方向。
): Modifier
swipeable()
可以拖動元素,釋放后,這些元素通常朝一個方向定義的兩個或多個錨點繼續滑動以呈現動畫效果。其常見用途是實現“滑動關閉”模式。
必傳的參數有:
- state:是一個SwipeableState,可以記錄當前的偏移數據
- anchors:錨點,用來記錄不同滑動數據對應的狀態
- orientation:滑動方向
- thresholds:不同錨點之間的臨界值
transformable()
多點觸控,在日常生活當中,多點觸控這樣的操作多數是在瀏覽圖片,網頁或者地圖之類的場景下被用到
transformable有三個參數:
- state:TransformableState,用來獲取多點觸控時候目標組件大小,位移,旋轉角度變化情況的
- lockRotationOnZoomPan:Boolean,這個參數的意思是如果設置為false,那么多點觸控的時候將會同時監聽雙指拖動,縮放以及旋轉,但是如果設置為true的時候,除非旋轉動作比其余兩個動作先執行,這樣會被監聽到,不然的話,只會監聽雙指拖動和縮放動作,旋轉事件將不會被監聽
- enabled:Boolean,是否可用
@Composable
private fun TransformableSample() {// set up all transformation statesvar 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 *= zoomChangerotation += rotationChangeoffset += 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())
}
scrollable()
雖然 verticalScroll() / horizontalScroll() 和 scrollable() 的名字很像,但它們并不是相同的東西,scrollable() 修飾符僅負責檢測滾動手勢,并不會幫我們自動偏移元素內容,滾動行為由開發者定義,用法類似 draggable() 修飾符
var offsetX by remember { mutableFloatStateOf(0f) }
Column {Text(text = "OffsetX: $offsetX")Box(Modifier.size(200.dp).background(Pink).scrollable(// 檢測水平方向的滾動手勢orientation = Orientation.Horizontal,// 使用 rememberScrollableState 創建并傳遞一個 ScrollableState 對象。// 通過 ScrollableState 可以獲取到滾動手勢的偏移量,進一步定義滾動行為。state = rememberScrollableState { delta ->offsetX += deltadelta // 為了支持嵌套滾動,必須返回消費的滾動距離量}))
}
nestedScroll
嵌套滑動,需要傳遞兩個參數,connection: NestedScrollConnection 和 dispatcher: NestedScrollDispatcher,源碼如下:
fun Modifier.nestedScroll(connection: NestedScrollConnection,dispatcher: NestedScrollDispatcher? = null
){...}
- connection:包含了嵌套滑動的和姓邏輯,通過回調可以在子布局獲得滑動事件前,預先消費掉部分或全部手勢偏移量,當然也可以獲取子布局消費后剩下的手勢偏移量。
- dispatcher:包含用于父布局的NestedScrollConnection,可以使用包含的 dispatch**系列方法動態控制組件完成滑動。
NestedScrollConnection
interface NestedScrollConnection {fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zerofun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset = Offset.Zerosuspend fun onPreFling(available: Velocity): Velocity = Velocity.Zerosuspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {return Velocity.Zero}
}
- onPreScroll:在子控件滑動之前,會先使用NestedScrollDispatcher詢問父控件是否需要消費available的偏移量,父控件可以在該方法內計算自身需要消費的量,然后返回自身消費了的偏移量。
- onPostScroll:在子控件滑動之后,會使用NestedScrollDispatcher通知父控件,告知其consumed的偏移量以及剩余available的偏移量,而父控件則可以根據情況判斷是否還要再偏移,以及使用和子控件同等的偏移還是剩余的偏移。完成之后返回自身消費了的偏移量
- onPreFling:在子控件進行慣性滑行之前,會先使用NestedScrollDispatcher詢問父控件是否需要消費available的速度值,父控件可以在該方法內計算自身需要消費的量,然后返回自身消費了的速度值
- onPostFling:在子控件進行慣性滑行之后,會使用NestedScrollDispatcher通知父控件,告知其consumed的速度值以及剩余available的速度值,而父控件則可以根據情況判斷是否還要再偏移,以及使用和子控件同等的速度還是剩余的速度。完成之后返回自身消費了的速度值。
一句話概括:在滑動前父控件可以通過onPreScroll回調先消費部分或全部偏移量;待子控件消費完后,父控件依舊可以通過onPostScroll方法進行消費,區別在于在onPreScroll回調中,父控件是優先消費的,而onPostScroll則是子控件優先消費,fling的兩個方法同理。
NestedScrollDispatcher
class NestedScrollDispatcher {// ....// 在滑動之前調用,將可用的偏移量傳遞給父控件fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {return parent?.onPreScroll(available, source) ?: Offset.Zero}//在滑動之后調用,將已經消費的偏移量以及剩余可用的偏移量傳遞給父控件fun dispatchPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset {return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero}//在滑動之后,如果產生了慣性滑動,那么需要將相應的速度值傳遞給父控件suspend fun dispatchPreFling(available: Velocity): Velocity {return parent?.onPreFling(available) ?: Velocity.Zero}//在慣性滑動之后,需要將已經消費了的速度值以及剩余可用的速度值傳遞給父控件suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {return parent?.onPostFling(consumed, available) ?: Velocity.Zero}
一句話概括:通過dispatchPreScroll方法詢問父控件是否需要消費,方法返回的就是父控件消費了的量,然后就可以做自己的滑動操作了,在滑動完成之后,再通過dispatchPostScroll方法通知父控件。
定制手勢處理
前面提到的手勢處理修飾符都是基于低級別的 pointerInput 修飾符進行封裝實現的。
使用 PointerInput Modifier
// SuspendingPointerInputFilter.kt
fun Modifier.pointerInput(key1: Any?, block: suspend PointerInputScope.() -> Unit
): Modifier
- keys:當 Composable 發生重組時,如果傳入的 keys 發生了變化,則手勢事件處理過程會被中斷
- block:在這個 PointerInputScope 作用域代碼塊中,變可以聲明手勢事件的處理邏輯了,發生在協程中。
PointerInputScope
- detectTapGestures():設置更細粒度的點擊監聽回調
- detectDragGestures():設置更細粒度的拖動手勢監聽回調
- detectTransformGestures():雙指拖動、縮放與旋轉手勢操作中更具體的手勢信息
- detectDragGesturesAfterLongPress():監聽長按后的拖動手勢
- detectHorizontalDragGestures():監聽水平拖動手勢
- detectVerticalDragGestures():監聽垂直拖動手勢
- forEachGesture:允許用戶可以對每一個手勢事件序列進行相同的定制處理
awaitPointerEventScope
AwaitPointerEventScope 作用域中,可以使用 Compose 中所有低級別的手勢處理掛起方法。
suspend fun <R> awaitPointerEventScope(block: suspend AwaitPointerEventScope.() -> R
): R
API名稱 | 作用 |
---|---|
awaitPointerEvent | 手勢事件 |
awaitFirstDown | 第一根手指的按下事件 |
drag | 拖動事件 |
horizontalDrag | 水平拖動事件 |
verticalDrag | 垂直拖動事件 |
awaitDragOrCancellation | 單次拖動事件 |
awaitHorizontalDragOrCancellation | 單次水平拖動事件 |
awaitVerticalDragOrCancellation | 單次垂直拖動事件 |
awaitTouchSlopOrCancellation | 有效拖動事件 |
awaitHorizontalTouchSlopOrCancellation | 有效水平拖動事件 |
awaitVerticalTouchSlopOrCancellation | 有效垂直拖動事件 |
參考資料:巧用Compose來實現手勢拖拽效果