簡體   English   中英

在 Android 中四向滑動的 RecyclerView

[英]RecyclerView with four way swipe in Android

如何使用四向滑動創建 RecyclerView 我在 java 中使用 MVVM 和 Room。

這是示例:

recyclerView 四向滑動

從你的 gif 中,我實現了一個RaceyclerView Demo版本。 但以同樣的方式,您可以從發布的 gif 中實現所有內容。

這個 RecyclerView 在做什么?

  • 允許向左或向右滑動並顯示新面板
  • 點擊這個面板可以處理
  • Recycler 檢測到點擊和長按
  • 當滑動小於最大尺寸的一半時,面板將折疊
  • 向右滑動選擇項目,可以選擇select最大選擇項目數
  • 可以觀察到選定的項目

它的外觀:

滑動回收站查看

這是一個很長的答案,所以首先我嘗試制作一個較短的版本來解釋一種方法。 首先,您必須為 RecyclerView 項目創建一個布局。 此布局可分為 3 個部分。 MainPanel 其中match_parent寬度和寬度為0dp的左側和右側的兩個側面板。 在為這個 RecyclerView 創建一個適配器之后,你已經為視圖設置onTouch監聽器。 onTouch function 必須檢測您何時向左/向右滑動(以顯示側面板)或頂部/底部(以滾動 Recycler)。 但它還必須檢測點擊和長按才能正確處理。 當您檢測到左右滑動時您可以更改側面板的寬度。 是一個包含完整代碼的 GitHub 存儲庫,下面我嘗試逐步給出解決方案。 (在 Kotlin 但我認為您可以輕松重構此代碼)


怎么做:

啟動項目:

  • 基於Room的數據庫。 一個名為DataViewEntity持有id 帶有標准查詢的DAO ,例如insertdeleteclearAllgetAll
  • 帶有 ViewModel 和 ViewModelFactory 的 MainActivity。 ViewModel 具有數據庫中所有 DataView 的 LiveData。 MainActivity 布局只有一個 Recycler 和用於插入新項目的按鈕。
  • 3 個圖標命名: plusdeleteselected

0.activity_main.xml:

<layout 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"
    >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.MainActivity"
        >

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/swipeRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="1dp"
            android:layout_marginEnd="1dp"
            android:splitMotionEvents="false"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/butAddCar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:src="@drawable/plus"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

文件結構:

在此處輸入圖像描述

1.在RecyclerView和CardView中添加Gradle依賴

// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'

// CardView
implementation "androidx.cardview:cardview:1.0.0"

2. Recycler 項目的克里特島漸變背景。 主視圖、刪除面板和選定面板。 (當然,它不一定是漸變,但你需要 3 個背景)

colors.xml:

<color name="recycler_left">#FFCDD2</color>
<color name="recycler_right">#C8E6C9</color>
<color name="recycler_delete_left">#E57373</color>
<color name="recycler_select_right">#81C784</color>

gradient_view.xml:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="0"
        android:endColor="@color/recycler_right"
        android:startColor="@color/recycler_left"
        android:type="linear" />

</shape>

gradient_view_delete.xml:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="0"
        android:endColor="@color/recycler_left"
        android:startColor="@color/recycler_delete_left"
        android:type="linear" />

</shape>

gradient_view_select.xml:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="0"
        android:endColor="@color/recycler_select_right"
        android:startColor="@color/recycler_right"
        android:type="linear" />

</shape>

3. 為 Recycler 項目創建一個布局。

