iT邦幫忙

2021 iThome 鐵人賽

DAY 14
1
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 14

D14/ 怎麼做拉動的操作? - Draggable Gesture

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • 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)
            }
        }
        
    }
}

https://ithelp.ithome.com.tw/upload/images/20210928/20141597ysyBRSsBBA.png

拉動 Item 的行為

在 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 可以被拉動了,但是並不會回到原位。因此我們需要一個讓 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:


上一篇
D13/ 怎麼做翻卡片的動畫 - Animation Part 2 & GraphicsLayer
下一篇
D15/ 為什麼 remember 是 composable function? - @Composable 是什麼
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言