Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add item type builder and enable drag to reorder #2

Merged
merged 2 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.example.compose_recyclerview

import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
Expand All @@ -12,11 +11,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.END
import androidx.recyclerview.widget.ItemTouchHelper.START
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.compose_recyclerview.adapter.ComposeRecyclerViewAdapter
import com.example.compose_recyclerview.data.LayoutOrientation

import com.example.compose_recyclerview.utils.InfiniteScrollListener

/**
* Composable function to display a RecyclerView with dynamically generated Compose items.
Expand All @@ -26,6 +30,12 @@ import com.example.compose_recyclerview.data.LayoutOrientation
* @param itemBuilder The lambda function responsible for creating the Compose content for each item at the specified index.
* @param onScrollEnd Callback triggered when the user reaches the end of the list during scrolling.
* @param orientation The layout direction of the RecyclerView.
* @param itemTypeBuilder The optional lambda function to determine the type of each item.
* * Required for effective drag and drop. Provide a non-null [ComposeRecyclerViewAdapter.ItemTypeBuilder] when enabling drag and drop functionality.
* * Useful when dealing with multiple item types, ensuring proper handling and layout customization for each type.
* @param onDragCompleted Callback triggered when an item drag operation is completed.
* @param onItemMove Callback triggered when an item is moved within the RecyclerView.
* @param onCreate Callback to customize the RecyclerView after its creation.
*/
@Composable
fun ComposeRecyclerView(
Expand All @@ -34,14 +44,17 @@ fun ComposeRecyclerView(
itemBuilder: @Composable (index: Int) -> Unit,
onScrollEnd: () -> Unit = {},
orientation: LayoutOrientation = LayoutOrientation.Vertical,
onCreate: (RecyclerView) -> Unit? = {}
itemTypeBuilder: ComposeRecyclerViewAdapter.ItemTypeBuilder? = null,
onDragCompleted: (position: Int) -> Unit = { _ -> },
onItemMove: (fromPosition: Int, toPosition: Int, itemType: Int) -> Unit = { _, _, _ -> },
onCreate: (RecyclerView) -> Unit = {}
) {
val context = LocalContext.current
var scrollState by rememberSaveable { mutableStateOf(bundleOf()) }

val layoutManager = remember {
val layoutManager = LinearLayoutManager(context)
val parcelableState = scrollState.getParcelable<Parcelable?>("RecyclerviewState")
layoutManager.onRestoreInstanceState(parcelableState)
layoutManager.onRestoreInstanceState(scrollState.getParcelable("RecyclerviewState"))
layoutManager.orientation = when (orientation) {
LayoutOrientation.Horizontal -> RecyclerView.HORIZONTAL
LayoutOrientation.Vertical -> RecyclerView.VERTICAL
Expand All @@ -53,13 +66,15 @@ fun ComposeRecyclerView(
ComposeRecyclerViewAdapter().apply {
this.totalItems = itemCount
this.itemBuilder = itemBuilder
itemTypeBuilder?.let {
this.itemTypeBuilder = itemTypeBuilder
}
this.layoutOrientation = orientation
}
}

val composeRecyclerView = remember {
RecyclerView(context).apply {
onCreate.invoke(this)
this.layoutManager = layoutManager
addOnScrollListener(object : InfiniteScrollListener() {
override fun onScrollEnd() {
Expand All @@ -70,46 +85,71 @@ fun ComposeRecyclerView(
}
}

val itemTouchHelper = remember {
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromType = adapter.getItemViewType(viewHolder.bindingAdapterPosition)
val toType = adapter.getItemViewType(target.bindingAdapterPosition)

if (fromType != toType) {
return false
}

adapter.onItemMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
onItemMove.invoke(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition,
fromType
)
return true
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { }

override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
viewHolder.itemView.alpha = 1f
onDragCompleted.invoke(viewHolder.bindingAdapterPosition)
}

override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}

override fun isLongPressDragEnabled(): Boolean {
return true
}
})
}

// Use AndroidView to embed the RecyclerView in the Compose UI
AndroidView(
factory = { composeRecyclerView },
factory = {
composeRecyclerView.apply {
onCreate.invoke(this)
itemTypeBuilder?.let {
itemTouchHelper.attachToRecyclerView(this)
}
}
},
modifier = modifier,
update = {
adapter.totalItems = itemCount
adapter.itemBuilder = itemBuilder
adapter.layoutOrientation = orientation
adapter.update(itemCount, itemBuilder, orientation, itemTypeBuilder)
}
)


DisposableEffect(key1 = Unit, effect = {
onDispose {
scrollState = bundleOf("RecyclerviewState" to layoutManager.onSaveInstanceState())
}
})
}

/**
* Abstract class for handling infinite scrolling events in a RecyclerView.
*/
abstract class InfiniteScrollListener : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0 || dx > 0) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val pastVisibleItems = layoutManager.findFirstVisibleItemPosition()
if (visibleItemCount + pastVisibleItems >= totalItemCount) {
onScrollEnd()
}
}
}
}

