Drag and Drop 시 Custom ShadowBuilder 사용하기

|

안드로이드에서 드래그&드랍(Drag & Drop)시 보여주는 반투명 이미지는 View.ShadowBuilder 클래스를 통해서 만들 수 있습니다. 자동으로 해당 View에서 반투명 이미지를 생성해주는데, 만약 개발자가 원하는 특정 이미지가 있다면 다음 코드를 이용해서 해당 이미지를 드래그시 사용할 수 있습니다.

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.view.View

class ImageShadowBuilder : View.DragShadowBuilder() {

    private var shadow: Drawable? = null

    companion object {

        fun fromResource(ctx: Context, resId: Int): View.DragShadowBuilder {
            val builder = ImageShadowBuilder()
            builder.shadow = ctx.resources.getDrawable(resId)

            builder.shadow?.let {
                it.setBounds(0, 0, it.minimumWidth, it.minimumHeight)
            }

            return builder
        }

        fun fromBitmap(ctx: Context, bm: Bitmap): View.DragShadowBuilder {

            val builder = ImageShadowBuilder()
            builder.shadow = BitmapDrawable(ctx.resources, bm)

            builder.shadow?.let {
                it.setBounds(0, 0, it.minimumWidth, it.minimumHeight)
            }

            return builder
        }

    }

    override fun onDrawShadow(canvas: Canvas?) {
        shadow?.draw(canvas)
    }

    override fun onProvideShadowMetrics(outShadowSize: Point?, outShadowTouchPoint: Point?) {
        outShadowSize?.x = shadow?.minimumWidth
        outShadowSize?.y = shadow?.minimumHeight

        outShadowTouchPoint?.x = (outShadowSize?.x ?: 0 / 2)
        outShadowTouchPoint?.y = (outShadowSize?.y ?: 0 / 2)
    }
}

Drag and Drop 구현

|

안드로이드에서 드래그&드랍(Drag & Drop) 기능은 API로 제공을 해주기 때문에 아래와 같은 요소만 구현해주면 간편하게 구현할 수 있습니다.

  • Drag를 위해 클립보트(Clipboard)에 저장할 내용
  • Drag 동안 화면에 반투명하게 보여줄 이미지
  • Drag 이벤트를 처리하는 부분


Drag 시작

private fun onLongClick(View view) {
        val data = ClipData.newPlainText("message", "hello")
        val builder = View.DragShadowBuilder(view); 

        view.startDragAndDrop(data, builder, view, 0)
}


Drag 이벤트 처리

setOnDragListener 인터페이스를 구현해주면 됩니다. 여기서 주의할 점은 return true 부분입니다. true로 리턴해야만 DragEvent.ACTION_DROP 등의 이벤트를 수신할 수 있습니다. 만약 false를 리턴하게 되면, DragEvent.ACTION_DRAG_STARTED 이벤트만 수신하며 나머지 이벤트는 받지 못합니다.

override fun onDrag(v: View?, event: DragEvent?): Boolean {
        when(event?.action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                Log.i("[snowdeer] ACTION_DRAG_STARTED()")

            }

            DragEvent.ACTION_DROP -> {
                Log.i("[snowdeer] ACTION_DROP(${event.x}, ${event.y})")

                // TODO : Item을 Drop 했을 때 처리
            }
        }

        return true
    }

코틀린의 철학

|

코틀린의 주요 특성

정적 타입 언어

코틀린은 정적 언어이기 때문에 컴파일 타임에 모든 객체와 메소드의 타입을 알 수 있습니다. 코틀린은 타입 추론(Type Inference)을 지원하고 있는데, 역시 컴파일 시점에 타입을 확정하고 검증합니다.

함수형 프로그래밍

코틀린은 함수형 프로그래밍을 지원합니다. 함수를 변수에 담을 수 있고, 파라메터 등으로 전달도 가능합니다. Java의 경우도 Java8 부터 어느 정도 지원하긴 합니다.


코틀린의 철학

실용성

코틀린은 기존의 Java의 불편한 점을 개선하고 간소화하는 방향으로 만들어진 실용적인 언어입니다.

  • Null에 대한 고민이 많이 사라졌습니다.
  • 타입 추론으로 코드가 간결해졌습니다.
  • var, val로 개발 실수를 많이 줄여줍니다.

간결성

코틀린은 기존 코드의 복잡하고 긴 코드들을 간소화할 수 있습니다. 예를 들어 gettersetter를 컴파일 시점에 자동으로 생성할 수도 있습니다. Nulltype 체크를 간결하게 할 수 있고, 다양한 람다(Lambda) 함수를 간결한 문법으로 사용할 수 있습니다.

Data Binding의 경우도 기존의 View를 Binding하는 방법보다 간결합니다.

안정성

NullPointerException을 컴파일 시점에 잡아 줄 수 있기 때문에 보다 쉽게 안정적인 코드를 작성할 수 있습니다.

상호 운용성

