今天大概會聊到的範圍
- draggable
- pointerInput
前兩天在 Feedy 上看文章順便想靈感時,突然發現這個行為:
列表項目可以左右拖移並展示出更多功能。我覺得這個行為很適合現在的我來仿造學習:有 State 概念、有 Animation 又有 Gesture。
從最單純的一個畫面開始吧,簡單的文字 + LazyColumn 組成的一個 List,仿造 Feedy 的列表。
@Composable
fun Item(title: String = "", content: String = "") {
Card(
backgroundColor = black,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp)
) {
Column {
Text(
title,
color = Color.White,
style = titleStyle,
)
Text(
content,
color = Color.White,
style = contentStyle,
softWrap = true,
maxLines = 4,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
fun ListScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
.background(color = blackBgColor)
) {
val repo = RandomWordRepo()
LazyColumn {
items(repo.randomContent) {
val (title, content) = it
Item(title = title, content = content)
}
}
}
}
在 Gesture modifier 中,我們有 draggable 這個 modifier 可以表達拖拉的行為。
@Composable
fun DraggableItem(title: String = "", content: String = "") {
Box(modifier = Modifier.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState {
Log.d("DraggableItem", "DraggableItem: $it")
}
)) {
Item(title = title, content = content)
}
}
在使用 draggable
時,我們要提供兩個資料:orientation
表示可以拉動的維度(水平還是垂直)、state
則是拉動距離的資料載體,使用 rememberDraggableState
可以取得 delta
值 (拉動距離) 作為 callback 使用
目前這個 Item 已經可以被拉動了(Log 會有資料),但是畫面不會跟著動。這時我們要將拉動距離和物件的位置做連動
@Composable
fun DraggableItem(title: String = "", content: String = "") {
// 增加一個 state 紀錄 offsetX
var offsetX by remember { mutableStateOf(0f) }
Box(modifier = Modifier
.offset { IntOffset(x = offsetX.roundToInt(), y = 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState {
offsetX += it // <--- 每次都將 drag 的距離加上 x offset
}
)) {
Item(title = title, content = content)
}
}
現在我們可以拉動 Item 了,但這個 Item 可以任意地被拉動,這不是我們所希望的。所以我在 offset {} 加上 maxOffset 的判斷
.offset {
val maxOffset = 100.dp.toPx()
val x =
when {
offsetX > maxOffset -> maxOffset
offsetX < -maxOffset -> -maxOffset
else -> offsetX
}
IntOffset(
x = x.roundToInt(),
y = 0
)
}
現在 Item 可以被拉動了,但是並不會回到原位。因此我們需要一個讓 Item 回到原先的位置的行為。
我希望當手指離開螢幕的時候,可以做回彈的動作。因此,我希望在 drag 結束時將 offsetX 設定回 0
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState {
offsetX += it
},
onDragStopped = { offsetX = 0f }
)
整個 drag 行為,也可以透過 pointerInput 來達成。pointerInput 是另一個 gesture modifier,他可以取得較 low-level 的互動數據並加以操作
最後,我想在 Item 移動時加上動畫。基本上我只需要將 offsetX 這個 float 透過 animateFloatAsState 做一次轉換就可以了。
var offsetX by remember { mutableStateOf(0f) }
val offsetXAnimate by animateFloatAsState(targetValue = offsetX)
接下來我們將所有用到 offsetX 的部分都改成 offsetXAnimate 就大功告成了!
Reference: