Code Example

Kotlin Android Draggable RecyclerView Snippets

A step by step Android Draggable RecyclerView example.

1. afollestad/drag-select-recyclerview

👇 Easy Google Photos style multi-selection for RecyclerViews, powered by Kotlin and AndroidX..

drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial

Step 1: Dependency

Add the following to your module's build.gradle file:

dependencies {

  implementation 'com.afollestad:drag-select-recyclerview:2.4.0'
}

Introduction

DragSelectTouchListener is the main class of this library.
DragSelectTouchListener

DragSelectTouchListener

This library will handle drag interception and auto scroll logic - if you drag to the top of the RecyclerView, the list will scroll up, and vice versa.

Basics

DragSelectTouchListener attaches to your RecyclerViews. It intercepts touch events when it's active, and reports to a receiver which handles updating UI

val receiver: DragSelectReceiver = // ...
val touchListener = DragSelectTouchListener.create(context, receiver)

Configuration

There are a few things that you can configure, mainly around auto scroll.

DragSelectTouchListener.create(context, adapter) {
  // Configure the auto-scroll hotspot
  hotspotHeight = resources.getDimensionPixelSize(R.dimen.default_56dp)
  hotspotOffsetTop = 0 // default
  hotspotOffsetBottom = 0 // default

  // Listen for auto scroll start/end
  autoScrollListener = { isScrolling -> } 

  // Or instead of the above...
  disableAutoScroll()

  // The drag selection mode, RANGE is the default
  mode = RANGE
}

The auto-scroll hotspot is a invisible section at the top and bottom of your RecyclerView, when your finger is in one of those sections, auto scroll is triggered and the list will move up or down until you lift your finger.

If you use PATH as the mode instead of RANGE, the behavior is a bit different:

drag-select-recyclerview Example Tutorial

Compare it to the GIF at the top.

Interaction

A receiver looks like this:

class MyReceiver : DragSelectReceiver {

  override fun setSelected(index: Int, selected: Boolean) {
    // do something to mark this index as selected/unselected
    if(selected && !selectedIndices.contains(index)) {
      selectedIndices.add(index)
    } else if(!selected) {
      selectedIndices.remove(index)
    }
  }

  override fun isSelected(index: Int): Boolean {
    // return true if this index is currently selected
    return selectedItems.contains(index)
  }

  override fun isIndexSelectable(index: Int): Boolean {
    // if you return false, this index can't be used with setIsActive()
    return true
  }

  override fun getItemCount(): Int {
    // return size of your data set
    return 0
  }
}

In the sample project, our adapter is also our receiver.

val recyclerView: RecyclerView = // ...
val receiver: DragSelectReceiver = // ...

val touchListener = DragSelectTouchListener.create(context, receiver)
recyclerView.addOnItemTouchListener(touchListener) // important!!

// true for active = true, 0 is the initial selected index
touchListener.setIsActive(true, 0)

drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial
drag-select-recyclerview Example Tutorial

Full Example

Here is a full example project:

(b). Extensions.kt

Here is the full code for our Extensions.kt file:


package com.afollestad.dragselectrecyclerviewsample

import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.IntegerRes
import androidx.annotation.Px
import androidx.core.content.ContextCompat

typealias PrefsEditor = SharedPreferences.Editor

@Px internal fun Context.dimen(@DimenRes res: Int): Int {
  return resources.getDimensionPixelSize(res)
}

@ColorInt internal fun Context.color(@ColorRes res: Int): Int {
  return ContextCompat.getColor(this, res)
}

internal fun Context.integer(@IntegerRes res: Int): Int {
  return resources.getInteger(res)
}

internal fun Activity.setLightNavBarCompat() {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    var flags = window.decorView.systemUiVisibility
    flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
    window.decorView.systemUiVisibility = flags
  }
}

private var toast: Toast? = null

internal fun Context.toast(message: String) {
  toast?.cancel()
  toast = Toast.makeText(this, message, Toast.LENGTH_SHORT)
      .apply { show() }
}

(c). ItemViewHolder.kt

Here is the full code for our ItemViewHolder.kt file:


package com.afollestad.dragselectrecyclerviewsample

import android.view.View
import android.widget.TextView
import com.afollestad.recyclical.ViewHolder

data class MainItem(
  val letter: String
)

