👏

ドラッグ&ドロップの実装1

に公開

最初にやること

viewModelを使うので、依存関係を設定しておきます。

ファイルの場所はこちら

  1. libs.versions.tomlのversionslibrariesの2か所に以下を追加
    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に以下を追加
    build.gradle.kts
    dependencies {
        //その他の依存関係
        implementation(libs.androidx.lifecycle.viewmodel.compose)
    }
    
  3. Sync Nowする ←よく忘れる💦

UIを作成

まずは、単純に画面に赤い〇を表示するだけのUIを作ります。
関数名はHogeScreen。
コードはこんな感じ。

Canvasのimportはこちら

import androidx.compose.foundation.Canvas

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にしておこう。

実行すると・・・

実行結果はこちら

日の丸弁当かな💦

ViewModelでボールの位置を決定

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

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に入れます。

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とすることにより、〇の表示位置が変わります。
実行すると。。。。

実行結果はこちら

ドラッグ&ドロップしたときの処理

次はHogeScreenでドラッグ&ドロップしたときの処理を書いていきます。
画面に対してクリック(タップ)したとか、ドラック&ドロップした(スワイプ)のような情報は、modifierのpointerInput(Unit)でキャッチ。
ラムダ{}の中はどういうジェスチャーに対して処理するのか?を記述。
今回はドラッグ&ドロップをするので、detectDragGestures()を使っていきます。

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を1つと、メソッド3つonDragStart onDrag onDragEnd追加します。

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を空っぽにする

ViewModelで作った関数をUIから動かす!

それぞれ、HogeScreenに作ったonDragStartとか、onDrag、onDaragEndにviewModelの関数を当てはめていきます。

HogeScreen.kt
//省略
    Canvas(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray)
            .pointerInput(Unit) {
                detectDragGestures(
+                    onDragStart = { offset ->
+                        viewModel.onDragStart(offset)
+                    },
+                    onDrag = { change, dragAmount ->
+                        viewModel.onDrag(dragAmount)
+                    },
+                    onDragEnd = { viewModel.onDragEnd() },
+                    onDragCancel = {},
+                )
            }
    ) {
//省略

onDragStartのoffsetはクリックした位置です。
onDragのchangeはドラッグにより移動したマウス(指?)の位置
dragAmountはドラッグによる移動量
viewModelに渡すのは移動量の方です。
onDragEndは何も渡さずそのまま実行させます。

実行結果はこちら

問題点について

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

Discussion