swipe_recycler_view.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="dataView"
            type="com.myniprojects.swiperecycler.database.DataView"
            />

        <variable
            name="clickListener"
            type="com.myniprojects.swiperecycler.recycler.SwipeListener"
            />

    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="6dp"
        app:cardElevation="4dp"
        app:cardPreventCornerOverlap="false"
        >

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/rootCL"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/gradient_view"
            android:onClick="@{()-> clickListener.onClick(dataView)}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            >
            <!-- Delete panel-->
            <FrameLayout
                android:id="@+id/frameDelete"
                android:layout_width="1px"
                android:layout_height="0dp"
                android:layout_gravity="center"
                android:background="@drawable/gradient_view_delete"
                android:onClick="@{()-> clickListener.onDeleteClick(dataView)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                >

                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:adjustViewBounds="true"
                    android:contentDescription="@string/delete"
                    android:padding="10dp"
                    app:srcCompat="@drawable/delete"
                    />

            </FrameLayout>

            <!-- Select panel-->
            <FrameLayout
                android:id="@+id/frameSelect"
                android:layout_width="1px"
                android:layout_height="0dp"
                android:layout_gravity="center"
                android:background="@drawable/gradient_view_select"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                >

                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:layout_gravity="center"
                    android:adjustViewBounds="true"
                    android:contentDescription="@string/select"
                    android:padding="10dp"
                    app:srcCompat="@drawable/selected"
                    />

            </FrameLayout>

            <!-- Main panel-->
            <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/carBackground"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                app:layout_constraintEnd_toStartOf="@id/frameSelect"
                app:layout_constraintStart_toEndOf="@id/frameDelete"
                app:layout_constraintTop_toTopOf="parent"
                >

                <TextView
                    android:id="@+id/txtContent"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="16dp"
                    android:layout_marginTop="8dp"
                    android:layout_marginBottom="8dp"
                    android:text="@{Integer.toString(dataView.id)}"
                    android:textColor="#1B1919"
                    android:textSize="50sp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    />

            </androidx.constraintlayout.widget.ConstraintLayout>

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</layout>

它是如何工作的? ConstraintLayout 是整個布局的根。 它在左側和右側擁有兩個 FrameLayout,中間是另一個 ConstrintLayout。 FrameLayouts 寬度設置為 1,因此它們是不可見的。 當有人在此項目上滑動時,我們可以更改 FrameLayouth 寬度以使其可見。

4. 創建 RecyclerViewAdapter。 我將 ListAdapter 與 DiffUtil 一起使用。 適配器還需要 Class 可以處理點擊或滑動,因此還需要創建新的 Class SwipeListener。 對代碼的很多部分進行了注釋更好地理解了代碼。

import android.annotation.SuppressLint
import android.os.Handler
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.myniprojects.swiperecycler.R
import com.myniprojects.swiperecycler.database.DataView
import com.myniprojects.swiperecycler.databinding.SwipeRecyclerViewBinding
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