class MainViewHolder(itemView: View) : ViewHolder(itemView) {
  val colorSquare: RectangleView = itemView.findViewById(R.id.colorSquare)
  val label: TextView = itemView.findViewById(R.id.label)
}

(d). MainActivity.kt

Here is the full code for our MainActivity.kt file:


package com.afollestad.dragselectrecyclerviewsample

import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.dragselectrecyclerview.DragSelectTouchListener
import com.afollestad.dragselectrecyclerview.Mode
import com.afollestad.dragselectrecyclerview.Mode.PATH
import com.afollestad.dragselectrecyclerview.Mode.RANGE
import com.afollestad.materialcab.attached.AttachedCab
import com.afollestad.materialcab.attached.destroy
import com.afollestad.materialcab.attached.isActive
import com.afollestad.materialcab.createCab
import com.afollestad.recyclical.datasource.emptySelectableDataSource
import com.afollestad.recyclical.setup
import com.afollestad.recyclical.viewholder.isSelected
import com.afollestad.recyclical.withItem
import com.afollestad.rxkprefs.Pref
import com.afollestad.rxkprefs.rxjava.observe
import com.afollestad.rxkprefs.rxkPrefs
import io.reactivex.disposables.SerialDisposable

/** @author Aidan Follestad (afollestad) */
class MainActivity : AppCompatActivity() {
  private val list by lazy { findViewById<RecyclerView>(R.id.list) }
  private val dataSource = emptySelectableDataSource().apply {
    onSelectionChange { invalidateCab() }
  }
  private val selectionModePref: Pref<Mode> by lazy {
    rxkPrefs(this).enum(
        KEY_SELECTION_MODE,
        RANGE,
        { Mode.valueOf(it) },
        { it.name }
    )
  }

  private lateinit var touchListener: DragSelectTouchListener

  private var activeCab: AttachedCab? = null
  private var selectionModeDisposable = SerialDisposable()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(findViewById<Toolbar>(R.id.main_toolbar))

    // Setup adapter and touch listener
    touchListener = DragSelectTouchListener.create(
        this,
        dataSource.asDragSelectReceiver()
    ) {
      this.mode = selectionModePref.get()
    }

    selectionModeDisposable.set(
        selectionModePref.observe()
            .filter { it != touchListener.mode }
            .subscribe {
              touchListener.mode = it
              invalidateOptionsMenu()
            }
    )

    dataSource.set(
        ALPHABET
            .dropLastWhile { it.isEmpty() }
            .map(::MainItem)
    )

    list.setup {
      withLayoutManager(GridLayoutManager([email protected], integer(R.integer.grid_width)))
      withDataSource(dataSource)

      withItem<MainItem, MainViewHolder>(R.layout.griditem_main) {
        onBind(::MainViewHolder) { index, item ->
          label.text = item.letter
          colorSquare.setBackgroundColor(COLORS[index])

          val context = itemView.context
          var foreground: Drawable? = null
          if (isSelected()) {
            foreground = ColorDrawable(context.color(R.color.grid_foreground_selected))
            label.setTextColor(context.color(R.color.grid_label_text_selected))
          } else {
            label.setTextColor(context.color(R.color.grid_label_text_normal))
          }
          colorSquare.foreground = foreground
        }
        onClick { toggleSelection() }
        onLongClick { touchListener.setIsActive(true, it) }
      }
    }
    list.addOnItemTouchListener(touchListener)

    setLightNavBarCompat()
  }

  override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.main, menu)
    when (selectionModePref.get()) {
      RANGE -> menu.findItem(R.id.range_selection)
          .isChecked = true
      PATH -> menu.findItem(R.id.path_selection)
          .isChecked = true
    }
    return super.onCreateOptionsMenu(menu)
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
      R.id.range_selection -> selectionModePref.set(RANGE)
      R.id.path_selection -> selectionModePref.set(PATH)
    }
    return super.onOptionsItemSelected(item)
  }

  override fun onBackPressed() {
    if (!activeCab.destroy()) {
      super.onBackPressed()
    }
  }

  override fun onDestroy() {
    selectionModeDisposable.dispose()
    super.onDestroy()
  }

  private fun invalidateCab() {
    if (dataSource.hasSelection()) {
      val count = dataSource.getSelectionCount()
      if (activeCab.isActive()) {
        activeCab?.title(literal = getString(R.string.cab_title_x, count))
      } else {
        activeCab = createCab(R.id.cab_stub) {
          menu(R.menu.cab)
          closeDrawable(R.drawable.ic_close)
          titleColor(literal = Color.BLACK)
          title(literal = getString(R.string.cab_title_x, count))

          onSelection {
            if (it.itemId == R.id.done) {
              val selectionString = (0 until dataSource.size())
                  .filter { index -> dataSource.isSelectedAt(index) }
                  .joinToString()
              toast("Selected letters: $selectionString")
              dataSource.deselectAll()
              true
            } else {
              false
            }
          }

          onDestroy {
            dataSource.deselectAll()
            true
          }
        }
      }
    } else {
      activeCab.destroy()
    }
  }
}

