Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Androidでもドラッグ&ドロップがしたい!

m.coder
October 05, 2022

 Androidでもドラッグ&ドロップがしたい!

DroidKaigi2022 Day2 14:05-14:30 (Online) の発表スライドです。

m.coder

October 05, 2022
Tweet

More Decks by m.coder

Other Decks in Technology

Transcript

  1. Copyright 2022 m.coder All Rights Reserved. 2 自己紹介 m.coder (

    @_m_coder ) フラー株式会社所属 Androidテックリード DroidKaigi 2021で「アプリのメンテナンス画面・強制アップ デートを再考する」という発表をしました https://droidkaigi.jp/2021/timetable/276757?day=1 個人ブログ始めました https://nanaten.github.io/blog/
  2. Copyright 2022 m.coder All Rights Reserved. 10 ホーム画面 ( Android

    ) ドラッグ&ドロップで並び替えやフォルダ作成が可能
  3. Copyright 2022 m.coder All Rights Reserved. 11 UITableView ( iOS

    ) ドラッグ&ドロップでリストの並び替えが可能 ※注:内部実装は自前で行う必要あり
  4. Copyright 2022 m.coder All Rights Reserved. 18 テーマ • リストのドラッグ&ドロップ操作の概要を知る

    • アプリ内のドラッグ&ドロップ • アプリ間のドラッグ&ドロップ
  5. Copyright 2022 m.coder All Rights Reserved. 19 テーマ • リストのドラッグ&ドロップ操作の概要を知る

    • アプリ内のドラッグ&ドロップ • アプリ間のドラッグ&ドロップ 【ゴール】 ドラッグ&ドロップの実装を(難しそうという理由で) 選択肢から外す人を減らせるといいな
  6. 01 | RecyclerView のドラッグ&ドロップ 02 | Jetpack DragAndDrop 03 |

    Jetpack Compose のドラッグ&ドロップって?(オマケ) アジェンダ Copyright 2022 m.coder All Rights Reserved. アジェンダ 20
  7. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , target: RecyclerView.ViewHolder , ): Boolean { return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* NO USE */ } } ) itemTouchHelper.attachToRecyclerView(recyclerView) 22 RecyclerViewのドラッグ&ドロップ ItemTouchHelperを使って実装する
  8. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , target: RecyclerView.ViewHolder , ): Boolean { return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* NO USE */ } } ) itemTouchHelper.attachToRecyclerView(recyclerView) 23 RecyclerViewのドラッグ&ドロップ ItemTouchHelperを使って実装する ItemTouchHelper.SimpleCallbackを利用すると比較的簡単 に実装可能
  9. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , target: RecyclerView.ViewHolder , ): Boolean { return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* NO USE */ } } ) itemTouchHelper.attachToRecyclerView(recyclerView) 24 RecyclerViewのドラッグ&ドロップ ItemTouchHelperを使って実装する ItemTouchHelper.SimpleCallbackを利用すると比較的簡単 に実装可能
  10. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , target: RecyclerView.ViewHolder , ): Boolean { return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* NO USE */ } } ) itemTouchHelper.attachToRecyclerView(recyclerView) 25 RecyclerViewのドラッグ&ドロップ ItemTouchHelperを使って実装する ItemTouchHelper.SimpleCallbackを利用すると比較的簡単 に実装可能 最低限override必要なメソッド - onMove - onSwiped(スワイプ操作が必要ない場合でもoverride必 須)
  11. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , target: RecyclerView.ViewHolder , ): Boolean { return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* NO USE */ } } ) itemTouchHelper.attachToRecyclerView(recyclerView) 26 RecyclerViewのドラッグ&ドロップ ItemTouchHelperを使って実装する ItemTouchHelper.SimpleCallbackを利用すると比較的簡単 に実装可能 最低限override必要なメソッド - onMove - onSwiped(スワイプ操作が必要ない場合でもoverride必 須) attachToRecyclerView でドラッグ操作したい RecyclerView にItemTouchHelper をセットする
  12. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper .UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { override fun onMove( … ): Boolean { return true } override fun onMoved( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , fromPos: Int , target: RecyclerView.ViewHolder , toPos: Int , x: Int , y: Int , ) { adapter.notifyItemMoved(fromPos, toPos) } … } ) itemTouchHelper.attachToRecyclerView(recyclerView) 28 RecyclerViewのドラッグ&ドロップ onMoved にドラッグ成功時の処理を書く 左のコードでは adapter.notifyItemMoved() で RecyclerView.Adapterに変更を通知している
  13. Copyright 2022 m.coder All Rights Reserved. val itemTouchHelper = ItemTouchHelper(

    object : ItemTouchHelper.SimpleCallback( ItemTouchHelper. UP or ItemTouchHelper. DOWN, ItemTouchHelper. LEFT) { … override fun onSelectedChanged ( viewHolder: RecyclerView.ViewHolder? , actionState: Int , ) { if (actionState == ACTION_STATE_DRAG) { // ドラッグ開始時 viewHolder?.itemView?.alpha = 0.5f } } override fun clearView( recyclerView: RecyclerView , viewHolder: RecyclerView.ViewHolder , ) { // ドラッグ操作終了時 viewHolder.itemView.alpha = 1.0f } … } ) itemTouchHelper.attachToRecyclerView(recyclerView) 30 RecyclerViewのドラッグ&ドロップ onSelectedChanged メソッド ドラッグ操作を開始した際に呼ばれる clearView メソッド ドラッグ操作を終了した際に呼ばれる
  14. Copyright 2022 m.coder All Rights Reserved. 33 RecyclerViewのドラッグ&ドロップ ItemTouchHelperの注意点 onMove,

    onMoved は他のアイテム上にドラッグされるたび に呼ばれる API通信などを行う場合は要注意
  15. Copyright 2022 m.coder All Rights Reserved. 35 Jetpack DragAndDrop •

    Google I/O 2022 で発表されたJetpackライブラリ • アプリ内やアプリ間のドラッグ&ドロップ操作をサポー ト • https://developer.android.com/jetpack/androidx/r eleases/draganddrop
  16. Copyright 2022 m.coder All Rights Reserved. 40 Jetpack DragAndDrop ドロップする側のView

    現状のDragAndDropライブラリの対象範囲はドロップ側
  17. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 41 Jetpack DragAndDrop DragStartHelperを用いる
  18. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 42 Jetpack DragAndDrop DragStartHelperを用いる 引数にはドラッグ対象のViewとOnDragStartListenerを渡す
  19. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 43 Jetpack DragAndDrop DragStartHelperを用いる 引数にはドラッグ対象のViewとOnDragStartListenerを渡す ドラッグ時のデータの設定を行う
  20. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 44 Jetpack DragAndDrop DragStartHelperを用いる 引数にはドラッグ対象のViewとOnDragStartListenerを渡す ドラッグ時のデータの設定を行う startDragAndDropメソッドを実行する
  21. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 45 Jetpack DragAndDrop DragStartHelperを用いる 引数にはドラッグ対象のViewとOnDragStartListenerを渡す ドラッグ時のデータの設定を行う startDragAndDropメソッドを実行する attach()を呼び出す
  22. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 46 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える
  23. Copyright 2022 m.coder All Rights Reserved. 47 ClipData ClipDataについて クリップボードへのデータコピーに用いられるクラス

    https://developer.android.com/guide/topics/text/copy-p aste https://developer.android.com/reference/android/conte nt/ClipData
  24. Copyright 2022 m.coder All Rights Reserved. 48 ClipData ClipDataについて クリップボードへのデータコピーに用いられるクラス

    https://developer.android.com/guide/topics/text/copy-p aste https://developer.android.com/reference/android/conte nt/ClipData
  25. Copyright 2022 m.coder All Rights Reserved. 49 ClipData ClipDataについて クリップボードへのデータコピーに用いられるクラス

    https://developer.android.com/guide/topics/text/copy-p aste https://developer.android.com/reference/android/conte nt/ClipData
  26. Copyright 2022 m.coder All Rights Reserved. 50 ClipData ClipDataについて クリップボードへのデータコピーに用いられるクラス

    https://developer.android.com/guide/topics/text/copy-p aste https://developer.android.com/reference/android/conte nt/ClipData
  27. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 51 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える
  28. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 52 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える DragShadowBuilderでドラッグ時のViewの描画を設定
  29. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 53 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える DragShadowBuilderでドラッグ時のViewの描画を設定 startDragAndDropの引数 ・ClipData ・DragShadowBuilder ・myLocalState(Object型) ・ドラッグ時の権限フラグ
  30. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 54 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える DragShadowBuilderでドラッグ時のViewの描画を設定 startDragAndDropの引数 ・ClipData ・DragShadowBuilder ・myLocalState(Object型) ・ドラッグ時の権限フラグ
  31. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val uri = Uri.parse( “{cat_image_uri}” ) val dragData = ClipData( "image", arrayOf("image/*"), ClipData.Item(uri) ) val builder = View.DragShadowBuilder(v) v.startDragAndDrop( dragData, builder, null, 0, ) } ).attach() 55 Jetpack DragAndDrop OnDragStartListener ClipData型のデータを扱える DragShadowBuilderでドラッグ時のViewの描画を設定 startDragAndDropの引数 ・ClipData ・DragShadowBuilder ・myLocalState(Object型) ・ドラッグ時の権限フラグ
  32. Copyright 2022 m.coder All Rights Reserved. DropHelper.configureView( requireActivity() , dropImageView,

    arrayOf("text/plain", "image/*"), OnReceiveContentListener { _, payload -> // 受け取ったPayloadの処理 payload } ) 56 Jetpack DragAndDrop DropHelper Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う
  33. Copyright 2022 m.coder All Rights Reserved. DropHelper.configureView( requireActivity() , dropImageView,

    arrayOf("text/plain", "image/*"), OnReceiveContentListener { _, payload -> // 受け取ったPayloadの処理 payload } ) 57 Jetpack DragAndDrop DropHelper Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う
  34. Copyright 2022 m.coder All Rights Reserved. DropHelper.configureView( requireActivity() , dropImageView,

    arrayOf("text/plain", "image/*"), OnReceiveContentListener { _, payload -> // 受け取ったPayloadの処理 payload } ) 58 Jetpack DragAndDrop DropHelper Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う
  35. Copyright 2022 m.coder All Rights Reserved. DropHelper.configureView( requireActivity() , dropImageView,

    arrayOf("text/plain", "image/*"), OnReceiveContentListener { _, payload -> // 受け取ったPayloadの処理 payload } ) 59 Jetpack DragAndDrop DropHelper Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う OnReceiveContentListenerで 受け取ったデータに応じた処理を行う
  36. Copyright 2022 m.coder All Rights Reserved. DropHelper.configureView( requireActivity() , dropImageView,

    arrayOf("text/plain", "image/*"), OnReceiveContentListener { _, payload -> val clip: ClipData = payload. clip when (clip.description.getMimeType(0)) { "text/plain" -> { // トースト表示 } "image/*" -> { val uri = clip.getItemAt( 0).uri dropImageView.setImageURI(uri) } } payload } ) 60 Jetpack DragAndDrop DropHelper Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う OnReceiveContentListenerで 受け取ったデータに応じた処理を行う
  37. Copyright 2022 m.coder All Rights Reserved. 61 Jetpack DragAndDrop DropHelper

    Jetpack DragAndDropで追加されたHelper configreView() でドロップさせたいViewに設定を行う OnReceiveContentListenerで 受け取ったデータに応じた処理を行う ドラッグ&ドロップが可能になる
  38. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> // 省略 ClipDataの生成などなど … v.startDragAndDrop( dragData, builder, null, DRAG_FLAG_GLOBAL or DRAG_FLAG_GLOBAL_URI_READ ) } ).attach() 65 Jetpack DragAndDrop startDragAndDropに DRAG_FLAG_GLOBAL を指定するとアプリ間ドラッグ&ドロップが可能になる DRAG_FLAG_GLOBAL_URI_READ を指定するとURIの読み取りが可能になる
  39. Copyright 2022 m.coder All Rights Reserved. 67 Jetpack DragAndDrop FileProvider・ContentProvider?

    →アプリ間でコンテンツを共有するためのクラス 外部アプリからURIを使ったアクセスを可能にする
  40. Copyright 2022 m.coder All Rights Reserved. 68 Jetpack DragAndDrop FileProvider、ContentProviderについては

    省略します  参考: https://developer.android.com/training/secure-file-sharing/setup-sharing https://developer.android.com/guide/topics/providers/content-provider-basics
  41. Copyright 2022 m.coder All Rights Reserved. DragStartHelper( imageView, DragStartHelper.OnDragStartListener {

    v, _ -> val bitmap = imageView.drawToBitmap() val fileName = "sample_photo.jpg" val tempFile = File( "{file_path}", fileName) // 省略 一時ファイルにFileOutputStream を用いて書き込み val uri = FileProvider.getUriForFile( requireContext() , BuildConfig.APPLICATION_ID + ".provider", tempFile ) // 省略 ClipDataの生成などなど … v.startDragAndDrop( dragData, builder, null, DRAG_FLAG_GLOBAL or DRAG_FLAG_GLOBAL_URI_READ ) } ).attach() 69 Jetpack DragAndDrop FileProviderやContentProviderなどを用いて、URIを適切に 外部アプリから読み取れるようにする
  42. Copyright 2022 m.coder All Rights Reserved. Modifier.pointerInput(Unit) { detectDragGestures( onDrag

    = { /* ドラッグ */ }, onDragStart = { /* ドラッグ開始 */ }, onDragEnd = { /* ドラッグ終了 */ }, onDragCancel = { /* ドラッグキャンセル */ } ) } 73 Jetpack Compose detectDragGestures ドラッグ操作の検出は可能そう それ以上の操作は全て自前でやる必要がありそう
  43. Copyright 2022 m.coder All Rights Reserved. 75 まとめ • ItemTouchHelper

    ◦ リストの並び替えを実装したい時 • Jetpack DragAndDrop ◦ リスト以外のUIでドラッグ&ドロップを実装したい時 ◦ アプリ間のドラッグ&ドロップを実装したい時 • Jetpack Composeのドラッグ&ドロップ ◦ もう少し待つ必要がありそう