ドラッグ＆ドロップの実装

:::note
「画面に赤い〇を表示させ、ドラッグ＆ドロップする」という機能を実装していきます！
役割の明確化をするため、以下のように定義します！
　・UI：　〇を描画させる処理
　・viewModel：　〇の位置を移動させる処理
:::
# 最初にやること
viewModelを使うので、依存関係を設定しておきます。

![](https://storage.googleapis.com/zenn-user-upload/369eb3dd69eb-20250405.png)

1. libs.versions.tomlの`versions`と`libraries`の2か所に以下を追加
   ```kotlin:libs.versions.toml
   [versions]
    //その他バージョン
    lifecycle-viewmodel-compose = "2.8.7"
   
   [libraries]
   //その他のライブラリ
    androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
   ```
2. build.gradle.ktsの`dependencied`に以下を追加
    ```kotlin:build.gradle.kts
    dependencies {
        //その他の依存関係
        implementation(libs.androidx.lifecycle.viewmodel.compose)
    }
    ```
3. **Sync Now**する　←よく忘れる💦
   
# UIを作成
まずは、単純に画面に赤い〇を表示するだけのUIを作ります。
関数名はHogeScreen。
コードはこんな感じ。

```kotlin:HogeScreen.kt
@Composable
fun HogeScreen(
    modifier: Modifier = Modifier,
) {
    Canvas(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray),
    ) {
        drawCircle(
            color = Color.Red,
            radius = 100f,
            center = center,
        )
    }
}
```
〇の描画は`Canvas`コンポーザブルの`drawCircle`を使います！
`modifier`でCanvas全体の表示設定をします。
とりま、`fillMaxSize()`でCanvasを全画面に広げ、`background()`で背景色を設定。
色は適当にLightGrayにしてみました。

Canvasのラムダ{}は何を描画するか？を設定するところ。
`drawCircle()`で〇を描画させます。
どんな〇なのかは、()の中で指定します。
適当に、色は赤、大きさは100fにしておこう。

実行すると・・・

![](https://storage.googleapis.com/zenn-user-upload/f1f6542009dc-20250405.png)

日の丸弁当かな💦

# ViewModelでボールの位置を決定
〇の出現位置は勝手に中央になっていましたが、viewModel側で決めたいと思います！
uiフォルダの中にHogeViewModel.ktファイルを作成し、HogeViewModelクラスを作成。
コードは以下のようにしました。

```kotlin:HogeViewModel.kt
class HogeViewModel: ViewModel() {
    //ボールの位置を保持するStateFlow
    private val _ballPosition: MutableStateFlow<Offset> = MutableStateFlow(Offset(100f, 100f))
    val ballPosition: StateFlow<Offset> = _ballPosition.asStateFlow()
}
```
〇の位置を`_ballPosition`で設定。
初期値は〇の大きさと同じ100fにしてみました！
HogeScreenに〇の位置は`Offset(100f,100f)`だからヨロ！と教えてあげなければいけません。
そのため、`ballPosition`変数をUI用に作成。

# HogeScreen⇔HogeViewModel
HogeScreenとHogeViewModelを相互にやり取りするための設定を、HogeScreenに入れます。

```diff kotlin:HogeScreen.kt
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun HogeScreen(
    modifier: Modifier = Modifier,
+    viewModel: HogeViewModel = viewModel(),  //追加
) {
+    val ballPosition by viewModel.ballPosition.collectAsState()　//追加

    Canvas(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray),
    ) {
        drawCircle(
            color = Color.Red,
            radius = 100f,
+            center = ballPosition,　//変更
        )
    }
}
```
私の環境だけかもしれませんが、`viewModel: HogeViewModel = viewModel()`のviewModel()がいつも自動的にインポートされず、エラーになります😢
しかたがないので、`import androidx.lifecycle.viewmodel.compose.viewModel`を手動で記述してます。
`drawCircle`の'center'をviewModelからもってきた`ballPosition`とすることにより、〇の表示位置が変わります。
実行すると。。。。

![](https://storage.googleapis.com/zenn-user-upload/859fa43d5757-20250405.png)


# ドラッグ＆ドロップしたときの処理
次はHogeScreenでドラッグ＆ドロップしたときの処理を書いていきます。
画面に対してクリック（タップ）したとか、ドラック＆ドロップした（スワイプ）のような情報は、modifierのpointerInput(Unit)でキャッチ。
ラムダ{}の中はどういうジェスチャーに対して処理するのか？を記述。
今回はドラッグ＆ドロップをするので、detectDragGestures()を使っていきます。
```diff kotlin:HogeScreen.kt
@Composable
fun HogeScreen(
    modifier: Modifier = Modifier,
    viewModel: HogeViewModel = viewModel(),
) {
    val ballPosition by viewModel.ballPosition.collectAsState()

    Canvas(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray)
+            .pointerInput(Unit) {
+                detectDragGestures(
+                    onDragStart = {},
+                    onDrag = { change, _ ->},
+                    onDragEnd = {},
+                    onDragCancel = {},
+                )
+            }
    ) {
        //省略
    }
}
```
'onDragStart'とかのラムダ{}は空っぽにしておく。
まだ何も処理作ってないからね＾＾；

# viewModelにボールを動かす処理を追加
HogeViewModelにドラッグ開始位置を記録する変数`dragStartOffset`を１つと、メソッド3つ`onDragStart` `onDrag` `onDragEnd`追加します。

```diff kotlin:HogeViewModel.kt
class HogeViewModel: ViewModel() {
    //ボールの位置を保持するStateFlow
    private val _ballPosition: MutableStateFlow<Offset> = MutableStateFlow(Offset(100f, 100f))
    val ballPosition: StateFlow<Offset> = _ballPosition.asStateFlow()

+    private var dragStartOffset: Offset? = null //ドラッグ開始の位置
    //以下全部追加
    fun onDragStart(offset: Offset) {
        dragStartOffset = offset
    }

    fun onDrag(dragAmount: Offset) {
        if (dragStartOffset != null) {
            val newPosition = Offset(
                _ballPosition.value.x + dragAmount.x,
                _ballPosition.value.y + dragAmount.y
            )
            _ballPosition.update { it.copy(newPosition.x, newPosition.y) }
        }
    }

    fun onDragEnd() {
        dragStartOffset = null
    }
}
```
各メソッドの役割は次の通りです。
* onDragStart
　`doragStartOffset`にドラッグ開始の位置を入れる

* onDrag
　ドラッグによる移動量は`dragAmount`から取得。
  〇の位置`_ballPosition`を移動した分`dragAmount`だけ加算し、新しい位置'newPosition'を作る。
　〇の位置を`newPosition`で更新。

* onDragEnd
　ドラッグが終わったら、`dragStartOffset`を空っぽにする

実行してみるとー

![](https://storage.googleapis.com/zenn-user-upload/ecf265e3c0bf-20250405.gif)


# 問題点について
実際に触ってみるとわかりますが、〇をドラッグしなくても〇は動きます。
〇とタップした位置の距離を測って、〇の範囲に入っているかどうか？を確認し、入っていればドラッグスタート！入っていなければ無視のような処理をしないとですね。
次回修正していきたいと思います！