기존에 Java에서 사용하던 코드나 라이브러리들을 코틀린에서 그대로 사용할 수 있습니다. 또한 혼용해서 사용도 가능하며, 컴파일하고 나면 Java와 같이 *.class 파일이 생성됩니다. 반대로 코틀린에서 작성한 코드를 Java에서 대부분 사용가능합니다. (100%는 아니고 특정 경우엔 약간의 코드 수정이 필요하기도 합니다.)


Java 대비 코틀린의 새로운 부분들

제어구조 식

if, when, try/catch 등의 제어 구조가 식으로 될 수 있습니다.

val a = 1
val b = 2
val c = 3

val ret = a + b + if(c == 3) c else 0

when

when 명령어는 Java의 switch의 확장 개념입니다. 객체끼리 비교도 가능하며 구문이 아니라 식으로 사용도 가능합니다. 다음 예제와 같은 형태로도 사용 가능합니다.

fun cases(obj: Any) = 
    when(obj) {
        1 -> "One"
        "hello" -> "snowdeer"
        is Long -> "obj is Long."
        else -> "Unknown"
    }

Null Safety

?를 이용해서 Null이 될 수 있는 타입과 될 수 없는 타입을 명시적으로 구분합니다.

확장 함수

어떤 클래스를 상속받지 않더라도 특정 클래스에 메소드를 추가할 수 있습니다.

fun String.getCenterChar() : Char = 
    this.get(this.lastIndex/2)

fun test() {
    println("Hello, snowdder".getCenterChar())
}

코틀린 확장 함수

|

코틀린은 확장 함수를 지원합니다.

Java의 경우 기존에 만들어진 클래스에 새로운 메소드를 추가하려면 해당 클래스를 상속하는 새로운 클래스를 만들어야 하는데, 코틀린에서는 확장 함수(Extension Function)를 이용해서 상속 없이 기존 클래스에 새로운 함수를 추가할 수 있습니다.

이 때 확장 함수를 추가할 대상 클래스는 리시버 타입(Receiver Type)이라고 합니다.

예를 들어 다음과 같은 방법으로 확장 함수를 추가할 수 있습니다.

fun main(args: Array<String>) {
    println("snowdeer".hello())
}

fun String.hello() : String {
    return "$this, hello"
}

확장 함수의 사용 방법은 클래스 내 정의된 메소드와 동일하게 점(.)을 찍고 호출할 수 있지만, 실제로는 클래스 외부에서 추가된 함수이기 때문에 함수 내에서는 privateprotected로 선언된 변수나 메소드에 접근할 수 없습니다.

Scale 및 Gesture Handler

|

화면의 TouchEvent에서 멀티 터치를 이용한 Scale이나 Gesture를 인식하는 Handler 코드 예제입니다.

package com.snowdeer.utils

import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector

interface OnScaleGestureEventListener {
    fun onSingleTab(x: Float, y: Float)
    fun onDoubleTab(x: Float, y: Float)
    fun onLongPress(x: Float, y: Float)
    fun onScaleChanged(scaleFactor: Float)
    fun onOffsetChanged(offsetX: Float, offsetY: Float)
}

class ScaleGestureHandler(ctx: Context) {

    private var scaleFactor = 1.0F
    private var offsetX = 0.0F
    private var offsetY = 0.0F
    private var scaledOffsetX = 0.0F
    private var scaledOffsetY = 0.0F
    private var focusX = 0.0F
    private var focusY = 0.0F

    private val scaleGestureDetector: ScaleGestureDetector
    private val gestureDetector: GestureDetector
    var onScaleGestureEventListener: OnScaleGestureEventListener? = null

    init {
        scaleGestureDetector = ScaleGestureDetector(ctx, ScaleListener())
        gestureDetector = GestureDetector(ctx, GestureListener())
    }

    fun handleTouchEvent(event: MotionEvent): Boolean {
        scaleGestureDetector.onTouchEvent(event)

        if (gestureDetector.onTouchEvent(event)) {
            return true
        }

        return false
    }

    fun reset() {
        scaleFactor = 1.0F
        offsetX = 0F
        offsetY = 0F
        scaledOffsetX = 0F
        scaledOffsetY = 0F
        focusX = 0.0F
        focusY = 0.0F
    }

    private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = Math.max(0.8f, Math.min(scaleFactor, 3.0f))

            focusX = detector.focusX
            focusY = detector.focusY

            onScaleGestureEventListener?.onScaleChanged(scaleFactor)

            offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
            offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY

            onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)

            return true
        }
    }

    private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
            onScaleGestureEventListener?.onSingleTab(e.x, e.y)
            return true
        }

        override fun onDoubleTap(e: MotionEvent): Boolean {
            onScaleGestureEventListener?.onDoubleTab(e.x, e.y)
            return true
        }

        override fun onLongPress(e: MotionEvent) {
            onScaleGestureEventListener?.onLongPress(e.x, e.y)
        }

        override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
            scaledOffsetX -= distanceX / scaleFactor
            scaledOffsetY -= distanceY / scaleFactor

            offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
            offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY

            onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)

            return true
        }
    }
}