DEV Community

Cover image for [Bahasa] Implementasi Draggable View di Android
Muhamad Wahyudin
Muhamad Wahyudin

Posted on

[Bahasa] Implementasi Draggable View di Android

Alkisah ketika sedang mendevelop prototipe UI mesin vending powerbank ReCharge yang dapat menampilkan iklan layar penuh, ada kebutuhan untuk suatu elemen / widget yang bisa dipindah-pindah di layar. Waktu itu yang terbayang adalah chat head nya Facebook dan lucky egg nya Tokopedia.

Mockup prototipe UI layar penuh

Mockup prototipe UI layar penuh

Facebook Chat Heads

Facebook Chat Heads. Jaman-jaman Holo theme itu udah “kekinian”.

Lucky Egg Tokopedia

Lucky Egg Tokopedia

Bare Minimum

private var widgetDX: Float = 0F   
private var widgetDY: Float = 0F   

fun setupStickyDraggable(){   
    iv_sticky_draggable.setOnTouchListener { v, event ->   
        when(event.actionMasked){   
            MotionEvent.ACTION_DOWN -> {   
                widgetDX = v.x - event.rawX   
                widgetDY = v.y - event.rawY   
            }   
            MotionEvent.ACTION_MOVE -> {   
                v.x = event.rawX + widgetDX   
                v.y = event.rawY + widgetDY   
            }   
            else -> {   
                return@setOnTouchListener false   
            }   
        }   
        true   
    }   
}   

Yang pertama adalah bare minimum, tujuannya disini agar view bisa digeser-geser dulu.

Disini kita set setOnTouchListener pada sebuah view iv_sticky_draggable (dalam kasus ini ImageView). Di setOnTouchListener ada dua parameter yaitu v (view itu sendiri) dan event.

Event yang kita butuhkan adalah ACTION_DOWN & ACTION_MOVE. Di line 8–9 kita simpan dulu koordinat dari view dikurangi koordinat absolut titik sentuh ke widgetDX & widgetDY (ketika ACTION_DOWN).

v.x & v.y adalah posisi relative view terhadap koordinat awal view.

event.rawX & event.rawY adalah koordinat absolut dari titik sentuh pada layar

Ketika ACTION_MOVE (line 12–13), kita update koordinat view dengan nilai yang sudah disimpan tadi, ditambah dengan nilai pergerakkan titik sentuh di layar, dengan begitu si view tersebut akan tergeser mengikuti sentuhan jari kita dilayar. That’s it.

It’s moving! Tapi tembus-tembus.

It’s moving! Tapi tembus-tembus.

Screen Border Collision

private var widgetDX: Float = 0F
private var widgetDY: Float = 0F

fun setupStickyDraggable(){
    iv_sticky_draggable.setOnTouchListener { v, event ->
        val viewParent:View = (v.parent as View)
        val PARENT_HEIGHT = viewParent.height
        val PARENT_WIDTH = viewParent.width

        when(event.actionMasked){
            MotionEvent.ACTION_DOWN -> {
                widgetDX = v.x - event.rawX
                widgetDY = v.y - event.rawY
            }
            MotionEvent.ACTION_MOVE -> {
                // Screen border Collision
                var newX = event.rawX + this.widgetDX
                newX = Math.max(0F, newX)
                newX = Math.min((PARENT_WIDTH - v.width).toFloat(), newX)
                v.x = newX

                var newY = event.rawY + this.widgetDY
                newY = Math.max(0F, newY)
                newY = Math.min((PARENT_HEIGHT - v.height).toFloat(), newY)
                v.y = newY
            }
            else -> {
                return@setOnTouchListener false
            }
        }
        true
    }
}

Oke viewnya sudah bisa bergeser, tapi masalahnya, view itu bisa hilang dari layar kalau kita geser ke tepian layar. Solusinya adalah dengan membatasi nilai X dan Y dalam rentang 0 hingga nilai maksimal Width dan Height parent dari view tersebut.

Jadi pertama kita ambil nilai Width dan Height (line 6–8). Kita harus cast parent dari view menjadi View agar dapat mengakses properties Width & Height-nya.

Disini saya batasi dengan bantuan Math.max() & Math.min(). Line 18 & 23 artinya bila nilai negatif maka akan dipaksa menjadi 0, sedangkan line 19 & 24 artinya nilai akan dipaksa menjadi PARENT_HEIGHT dikurangi tinggi si view bila nilainya lebih besar dari itu. Setelah melalui 2 pengecekan (pada setiap axis) tersebut barulah di set ke Width & Height pada view.

Collide with screen border

Collide with screen border

Sticky to Screen Border

private var widgetDX: Float = 0F
private var widgetDY: Float = 0F

