Android - Swipe layout

Chẳng là mình đang tập tành code kotlin cho android, app của mình cho phép người dùng swipe item trái phải các kiểu và thực hiện các action ứng với mỗi trường hợp người dùng vuốt trái, phải.

Sau một cả ngày mò mẫm thì mình tìm được một thư viện hỗ trợ khá tốt và đáp ứng được nhu cầu của mình - AndroidSwipeLayout

Cài đặt

Gradle: các bạn có thể thay đổi version của recyclerview theo như mình mong muốn

dependencies {
    compile 'com.android.support:recyclerview-v7:21.0.0'
    compile 'com.android.support:support-v4:20.+'
    compile "com.daimajia.swipelayout:library:[email protected]"
}

Maven

<dependency>
	<groupId>com.google.android</groupId>
	<artifactId>support-v4</artifactId>
	<version>r6</version>
</dependency>
<dependency>
	<groupId>com.google.android</groupId>
	<artifactId>recyclerview-v7</artifactId>
	<version>21.0.0</version>
</dependency>
<dependency>
    <groupId>com.daimajia.swipelayout</groupId>
    <artifactId>library</artifactId>
    <version>1.2.0</version>
    <type>apklib</type>
</dependency>

Sử dụng

Nguyên lý cơ bản của thư viện này là nó sẽ có 2 layer phục vụ cho việc hiển thị, SurfaceView chính là layer ở phía trên mà bạn vuốt, BottomView là layer ở phía dưới sẽ được hiển thị khi bạn vuốt

<com.daimajia.swipe.SwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="80dp">
    <!-- Bottom View Start-->
     <LinearLayout
        android:background="#66ddff00"
        android:id="@+id/bottom_wrapper"
        android:layout_width="160dp"
        android:weightSum="1"
        android:layout_height="match_parent">
        <!--What you want to show-->
    </LinearLayout>
    <!-- Bottom View End-->

    <!-- Surface View Start -->
     <LinearLayout
        android:padding="10dp"
        android:background="#ffffff"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--What you want to show in SurfaceView-->
    </LinearLayout>
    <!-- Surface View End -->
</com.daimajia.swipe.SwipeLayout>

Đoạn code ở trên mô tả cấu trúc cơ bản của SwipeLayout, ở phần BottomView, bạn có thể đặt bao nhiêu item cũng được, chỉ cần chú ý rằng SurfaceView chính là layer cuối cùng của SwipeLayout.

Các bạn có thể tham khảo thêm ở đây: swipe_item.xml

Thư viện này hỗ trợ 2 kiểu swipe: LayDown hoặc PullOut, đồng thời hỗ trợ xác định hướng vuốt: LEFT, RIGHT, TOP, BOTTOM

  1. LayDown

    LayDown

  2. PullOut

    PullOut

Sau khi có SwipeLayout, chúng ta cần cài đặt cho nó.

Dưới đây là một ví dụ sử dụng SwipeLayout cho RecyclerView, chúng ta có một adapter đơn giản có sử dụng callback. Nếu các bạn chỉ sử dụng nó cho một item nào đó thì chỉ cần addSwipeListener và set showMode cho nó là xong

