Groupie 基本系からドラッグ&ドロップ、スワイプなど

こんにちはめっしーです(Twitter：https://twitter.com/messi_0601)

最近JetpackComposeが世間的には話題になってますが、あえてGroupieを触れたので一連の使い方まとめました。


# Groupieとは
複雑なRecyclerViewレイアウト用のシンプルで柔軟なライブラリ

https://github.com/lisawray/groupie


# 導入
## gradle

```kotlin
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-databinding:$groupie_version"
//databindingとviewbindingを両方利用している場合は以下を使う
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
```

```kotlin
allprojects {
  repositories {
    maven { url "https://jitpack.io" }
  }
}
```

## xml

activityかfragmentのxmlにRecyclerView追加

```xml
<androidx.recyclerview.widget.RecyclerView
      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" />
```

次にitemに表示させたいviewの作成
今回はtextのみを表示する

``` xml
<?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">

  <data>
    <variable
      name="itemLabel"
      type="String" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="100dp">

    <TextView
      android:id="@+id/textView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{itemLabel}"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />
  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```


## Itemクラス

viewbindingを利用の際には`initializeViewBinding`必要

```kotlin
class Item(private val itemLabel: String) : BindableItem<HogeBinding>() {
  override fun bind(viewBinding: ModelHogeItemBinding, position: Int) {
    viewBinding.itemLabel = itemLabel
  }

  override fun getLayout(): Int = R.layout.model_hoge_item

  //viewbindingの際には必要
  override fun initializeViewBinding(view: View): ModelHogeItemBinding {
    return ModelHogeItemBinding.bind(view)
  }
}
```


## activity/fragment

databinding想定

```kotlin
val adapter = GroupieAdapter()
with(binding) {
  recyclerView.adapter = adapter
  recyclerView.layoutManager = LinearLayoutManager(context)
}
```

**adapterに対してdateの渡し方**

- **addを使う**

Itemの1つ1つ入れていく形

livedateを使用してる。

```kotlin
viewModel.itemList.observe(viewLifecycleOwner) {
  for (item in it) {
    adapter.add(Item(item))
	}
}
```

- **updateを使う**

updateにはItemクラスのcollectionが入る

```kotlin
val hogeList = listOf<Group>(
  Item("test1"),
  Item("test2"),
  Item("test3")
)
adapter.update(hogeList)
```

# click処理について

## Item自体のクリック

`OnItemClickListener`を継承でつける

```kotlin
class GroupieCarouselItem(
  private val colorInt: Int,
  private val adapter: GroupieAdapter
) : BindableItem<ModelGroupieCarouselItemBinding>(), OnItemClickListener {

  //bindとgetLayout省略してますが必要です。
  
  //以下2つを追加
  init {
    adapter.setOnItemClickListener(this)
  }
  override fun onItemClick(item: Item<*>, view: View) {
    //ここにクリックした際の処理をかく
  }
}

```

## アイテムの中にあるviewでクリック処理
**Itemクラスのbindで定義する。**

interfaceでListenerを作り、bindで指定する。
ここではItem内にinterfaceを作成した。

```kotlin
class GroupieItem(
  private val playgroundModel: GroupiePlaygroundModel,
  private val listener: Listener
) : BindableItem<ModelGroupieItemBinding>() {

  interface Listener {
    fun onItemClick()
  }

  override fun bind(viewBinding: ModelGroupieItemBinding, position: Int) {
    viewBinding.toGroupieView.setOnClickListener {
      listener.onItemClick()
    }
  }
}
```
fragmentにGroupieItem.Listenerを継承で渡す。

```kotlin

class GroupieFragment : Fragment(), GroupieItem.Listener {
  override fun onItemClick() {
    //クリック処理をかく
  }
}
```

# データの削除

```kotlin
adapter.remove(item)
```

# 様々な種類のGroupie
Groupieの種類を紹介していきます。

## HeaderとBodyアイテムがあるタイプ
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188385/261ac4b5-b278-1e9b-8088-9e44cd64feab.gif)

``` kotlin
val basicHeaderItem = listOf("フルーツ","動物")
val basicFruitsItems = listOf("りんご","みかん","ばなな","メロン","スイカ","ぶどう","マスカット")
val basicAnimalItems = listOf("ぞう","キリン","ペンギン","ライオン")

private val _itemList = MutableLiveData<List<Group>>()
val itemList: LiveData<List<Group>> = _itemList

fun fetchData() {
  val list = mutableListOf<Group>()
  val bodyList = listOf(
    groupieSample.basicFruitsItems,
    groupieSample.basicAnimalItems
  )
  //ここでlistに表示したい順に入れていく
  for ((index, title) in groupieSample.basicHeaderItem.withIndex()) {
    list += HeaderItem(title)
    for (item in bodyList[index]) {
      list += BodyItem(item)
    }
  }
  _itemList.value = list
}
```