class SwipeRecyclerAdapter(
    private val swipeListener: SwipeListener, panelSize: Int
) : ListAdapter<DataView, SwipeRecyclerAdapter.ViewHolder>(
    SwipeDiffCallback()
)
{
    companion object
    {
        const val MAX_SELECT_NUMBER: Int = 4 // maximum number of items that user can select

        var PANEL_SIZE = 125 // delete and select panel width, the base is 125 but in the constructor we can pass new value based on DP which override this
            private set
    }

    // LiveData which holds all selected items in Recycler
    private val _selectedValues: MutableLiveData<ArrayList<Int>> = MutableLiveData()
    val selectedValues: LiveData<ArrayList<Int>>
        get() = _selectedValues

    init
    {
        PANEL_SIZE = panelSize //
        _selectedValues.value = ArrayList()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
    {
        return ViewHolder.from(
            parent,
            _selectedValues,
            swipeListener
        )
    }


    override fun onBindViewHolder(holder: ViewHolder, position: Int)
    {
        holder.bind(getItem(position)!!, swipeListener)
    }


    class ViewHolder private constructor(
        private val binding: SwipeRecyclerViewBinding,
        private val selectedItems: MutableLiveData<ArrayList<Int>>,
        private val swipeListener: SwipeListener // listener which enables to handle click etc.
    ) :
            RecyclerView.ViewHolder(binding.root), View.OnTouchListener
    {
        private var xStart = 0F // variables which track swiping in onTouch event
        private var lastY = 0F
        private var yStart = 0F
        private val handler: Handler = Handler() // Handler enable to detect long click
        private var isLongClickCanceled = false
        private var wasLongClicked = false
        private var startScrolling = false
        private var status = 0
            set(value)
            {
                field = when
                {
                    value > 0 ->
                    {
                        min(value, PANEL_SIZE)
                    }
                    value < 0 ->
                    {
                        max(value, -PANEL_SIZE)
                    }
                    else ->
                    {
                        value
                    }
                }
                setSizes()
            }

        companion object
        {
            fun from(
                parent: ViewGroup,
                selectedCar: MutableLiveData<ArrayList<Int>>,
                swipeListener: SwipeListener
            ): ViewHolder
            {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = SwipeRecyclerViewBinding.inflate(layoutInflater, parent, false)
                return ViewHolder(
                    binding, selectedCar, swipeListener
                )
            }

            private const val LONG_CLICK_TIME = 550L // time in millis to detect long click
            private const val CLICK_DISTANCE = 75 //distance in pixels to disable click/long click and enable scrolling or swiping
        }


        private val isValueSelected: Boolean
            get()
            {
                return selectedItems.value!!.contains(binding.dataView!!.id)
            }

        private val canAdd: Boolean
            get()
            {
                return selectedItems.value!!.size < MAX_SELECT_NUMBER
            }

        private fun addItem()
        {
            if (!isValueSelected)
            {
                selectedItems.value!!.add(binding.dataView!!.id)
                selectedItems.value = selectedItems.value
            }
        }

        private fun removeItem()
        {
            if (isValueSelected)
            {
                selectedItems.value!!.remove(binding.dataView!!.id)
                selectedItems.value = selectedItems.value
            }
        }


        @SuppressLint("ClickableViewAccessibility")
        fun bind(
            dataView: DataView,
            swipeListener: SwipeListener
        )
        {
            binding.dataView = dataView
            binding.clickListener = swipeListener

            binding.rootCL.setOnLongClickListener {
                swipeListener.clickLongListener(dataView.id)
                true
            }

            binding.rootCL.setOnTouchListener(this)

            status = if (isValueSelected) //value was selected, show right panel
            {
                -PANEL_SIZE
            }
            else
            {
                0
            }

            binding.executePendingBindings()
        }

        private val leftPanel = binding.rootCL.getChildAt(0)
        private val rightPanel = binding.rootCL.getChildAt(1)
        private val centerPanel = binding.rootCL.getChildAt(2)

        private fun setSizes()
        {
            when
            {
                status == 0 -> // center
                {
                    leftPanel.layoutParams.width = 1
                    rightPanel.layoutParams.width = 1
                }
                status > 0 -> //right
                {
                    leftPanel.layoutParams.width = status
                    rightPanel.layoutParams.width = 1
                }
                else -> //left
                {
                    leftPanel.layoutParams.width = 1
                    rightPanel.layoutParams.width = -status
                }
            }

            leftPanel.requestLayout()
            rightPanel.requestLayout()

            centerPanel.setBackgroundResource(R.drawable.gradient_view)
            leftPanel.setBackgroundResource(R.drawable.gradient_view_delete)
            rightPanel.setBackgroundResource(R.drawable.gradient_view_select)
        }

        // here swiping, clicking and scrolling is detected. MotionEvent is tracked and function recognize what to do
        override fun onTouch(v: View?, event: MotionEvent?): Boolean
        {
            if (v != null && event != null)
            {
                v.parent.requestDisallowInterceptTouchEvent(true)
                when (event.action)
                {
                    MotionEvent.ACTION_DOWN ->
                    {
                        xStart = event.x
                        yStart = event.y
                        isLongClickCanceled = false
                        wasLongClicked = false
                        startScrolling = false
                        handler.postDelayed({ //long click
                            wasLongClicked = true
                            v.performLongClick()
                                            }, LONG_CLICK_TIME)
                    }
                    MotionEvent.ACTION_UP ->
                    {
                        handler.removeCallbacksAndMessages(null)
                        if (!startScrolling && !isLongClickCanceled && !wasLongClicked)
                        {
                            if ((event.eventTime - event.downTime) < LONG_CLICK_TIME) //click
                            {
                                if (status == 0)
                                {
                                    v.performClick()
                                }
                                else
                                {
                                    status = 0
                                    removeItem()
                                }
                            }
                        }
                        else if (!startScrolling)
                        {
                            when
                            {
                                status > (PANEL_SIZE / 2) -> //show left
                                {
                                    status = PANEL_SIZE
                                    removeItem()
                                }
                                status < -(PANEL_SIZE / 2) -> //show right
                                {

                                    if (canAdd)//car can be added
                                    {
                                        status = -PANEL_SIZE
                                        addItem()
                                    }
                                    else
                                    {
                                        status = 0
                                        swipeListener.cannotSelectValue()
                                    }
                                }
                                else ->
                                {
                                    status = 0
                                    removeItem()
                                }
                            }
                        }
                    }
                    MotionEvent.ACTION_MOVE ->
                    {
                        if (startScrolling)
                        {
                            swipeListener.scroll((lastY - event.rawY).toInt())
                            lastY = event.rawY
                        }
                        else
                        {
                            if (!wasLongClicked)
                            {

                                if (isLongClickCanceled)
                                {
                                    val deltaX = (event.x - xStart).toInt()

                                    if (abs(deltaX) > 75)
                                    {

                                        if (deltaX > 0)
                                        {
                                            status = (deltaX - CLICK_DISTANCE)
                                        }
                                        else if (deltaX < 0)
                                        {
                                            status = (deltaX + CLICK_DISTANCE)
                                        }

                                    }
                                }
                                else if (abs(yStart - event.y) > CLICK_DISTANCE)
                                {
                                    lastY = event.rawY
                                    startScrolling = true
                                    handler.removeCallbacksAndMessages(null)
                                }
                                else if (!isLongClickCanceled && abs(xStart - event.x) >= CLICK_DISTANCE)
                                {
                                    isLongClickCanceled = true
                                    handler.removeCallbacksAndMessages(null)
                                }
                            }
                        }

                    }
                }
            }
            return true
        }
    }


}

// DiffUtil class, it helps to better calculate when to refresh Recycler
class SwipeDiffCallback : DiffUtil.ItemCallback<DataView>()
{
    override fun areItemsTheSame(oldItem: DataView, newItem: DataView): Boolean
    {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: DataView, newItem: DataView): Boolean
    {
        return oldItem == newItem
    }
}

// listener which can handle clicking, swiping, scrolling and selecting too many items
class SwipeListener(
    val clickListener: (dataViewId: Int) -> Unit,
    val clickLongListener: (dataViewId: Int) -> Unit,
    val clickDeleteListener: (dataViewId: Int) -> Unit,
    val scroll: (dy: Int) -> Unit,
    val cannotSelectValue: () -> Unit
)
{
    fun onClick(dataView: DataView) = clickListener(dataView.id)
    fun onDeleteClick(dataView: DataView) = clickDeleteListener(dataView.id)
}

5. 創建Item裝飾器,在Recycler中的item之間添加空格

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class TopSpacingItemDecoration(private val padding: Int) : RecyclerView.ItemDecoration()
{
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    )
    {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.top = padding
        outRect.bottom = padding
    }
}

