0

Context: WorkoutStartFragment hosts a RecyclerView (For exercises within the workout) that hosts a RecyclerView (For sets). Each RecyclerView has their own itemTouchHelper. For the exercise itemTouchHelper (1st RecyclerView), it only allows a drag and drop feature for moving around exercises up and down to relocate them. For the set itemTouchHelper (2nd RecyclerView), it only allows a swipe feature for swiping away sets to delete them.

Issue: After dragging and dropping an exercise, followed by adding a set then removing the set via swiping, a crash occurs, giving an IndexOutOfBoundsException. This occurs within SetAdapter's removeSet (Called by onswiped) here, as position is outside of sets:

SetAdapter's itemTouchHelper (Code in WorkoutExerciseAdapter):

// Enable drag-and-drop and swipe-to-delete for sets
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        return false
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val setPosition = viewHolder.bindingAdapterPosition
        setAdapter.removeSet(setPosition)
    }
}

val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(holder.setsRecyclerView)

WorkoutExerciseAdapter's itemTouchHelper (Code in WorkoutStartFragment)

// Initialize itemTouchHelper with custom callback
val callback = AutoScrollItemTouchHelperCallback(
    adapter, recyclerView, onDragEnd = {
        updateExerciseOrder() // Update exercise order when drag ends
        adapter.setExercises(selectedExercises)
    }
)
itemTouchHelper = ItemTouchHelper(callback)
itemTouchHelper.attachToRecyclerView(recyclerView)

AutoScrollItemTouchHelperCallback:

class AutoScrollItemTouchHelperCallback(
    private val adapter: ItemTouchHelperAdapter,
    private val recyclerView: RecyclerView,
    private val onDragEnd: () -> Unit,
) : ItemTouchHelper.Callback() {

    private val scrollSpeed = 20

    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        val swipeFlags = 0
        return makeMovementFlags(dragFlags, swipeFlags)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        adapter.onItemMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
        return true
    }

    override fun isItemViewSwipeEnabled(): Boolean {
        return true
    }

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

    }

    override fun isLongPressDragEnabled(): Boolean {
        return true
    }

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

    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        viewHolder.itemView.alpha = 1.0f
        onDragEnd()
    }

    override fun onChildDraw(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && isCurrentlyActive) {
            autoScroll(viewHolder)
        }
    }

    private fun autoScroll(viewHolder: RecyclerView.ViewHolder) {
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val viewTop = viewHolder.itemView.top
        val viewBottom = viewHolder.itemView.bottom
        val parentHeight = recyclerView.height

        if (viewTop < 0) {
            recyclerView.scrollBy(0, -scrollSpeed)
        } else if (viewBottom > parentHeight) {
            recyclerView.scrollBy(0, scrollSpeed)
        }
    }

}

Demonstration:
Upon swiping a set, the following will run.

Set Adapter's RemoveSet

fun removeSet(position: Int) {
    sets.removeAt(position)
    completions.removeAt(position) // Remove corresponding completion status
    renumberSets()
    notifyItemRemoved(position)
    notifyItemRangeChanged(position, sets.size)
    listener.onSetRemoved(sets)
}

Set Adapter's Interface

class SetAdapter(
    private val sets: MutableList<Triple<Float, Int, String>>,
    private val completions: MutableList<Boolean>,
    private val listener: OnSetChangeListener,
    private val areCheckboxesEnabled: Boolean
) : RecyclerView.Adapter<SetAdapter.SetViewHolder>() {

    interface OnSetChangeListener {
        fun onSetChanged(setIndex: Int, weight: Float, reps: Int)
        fun onSetRemoved(updatedSets: List<Triple<Float, Int, String>>) //This
        fun onSetTypeChanged(setIndex: Int, setType: String)
        fun onSetCompletionChanged(setIndex: Int, isComplete: Boolean)
    }

Calling Set Adapter (From WorkoutExerciseAdapter)

override fun onBindViewHolder(holder: WorkoutExerciseViewHolder, position: Int) {
        val workoutExercise = exercises[holder.bindingAdapterPosition]

        // Set up the nested RecyclerView for sets
        val sets = workoutExercise.weights.zip(workoutExercise.reps).mapIndexed { index, pair ->
            Triple(pair.first, pair.second, workoutExercise.sets.getOrNull(index) ?: "")
        }.toMutableList()

        Log.d("WorkoutExerciseAdapter", "onBindViewHolder - Exercise: ${workoutExercise.title}, Sets: ${workoutExercise.sets}")

        val setAdapter = SetAdapter(sets, workoutExercise.completion,
        object : SetAdapter.OnSetChangeListener {
            override fun onSetChanged(setIndex: Int, weight: Float, reps: Int) {
            }

            override fun onSetRemoved(updatedSets: List<Triple<Float, Int, String>>) {
                //this
                if (holder.bindingAdapterPosition != RecyclerView.NO_POSITION) {
                    workoutExercise.sets.clear()
                    workoutExercise.weights.clear()
                    workoutExercise.reps.clear()
                    updatedSets.forEach { (weight, reps, setType) ->
                        workoutExercise.weights.add(weight)
                        workoutExercise.reps.add(reps)
                        workoutExercise.sets.add(setType)
                    }
                }
            }

This Log:

Log.d("WorkoutExerciseAdapter", "onBindViewHolder - Exercise: ${workoutExercise.title}, Sets: ${workoutExercise.sets}")

Shows that sets are correct upon dragging and dropping or anytime exercises are bound in the workoutExerciseAdapter. Within SetAdapter, sets are the same, until it comes to removeSet (Within SetAdapter). Logging removeSet shows that sets are what it was before dragging and dropping, despite
1. All sets being rebound after drag and drop
2. Adding sets also being bound after drag and drop
3. Logging Set Adapter and finding that all sets are correct in anywhere outside the removeSet function, even in other functions

My belief is that this occurs because the two itemTouchHelpers may have their functions collide with one another, or perhaps only 1 can be activated at a time. Without dragging and dropping, everything works fine. Additionally, I experimented with using a button to remove a set instead, and it works, meaning that it is likely an itemTouchHelper issue. I'm not sure how I'm supposed to fix this issue.

New contributor
Gene Lee is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.