fun setupStickyDraggable(){
    iv_sticky_draggable.setOnTouchListener { v, event ->
        val viewParent:View = (v.parent as View)
        val PARENT_HEIGHT = viewParent.height
        val PARENT_WIDTH = viewParent.width

        when(event.actionMasked){
            MotionEvent.ACTION_DOWN -> {
                widgetDX = v.x - event.rawX
                widgetDY = v.y - event.rawY
            }
            MotionEvent.ACTION_MOVE -> {
                // Screen border Collision
                var newX = event.rawX + this.widgetDX
                newX = Math.max(0F, newX)
                newX = Math.min((PARENT_WIDTH - v.width).toFloat(), newX)
                v.x = newX

                var newY = event.rawY + this.widgetDY
                newY = Math.max(0F, newY)
                newY = Math.min((PARENT_HEIGHT - v.height).toFloat(), newY)
                v.y = newY
            }
            MotionEvent.ACTION_UP -> {
                // Stick to Left or Right screen
                if(event.rawX >= PARENT_WIDTH / 2)
                    v.x = (PARENT_WIDTH) - (v.width).toFloat()
                else
                    v.x = 0F

                // Stick to Top or Bottom screen
                if(event.rawY >= PARENT_HEIGHT / 2)
                    v.y = (PARENT_HEIGHT) - (v.height).toFloat()
                else
                    v.y = 0F

                // IF BOTH X & Y set to stick, the view will only stick to corner
            }
            else -> {
                return@setOnTouchListener false
            }
        }
        true
    }
}

Agar DraggableView nya tidak menghalangi user, kita bisa tambah behavior sticky, jadi setelah di-drag, view akan menempel pada tepian layar (atau parent view) baik dalam sumbu x, sumbu y atau keduanya.

Untuk implementasinya digunakan MotionEvent.ACTION_UP, sebagai contoh agar sticky pada sumbu X (line 29–32), bila koordinat titik sentuh berada di tengah ke kanan, maka view akan diset ke screen border kanan (PARENT_HEIGHT - v.height), bila berada di tengah ke kiri, maka view akan diset ke screen border kiri (0F). Begitu pula untuk sumbu Y (line 35–38).

Sticky X

Sticky X

Sticky Y

Sticky Y

Sticky XY

Sticky XY

Sticky to First Position

private var widgetDX: Float = 0F
private var widgetDY: Float = 0F
// Add these
private var widgetXOrigin : Float = 0F
private var widgetYOrigin : Float = 0F

fun setupStickyDraggable(){
    iv_sticky_draggable.setOnTouchListener { v, event ->
        val viewParent:View = (v.parent as View)
        val PARENT_HEIGHT = viewParent.height
        val PARENT_WIDTH = viewParent.width

        when(event.actionMasked){
            MotionEvent.ACTION_DOWN -> {
                widgetDX = v.x - event.rawX
                widgetDY = v.y - event.rawY
                // save widget origin coordinate
                widgetXOrigin = v.x
                widgetYOrigin = v.y
            }
            MotionEvent.ACTION_MOVE -> {
                // Screen border Collision
                var newX = event.rawX + this.widgetDX
                newX = Math.max(0F, newX)
                newX = Math.min((PARENT_WIDTH - v.width).toFloat(), newX)
                v.x = newX

                var newY = event.rawY + this.widgetDY
                newY = Math.max(0F, newY)
                newY = Math.min((PARENT_HEIGHT - v.height).toFloat(), newY)
                v.y = newY
            }
            MotionEvent.ACTION_UP -> {
                // Back to original position
                v.x = widgetXOrigin
                v.y = widgetYOrigin
            }
            else -> {
                return@setOnTouchListener false
            }
        }
        true
    }
}

Behavior lain yang dapat kita implementasikan adalah Sticky to first position. Disini kita perlu menyimpan nilai koordinat awal view (line 18–19) pada variabel mis. widgetXOrigin & widgetYOrigin (line 4–5) saat ACTION_DOWN, kemudian pada ACTION_UP kita tinggal set koordinat view pada nilai koordinat awal tersebut (line 35–36)

Animation

Lebih lanjut lagi, agar gerakkan pada behavior sticky-nya smooth, kita bisa menggunakan .animate() pada view tersebut. Jadi alih-alih meng-assign langsung dengan = , kita gunakan seperti ini:

// TO ANIMATE USE animate()
v.animate().x(0F).setDuration(250).start()
// INSTEAD OF
v.x = 0F

Berikut hasilnya:

Smooth sticky

Smooth sticky

Ready to use Library

Untuk memudahkan kalian mengimplementasi hal-hal diatas, saya buat sebuah library open-source yang mencakup hal-hal diatas, silahkan cek disini:

GitHub logo hyuwah / DraggableView

DraggableView is an Android library to make floating draggable view easily using extensions on Kotlin & provided utils class on Java

Sekian cerita kali ini mengenai implementasi Draggable View di Android.
Semoga membantu & selamat mencoba :)

Originally posted on Medium

Cover Photo by Marcel Walter on Unsplash

Discussion (0)