## Carousel
![Videotogif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188385/a41094cb-89d1-7885-978a-090b977650ba.gif)
`layoutManager`を`LinearLayoutManager.HORIZONTAL`に変える

```kotlin
binding.recyclerView.layoutManager =
        LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
```


## スワイプ削除とドラッグ移動

![Videotogif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188385/00776829-f398-6b17-2b6d-09260beaae6d.gif)

### Itemクラス

**スワイプ動作**
スワイプ方向を返す`getSwipeDirs()`を追加

**ドラッグ移動**
ドラッグ方向を返す`getDragDirs()`を追加

```kotlin
class GroupieSwipeItem() : BindableItem<ModelGroupieSwipeItemBinding>() {

  override fun bind(viewBinding: ModelGroupieSwipeItemBinding, position: Int) {}
  override fun getLayout(): Int = R.layout.model_groupie_swipe_item
  //追加
  override fun getSwipeDirs(): Int = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
  override fun getDragDirs(): Int = ItemTouchHelper.UP or ItemTouchHelper.DOWN
}
```

### fragment/activity

```kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)
  //...

  //追加：recyclerViewでドラッグ&ドロップ移動やスワイプ動作をする際に加える
  ItemTouchHelper(itemTouchHelper).attachToRecyclerView(binding.recyclerView)
}

private val itemTouchHelper: TouchCallback by lazy {
  object : TouchCallback() {
    override fun onMove(
      recyclerView: RecyclerView,
      viewHolder: RecyclerView.ViewHolder,
      target: RecyclerView.ViewHolder
    ): Boolean  {
      val fromPosition = viewHolder.adapterPosition
      val toPosition = target.adapterPosition
      binding.recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition)
      return false
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
      val item = adapter.getItem(viewHolder.adapterPosition)
      adapter.remove(item)
    }
  }
}
```

**TouchCallbackについて**
ドラッグとスワイプの方向で作成できるデフォルトのコールバックの単純なラッパー。
このクラスはフラグのコールバックを処理します。ユースケースに応じて、onMoveまたはonSwipedをオーバーライドする必要があります。
https://developer.android.com/reference/androidx/recyclerview/widget/ItemTouchHelper.SimpleCallback?hl=ja

#### onMove

```kotlin
override fun onMove(
  recyclerView: RecyclerView,
  viewHolder: RecyclerView.ViewHolder,
  target: RecyclerView.ViewHolder
): Boolean {
  //adapterPositionでアイテムのポジションをIntで取ってる
  val fromPosition = viewHolder.adapterPosition
  val toPosition = target.adapterPosition
  binding.recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition)
  return false
}
```

ドラッグアンドドロップが完了した際に呼び出されるメソッド

|引数 |説明  |
|---|---|
|recyclerView  |ドラッグアンドドロップが発生したRecyclerViewのオブジェクト。  |
|viewHolder  |開始時のオブジェクト。開始時のポジション情報を保持している。  |
|target  |完了時のオブジェクト。完了時のポジション情報を保持している。  |


最後に返り値として正常にドラッグアンドドロップが終了した際にtrueかfalseを返す
以下ドキュメント

```kotlin
/**
* @return True if the {@code viewHolder} has been moved to the adapter position of
* {@code target}.
* @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
*/
```

上記のに書いてあるようにtureの場合`onMoved`メソッドが呼び出される
呼び出す必要がない場合は`false`で問題ない。


#### onSwiped

```kotlin
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
  val item = adapter.getItem(viewHolder.adapterPosition)
  //スワイプでアイテムを消してる
  adapter.remove(item)
}
```

|引数 |説明  |
|---|---|
|viewHolder  |スワイプされたViewHolderのオブジェクト  |
| direction  |viewHolderがスワイプされる方向をとる  |



# サンプル
こちらは自分が行ってきた技術サンプルが詰まってます。
その中のGroupieフォルダーに今回のコードが書かれてあります。ぜひご覧ください。

https://github.com/175atsu/AndroidTechSample

# 参考
https://github.com/lisawray/groupie

https://qiita.com/takahirom/items/4125d7c871fad534d3c2

https://qiita.com/zoothezoo/items/487c3c90869658448307


# おわり

ではまた次回の記事で
 Twitterフォローも是非よろしくお願いします！
https://twitter.com/messi_0601