【问题标题】:RecyclerView with four way swipe in Android在 Android 中具有四向滑动的 RecyclerView
【发布时间】:2020-10-17 07:35:34
【问题描述】:

如何创建 RecyclerView 四向滑动?我在 java 中使用 MVVM 和 Room。

这是示例:

【问题讨论】:

    标签: java android android-layout mvvm android-recyclerview


    【解决方案1】:

    从你的 gif 图中,我实现了一个版本 RaceyclerView Demo。但以同样的方式,您可以从发布的 gif 中实现所有内容。

    这个 RecyclerView 在做什么?

    • 允许向左或向右滑动并显示新面板
    • 点击此面板即可处理
    • Recycler 检测到点击和长按
    • 当滑动不到最大尺寸的一半时,面板将折叠
    • 右滑选择item,可以选择最多选中的item数
    • 可以观察所选项目

    外观:

    这是一个非常长的答案,所以首先我尝试制作一个较短的版本来解释一种方法。首先,您必须为 RecyclerView 项目创建一个布局。此布局可分为 3 个部分。 MainPanel 其中match_parent 宽度和左右两个宽度为0dp 的侧面板。在您为此 RecyclerView 创建适配器后,您已为视图设置了 onTouch 侦听器。这个onTouch 函数必须检测您何时向左/向右滑动(以显示侧面板)或顶部/底部(以滚动 Recycler)。但它还必须检测点击和长按才能正确处理。当您检测到左右滑动时您可以更改侧面板的宽度。 Here 是一个包含完整代码的 GitHub 存储库,下面我尝试逐步给出解决方案。 (在 Kotlin 中,但我认为您可以轻松重构此代码)


    怎么做:

    开始项目:

    • 基于Room的数据库。一个Entity 称为DataView,其中包含idDAOinsertdeleteclearAllgetAll 等标准查询
    • 带有 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.将 Gradle 依赖添加到 RecyclerView 和 CardView

    // 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 一起使用。适配器还需要可以处理单击或滑动的类,因此还需要创建新的类 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.创建项目装饰器以在 Recycler 中的项目之间添加空间

    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()
            }
        }
    }
    

    【讨论】:

    • 非常感谢您的完整解释
    猜你喜欢
    • 2018-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多