6.最后一部分,在MainActivity中設置好一切

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.myniprojects.swiperecycler.R
import com.myniprojects.swiperecycler.database.AppDatabase
import com.myniprojects.swiperecycler.databinding.ActivityMainBinding
import com.myniprojects.swiperecycler.recycler.SwipeListener
import com.myniprojects.swiperecycler.recycler.SwipeRecyclerAdapter
import com.myniprojects.swiperecycler.recycler.TopSpacingItemDecoration
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity()
{
    private lateinit var viewModel: MainActivityViewModel
    private lateinit var toast: Toast
    private lateinit var binding: ActivityMainBinding

    // simple function which only show one toast without accumulation
    private fun showToast(text: Any)
    {
        if (this::toast.isInitialized)
            toast.cancel()
        toast = Toast.makeText(this, text.toString(), Toast.LENGTH_SHORT)
        toast.show()
    }

    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // Init view model
        val database = AppDatabase.getInstance(application).dataViewDAO
        val viewModelFactory = MainActivityViewModelFactory(database)
        viewModel = ViewModelProvider(this, viewModelFactory).get(MainActivityViewModel::class.java)

        // set listener to handle events like click, long click, swipe left/right, selecting too many items and scrolling
        val swipeListener = SwipeListener(
            { id -> showToast("Click $id") }, // click
            { id -> showToast("Long click $id") }, // long click
            { id ->
                viewModel.delete(id)
            }, // delete
            { dy -> swipeRecyclerView.scrollBy(0, dy) }, // scroll
            {
                showToast("You can select up to  ${SwipeRecyclerAdapter.MAX_SELECT_NUMBER}")
            }
        )

        val adapter = SwipeRecyclerAdapter(
            swipeListener,
            resources.displayMetrics.widthPixels / 8 //maximum size of left/right panel
        )

        // observe items in database and update RecyclerView
        viewModel.dataViewItems.observe(this, {
            adapter.submitList(it)
        })

        // observe selected items in RecyclerView
        adapter.selectedValues.observe(this, {
            showToast("Selected id: $it")
        })

        binding.swipeRecyclerView.adapter = adapter
        binding.swipeRecyclerView.addItemDecoration(TopSpacingItemDecoration(10)) //setting space between items in RecyclerView

        // add new value to RecyclerView
        binding.butAddCar.setOnClickListener {
            viewModel.insertNextValueToDB()
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM