こんにちはめっしーです(Twitter:https://twitter.com/messi_0601)
最近JetpackComposeが世間的には話題になってますが、あえてGroupieを触れたので一連の使い方まとめました。
Groupieとは
複雑なRecyclerViewレイアウト用のシンプルで柔軟なライブラリ
導入
gradle
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"
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
xml
activityかfragmentのxmlにRecyclerView追加
<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 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
必要
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想定
val adapter = GroupieAdapter()
with(binding) {
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(context)
}
adapterに対してdateの渡し方
- addを使う
Itemの1つ1つ入れていく形
livedateを使用してる。
viewModel.itemList.observe(viewLifecycleOwner) {
for (item in it) {
adapter.add(Item(item))
}
}
- updateを使う
updateにはItemクラスのcollectionが入る
val hogeList = listOf<Group>(
Item("test1"),
Item("test2"),
Item("test3")
)
adapter.update(hogeList)
click処理について
Item自体のクリック
OnItemClickListener
を継承でつける
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を作成した。
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を継承で渡す。
class GroupieFragment : Fragment(), GroupieItem.Listener {
override fun onItemClick() {
//クリック処理をかく
}
}
データの削除
adapter.remove(item)
様々な種類のGroupie
Groupieの種類を紹介していきます。
HeaderとBodyアイテムがあるタイプ
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
layoutManager
をLinearLayoutManager.HORIZONTAL
に変える
binding.recyclerView.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
スワイプ削除とドラッグ移動
Itemクラス
スワイプ動作
スワイプ方向を返すgetSwipeDirs()
を追加
ドラッグ移動
ドラッグ方向を返すgetDragDirs()
を追加
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
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
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を返す
以下ドキュメント
/**
* @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
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val item = adapter.getItem(viewHolder.adapterPosition)
//スワイプでアイテムを消してる
adapter.remove(item)
}
引数 | 説明 |
---|---|
viewHolder | スワイプされたViewHolderのオブジェクト |
direction | viewHolderがスワイプされる方向をとる |
サンプル
こちらは自分が行ってきた技術サンプルが詰まってます。
その中のGroupieフォルダーに今回のコードが書かれてあります。ぜひご覧ください。
参考
おわり
ではまた次回の記事で
Twitterフォローも是非よろしくお願いします!
https://twitter.com/messi_0601