[Bahasa] Implementasi Draggable View di Android
Tags: android, bahasa
Published: 2020-05-25 12:24:56

Alkisah ketika sedang mendevelop prototipe UI mesin vending powerbank [ReCharge](https://recharge.id/) 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](https://miro.medium.com/max/233/1*519vvfzJ2JTpWrSzzCV36g.png)
<figcaption>Mockup prototipe UI layar penuh</figcaption>

![Facebook Chat Heads](https://miro.medium.com/max/472/0*XiSPZBnjQtsyTGZp.jpg)
<figcaption>Facebook Chat Heads. Jaman-jaman Holo theme itu udah “kekinian”.</figcaption>

![Lucky Egg Tokopedia ](https://miro.medium.com/max/236/0*zQ_wexzQ_A8vJ7tq)
<figcaption>Lucky Egg Tokopedia</figcaption>

## Bare Minimum

```kotlin
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.](https://miro.medium.com/max/264/1*UjQKDZKNDKVIkTIzkCL78g.gif)
<figcaption>It’s moving! Tapi tembus-tembus.</figcaption>

## Screen Border Collision

```kotlin
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](https://miro.medium.com/max/264/1*SUlSlwVH7ADO1xmIIPWNgQ.gif)
<figcaption>Collide with screen border</figcaption>

## Sticky to Screen Border

```kotlin
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 ](https://miro.medium.com/max/264/1*WISEprzRpr4rajmycPm7Qw.gif)
<figcaption>Sticky X</figcaption>

![Sticky Y](https://miro.medium.com/max/264/1*Ib-rvxj83cwT6c4vnw1z-g.gif)
<figcaption>Sticky Y</figcaption>

![Sticky XY](https://miro.medium.com/max/264/1*BYker4gDuvpAaFTLRqiWyQ.gif)
<figcaption>Sticky XY</figcaption>

## Sticky to First Position

```kotlin
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:

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

Berikut hasilnya:

![Smooth sticky](https://miro.medium.com/max/268/1*dMzIJlT12hmSTkVkzNnxEQ.gif)
<figcaption>Smooth sticky</figcaption>

## 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 hyuwah/DraggableView no-readme %}

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

Originally [posted on Medium](https://medium.com/@hyuwah/implementasi-draggable-view-di-android-eb84e50fbba9)   
Cover Photo by [Marcel Walter](https://unsplash.com/@marcelwalter) on Unsplash