const val KEY_SELECTION_MODE = "selection-mode"

val ALPHABET = "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
    .split(" ")
val COLORS = intArrayOf(
    Color.parseColor("#F44336"), Color.parseColor("#E91E63"),
    Color.parseColor("#9C27B0"), Color.parseColor("#673AB7"),
    Color.parseColor("#3F51B5"), Color.parseColor("#2196F3"),
    Color.parseColor("#03A9F4"), Color.parseColor("#00BCD4"),
    Color.parseColor("#009688"), Color.parseColor("#4CAF50"),
    Color.parseColor("#8BC34A"), Color.parseColor("#CDDC39"),
    Color.parseColor("#FFEB3B"), Color.parseColor("#FFC107"),
    Color.parseColor("#FF9800"), Color.parseColor("#FF5722"),
    Color.parseColor("#795548"), Color.parseColor("#9E9E9E"),
    Color.parseColor("#607D8B"), Color.parseColor("#F44336"),
    Color.parseColor("#E91E63"), Color.parseColor("#9C27B0"),
    Color.parseColor("#673AB7"), Color.parseColor("#3F51B5"),
    Color.parseColor("#2196F3"), Color.parseColor("#03A9F4")
)

(e). RectangleView.kt

Here is the full code for our RectangleView.kt file:


package com.afollestad.dragselectrecyclerviewsample

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout

/** @author Aidan Follestad (afollestad) */
class RectangleView(
  context: Context,
  attrs: AttributeSet?
) : FrameLayout(context, attrs) {

  override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
  ) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    setMeasuredDimension(measuredWidth, (measuredWidth * 1.4f).toInt())
  }
}

(f). RecyclicalTranslator.kt

Here is the full code for our RecyclicalTranslator.kt file:


package com.afollestad.dragselectrecyclerviewsample

import com.afollestad.dragselectrecyclerview.DragSelectReceiver
import com.afollestad.recyclical.datasource.SelectableDataSource

fun SelectableDataSource<*>.asDragSelectReceiver(): DragSelectReceiver {
  return object : DragSelectReceiver {
    override fun getItemCount(): Int = size()

    override fun setSelected(
      index: Int,
      selected: Boolean
    ) {
      if (selected) selectAt(index) else deselectAt(index)
    }

    override fun isSelected(index: Int): Boolean = isSelectedAt(index)

    override fun isIndexSelectable(index: Int): Boolean = true
  }
}

(g). griditem_main.xml

Here is the full code for our griditem_main.xml file:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="4dp"
    >

  <com.afollestad.dragselectrecyclerviewsample.RectangleView
      android:id="@+id/colorSquare"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      >

    <TextView
        android:id="@+id/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:fontFamily="sans-serif-medium"
        android:gravity="center"
        android:textColor="#fff"
        android:textSize="@dimen/main_label_textsize"
        android:textStyle="bold|italic"
        tools:ignore="UnusedAttribute"
        tools:text="A"
        />

  </com.afollestad.dragselectrecyclerviewsample.RectangleView>

</FrameLayout>

(h). activity_main.xml

Here is the full code for our activity_main.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

  <FrameLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="?colorPrimary"
      android:elevation="@dimen/mcab_toolbar_elevation"
      tools:ignore="UnusedAttribute"
      >

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/main_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        />

    <ViewStub
        android:id="@+id/cab_stub"
        android:layout_width="match_parent"
        android:layout_height="?actionBarSize"
        />

  </FrameLayout>

  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/list"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:scrollbars="vertical"
      />

</LinearLayout>

Reference

You can DOWNLOAD FULL CODE.
You can also browse code or read more here.
Follow code author here.


Read More.

Related Posts