あけましておめでとうございます。モバイルコースのEtsushiです。
初詣でおみくじを引いたところ「慎めば吉」でした。
就職の年にどうなんだ… という感じですが、何事も消極的な方が良いようです。
本記事では、AndroidでViewをユーザがドラッグできるよう実装する際に、意外とベストマッチする方法が見つからず時間を取られてしまったのでその内容を紹介します。
実装内容
- ImageViewをドラッグで動かせるようにする
- ActionBarやButtomNavigationBarには被らないようにする
今回ドラッグさせる対象はImageViewですが、どのViewでも大丈夫です。
Viewのドラッグ実装方針
Androidにはドラッグ&ドロップ用のフレームワークも用意されており、こちらを用いて簡単にViewのドラッグを実装することができます。
View間で要素の受け渡しを行ったり,アプリ間でデータのやり取りを行いたい場合はこちらのフレームワークを採用するのが良いと思います。
しかしながら、こちらのフレームワークを利用するとActionBar上などViewが上に重なってほしくない領域にまでViewを移動させることができてしまいます。
View.DropShadowBuilderを継承したクラスを作成することで表示領域を制御できるかもしれませんが、かなりややこしいロジックを組むことになりそうです。
さらに言うと、フレームワークを使うとView.DropShadowBuilderを継承してもドラッグ中のアイテムは自動で半透明になってしまい、透過度は制御できません。
このため、今回はドラッグ&ドロップのフレームワークを使わず、setOnTouchListnerとGestureDetectorを用いて実装します。
setOnTouchListnerはViewへのタッチイベントを取得することができ、GestureDetectorではsetOnTouchListerで検出したイベントを元にフリックやスクロールといった操作の内容ごとに処理を行うことができます。
実装
setOnTouchListnerとGestureDetectorを用いてViewをドラッグする実装します。
内容は以下の通りです。
- 空のViewを用意し、タッチイベントとスクロール操作を検出する
- 1で取得したスクロール量だけImageViewの位置を更新する
まずはレイアウトファイルを編集します。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<View
android:id="@+id/view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sample_image"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
次にActivity側を編集します。
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
}
private fun setupUI() {
val gestureDetector = createGestureDetector()
gestureDetector.setIsLongpressEnabled(false) // ロングタップ無効化
binding.view.setOnTouchListener { _, motionEvent -> // 空Viewへのタッチイベントをハンドリング
gestureDetector.onTouchEvent(motionEvent)
}
}
private fun createGestureDetector(): GestureDetector {
return GestureDetector(this, object: GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
// 画像の位置を更新
binding.imageView.translationX -= distanceX
binding.imageView.translationY -= distanceY
return true
}
})
}
}
以上で完成です。
無事に要件に合うドラッグを実現することができました。
注意点
実装上の注意点として、空のViewを用意せずに直接ImageViewをスクロールさせると、ドラッグ時にImageViewの動きがガタガタになり不安定になります。
これはImageViewの位置を更新した際に進行方向と逆方向のスクロールとして検出されているためではないかと考えています。
また、setOnTouchListnerのみを使いGestureDetectorを使わない方針で実装している記事も多く見かけましたが、実装してみると同じような理由で動作が不安定になることが多かったです。
おわりに
ドラッグくらい簡単だろうとタカを括っていたら意外と要件に合い、滑らかに動作する実装方法が見つからなかったので記事にしました。同じ様にお困りの方に読んでいただければ幸いです。
参考文献
- https://developer.android.com/guide/topics/ui/drag-drop?hl=ja
- https://developer.android.com/training/gestures/detector?hl=ja
- https://rni-dev.hatenablog.com/entry/2021/03/19/115000
- https://akira-watson.com/android/imageview-drag.html
- https://kurukurupapa.hatenablog.com/entry/20120422/1335098811
- https://note.com/masato1230/n/n31570bc82268