class RecyclerSwipeViewAdapter(
    private val items: List<Item>,
    private val mListener: OnSwipeItem
) : RecyclerSwipeAdapter<RecyclerSwipeViewAdapter.SimpleViewHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.swipe_item, parent, false)
        return SimpleViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: SimpleViewHolder, position: Int) {
        val item = items[position]
        viewHolder.bindItem(item, position)
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun getSwipeLayoutResourceId(position: Int): Int {
        return R.id.swipe
    }

    inner class SimpleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val text: TextView = itemView.text_view
        private val swipeLayout: SwipeLayout = itemView.swipe

        init {
            swipeLayout.addDrag(SwipeLayout.DragEdge.Right, itemView.findViewById(R.id.bottom_right))
            swipeLayout.addDrag(SwipeLayout.DragEdge.Left, itemView.findViewById(R.id.bottom_left))
        }

        fun close() {
            swipeLayout.close()
        }

        fun bindItem(item: Item, position: Int) {
            text.text = item.text

            // Set swipe style
            swipeLayout.showMode = SwipeLayout.ShowMode.PullOut

            // Set listener
            swipeLayout.addSwipeListener(object : SwipeLayout.SwipeListener {
                override fun onClose(layout: SwipeLayout) { }

                override fun onUpdate(layout: SwipeLayout, leftOffset: Int, topOffset: Int) { }

                override fun onStartOpen(layout: SwipeLayout) {}

                override fun onOpen(layout: SwipeLayout) { }

                override fun onStartClose(layout: SwipeLayout) { }

                override fun onHandRelease(layout: SwipeLayout, xvel: Float, yvel: Float) {
                    val edge = layout.dragEdge.name
                    if (layout.openStatus.toString() !== "Close") {
                        when (edge) {
                            SwipeLayout.DragEdge.Right.name -> {
                                // Drag RIGHT
                                mListener.onSwipeRight(item)
                            }
                            SwipeLayout.DragEdge.Left.name -> {
                                // Drag LEFT
                                mListener.onSwipeLeft(item)
                            }
                            SwipeLayout.DragEdge.Top.name -> {
                                // Drag TOP
                                mListener.onSwipeTop(item)
                            }
                            SwipeLayout.DragEdge.Bottom.name -> {
                                // Drag BOTTOM
                                mListener.onSwipeBottom(item)
                            }
                        }
                    }
                }
            })

            text.setOnClickListener {
                mListener.onClickItem(item)
            }
        }
    }

    interface OnSwipeItem {
        fun onSwipeLeft(item: Item)
        fun onSwipeRight(item: Item)
        fun onSwipeTop(item: Item)
        fun onSwipeBottom(item: Item)
        fun onClickItem(item: Item)
    }
}

Trong Activity/Fragment:

private fun setAdapter(items: List<Item>) {
    swipeAdapter = RecyclerSwipeViewAdapter(items, object: RecyclerSwipeViewAdapter.OnSwipeItem {
        override fun onSwipeLeft(item: Item) {}
        override fun onSwipeRight(item: Item) {}
        override fun onSwipeTop(item: Item) {}
        override fun onSwipeBottom(item: Item) {}
        override fun onClickItem(item: Item) {}
    })

    recycler_view.apply {
        layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
        addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
        adapter = swipeAdapter
    }
}

Note: nếu ứng dụng của bạn chỉ cần nhận diện việc người dùng vuốt theo hướng nào mà ko cần phải hiển thị phần BottomView như ở trên thì bạn có thể tham khảo qua đoạn code dưới đây

abstract class OnSwipeTouchListener(context: Context) : View.OnTouchListener {
    private val gestureDetector: GestureDetector

    init {
        gestureDetector = GestureDetector(context, GestureListener())
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
        private val SWIPE_THRESHOLD = 100
        private val SWIPE_VELOCITY_THRESHOLD = 100

        override fun onDown(e: MotionEvent): Boolean {
            return true
        }

        override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
            var result = false
            try {
                val diffY = e2.y - e1.y
                val diffX = e2.x - e1.x
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                        if (diffX > 0) {
                            onSwipeRight()
                        } else {
                            onSwipeLeft()
                        }
                        result = true
                    }
                } else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
                    if (diffY > 0) {
                        onSwipeBottom()
                    } else {
                        onSwipeTop()
                    }
                    result = true
                }
            } catch (exception: Exception) {
                exception.printStackTrace()
            }

            return result
        }
    }

    abstract fun onSwipeRight()
    abstract fun onSwipeLeft()
    abstract fun onSwipeTop()
    abstract fun onSwipeBottom()
}

Ở đoạn code trên, chúng ta sử dụng GestureDetector.SimpleOnGestureListener để xác định hướng mà người dùng vuốt. Tiếp theo trong Activity/Fragment chúng ta chỉ cần implement 4 phương thức abstract là xong

text_view.setOnTouchListener(object : OnSwipeTouchListener(this) {
    override fun onSwipeRight() {}

    override fun onSwipeLeft() {}

    override fun onSwipeTop() {}

    override fun onSwipeBottom() {}
})