/**
* Callback triggered when the user reaches the end of the list during scrolling.
*/
protected abstract fun onScrollEnd()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import com.example.compose_recyclerview.data.LayoutOrientation
/**
* RecyclerView adapter for handling dynamically generated Compose items.
*/
internal class ComposeRecyclerViewAdapter :
RecyclerView.Adapter<ComposeRecyclerViewAdapter.ComposeRecyclerViewHolder>() {
class ComposeRecyclerViewAdapter :
RecyclerView.Adapter<ComposeRecyclerViewAdapter.ComposeRecyclerViewHolder>(){

interface ItemTypeBuilder {
fun getItemType(position: Int): Int
}

var totalItems: Int = 0
set(value) {
Expand All @@ -25,7 +29,10 @@ internal class ComposeRecyclerViewAdapter :
}
}

var itemBuilder: (@Composable (index: Int) -> Unit)? = null
var itemBuilder: (@Composable (index: Int) -> Unit)? =
null

var itemTypeBuilder: ItemTypeBuilder? = null

var layoutOrientation: LayoutOrientation = LayoutOrientation.Vertical
set(value) {
Expand All @@ -34,6 +41,7 @@ internal class ComposeRecyclerViewAdapter :
notifyItemChanged(0)
}


inner class ComposeRecyclerViewHolder(val composeView: ComposeView) :
RecyclerView.ViewHolder(composeView)

Expand All @@ -44,17 +52,20 @@ internal class ComposeRecyclerViewAdapter :
}

override fun onBindViewHolder(holder: ComposeRecyclerViewHolder, position: Int) {
holder.composeView.setContent {
when (layoutOrientation) {
LayoutOrientation.Horizontal -> {
Row {
itemBuilder?.invoke(position)
holder.composeView.apply {
tag = holder
setContent {
when (layoutOrientation) {
LayoutOrientation.Horizontal -> {
Row {
itemBuilder?.invoke(position)
}
}
}

LayoutOrientation.Vertical -> {
Column {
itemBuilder?.invoke(position)
LayoutOrientation.Vertical -> {
Column {
itemBuilder?.invoke(position)
}
}
}
}
Expand All @@ -64,6 +75,24 @@ internal class ComposeRecyclerViewAdapter :
override fun getItemCount(): Int = totalItems

override fun getItemViewType(position: Int): Int {
return position
return itemTypeBuilder?.getItemType(position) ?: 0
}

fun onItemMove(fromPosition: Int, toPosition: Int) {
notifyItemMoved(fromPosition, toPosition)
}

fun update(
itemCount: Int,
itemBuilder: @Composable (index: Int) -> Unit,
layoutOrientation: LayoutOrientation,
itemTypeBuilder: ItemTypeBuilder?
) {
this.totalItems = itemCount
this.itemBuilder = itemBuilder
this.layoutOrientation = layoutOrientation
itemTypeBuilder?.let {
this.itemTypeBuilder = it
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.compose_recyclerview.utils

import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

/**
* Abstract class for handling infinite scrolling events in a RecyclerView.
*/
abstract class InfiniteScrollListener : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0 || dx > 0) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val pastVisibleItems = layoutManager.findFirstVisibleItemPosition()
if (visibleItemCount + pastVisibleItems >= totalItemCount) {
onScrollEnd()
}
}
}
}

/**
* Callback triggered when the user reaches the end of the list during scrolling.
*/
protected abstract fun onScrollEnd()
}
Loading