본문 바로가기
android

How to draw Rect Overlay View?

by Gil 2020. 1. 5.
728x90

 

카메라 캡쳐 및 QR 코드 인식 영역에서 가운데가 뚫린 형태의 뷰가 많이 사용되고 있습니다.

위와 같이 가운데가 뚫린 Overlay 형태의 뷰를 그리는 방법에 대해서 알아보겠습니다. 

원래는 xml 단계에서 처리할 수 있을거라 예상했지만, Round Corner 형태의 사각형을 사용하기 때문에 onDraw 단계에서 직접 그려주는 방법으로 방향을 잡았습니다. 

 

Android View Lifecycle

 

안드로이드 뷰를 그리는 LifeCycle 함수 중, Overlay 커스텀 뷰 제작에서 사용할 것들만 간단히 살펴보겠습니다. 

1) onMeasure() 에서 사이즈를 측정하고, 2) layout() 뷰의 위치와 크기를 할당합니다. 그리고 3) dispatchDraw() 에서 그립니다. 


<참고>

레이아웃인 뷰는 onDraw() 를 호출하지 않고, onDispatchDraw() 만 호출하여 배경을 그립니다.
onDraw() 를 사용하려면 setWillNotDrawEnabled(false); 함수를 호출하면 됩니다. 

커스텀 뷰는 LinearLayout을 상속받아 작업하였기에 onDispatchDraw() 함수를 사용하면 됩니다. 


커스텀뷰 제작은 기본적인 Canvas에 drawBitmap() 함수를 이용해서 사각형, 원형 등을 만드는 방법을 이용합니다. 
Round Corner를 활용하려면 drawRoundRect() 함수를 활용합니다. 

PorterDuffXfermode 를 알아야 한다!

<PorterDuffXfermode를 활용하여 RoundRect를 그리는 코드>

paint.color = Color.TRANSPARENT
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT) // 요 부분이 punch 해주는 효과

val borderPaint =
Paint(Paint.ANTI_ALIAS_FLAG)
borderPaint.color = Color.WHITE

if (mode == MODE.DEFAULT) {
	osCanvas.drawRoundRect(
		RectF(
          left!!.minus(borderWidth),
          top!!.minus(borderWidth),
          right!!.plus(borderWidth),
          bottom!!.plus(borderWidth)
          ), 30f, 30f, borderPaint
     )
}
osCanvas.drawRoundRect(RectF(left!!, top!!, right!!, bottom!!), 25f, 25f, paint)

 

Thomas Porter 와 Tom Duff 라는 사람이 만든 'Composing Digital Images' 라는 홈페이지 작업물에서 이름을 따왔다고 합니다. 
디지털 이미지를 어떻게 조합할 것인가에 대한 답을 주는 함수입니다. 

source image와 Destination image 를 조합해서 가운데 사각형은 비어있고, 그 외부 영역은 dim 처리가 되어있게 만드려면 어떻게 해야할까? 

PorterDuffXfermode MODE 종류

 

PorterDuffXfermode 의 MODE의 종류중에 하나를 선택하여 뷰를 그려보겠습니다.

해당 모드중에 어떤걸 선택해야 원하는 결과가 나올 수 있을까 고민을 해봤습니다. 

  Destination  Source 최종 목적
 

위의 Mode 중에서 Destination 영역만 남기고 Source 영역은 뻥 뚤리게 하려면 'Source Out' 모드를 선택하면 됩니다. 

위의 코드로 다시 돌아가서 살펴보면  PorterDuffXfermode(PorterDuff.Mode.SRC_OUT) 부분을 확인할 수 있습니다. 

(참고: https://developer.android.com/reference/android/graphics/PorterDuff.Mode.html)


<커스텀 뷰 전체 소스> 

class RectOverlayView : LinearLayout {
    private var bitmap: Bitmap? = null
    private var left: Float? = 0f
    private var top: Float? = 0f
    private var right: Float? = 0f
    private var bottom: Float? = 0f

    private val borderWidth = 2.toPx().toFloat()

    private var mode: MODE = MODE.DEFAULT

    constructor(
        context: Context?,
        left: Float?,
        top: Float?,
        right: Float?,
        bottom: Float?
    ) : super(context) {
        this.left = left
        this.top = top
        this.right = right
        this.bottom = bottom

        z = -1f // 레이아웃 트리 관계에서 최하단에 위치하도록

        visibility = View.INVISIBLE
    }

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
    }

    override fun onLayout(
        changed: Boolean,
        l: Int,
        t: Int,
        r: Int,
        b: Int
    ) {
        super.onLayout(changed, l, t, r, b)
        bitmap = null
    }

    override fun dispatchDraw(canvas: Canvas) {
        super.dispatchDraw(canvas)
        if (bitmap == null) {
            createWindowFrame()
        }
        canvas.drawBitmap(bitmap!!, 0f, 0f, null)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }

    protected fun createWindowFrame() {
        bitmap = Bitmap.createBitmap(
            width,
            height,
            Bitmap.Config.ARGB_8888
        )

        val osCanvas = Canvas(bitmap!!)
        val outerRectangle =
            RectF(0f, 0f, width.toFloat(), height.toFloat())
        val paint =
            Paint(Paint.ANTI_ALIAS_FLAG)

        when (mode) {
            MODE.DEFAULT -> {
                paint.color = Color.BLACK
                paint.alpha = 99
            }
            MODE.DIM -> {
                paint.setMaskFilter(BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL))
                paint.color = Color.BLACK
                paint.alpha = 180
            }
        }

        osCanvas.drawRect(outerRectangle, paint)


        paint.color = Color.TRANSPARENT
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT) // 요 부분이 punch 해주는 효과

        val borderPaint =
            Paint(Paint.ANTI_ALIAS_FLAG)
        borderPaint.color = Color.WHITE

        if (mode == MODE.DEFAULT) {
            osCanvas.drawRoundRect(
                RectF(
                    left!!.minus(borderWidth),
                    top!!.minus(borderWidth),
                    right!!.plus(borderWidth),
                    bottom!!.plus(borderWidth)
                ), 30f, 30f, borderPaint
            )
        }
        osCanvas.drawRoundRect(RectF(left!!, top!!, right!!, bottom!!), 25f, 25f, paint)
    }


    fun changeMode(mode: MODE) {
        this.mode = mode
        invalidate()
    }


    companion object {
        /**
         * 화면 모드로 1) 기본모드 2) 배경이 안보이는 DIM 모드
         */
        enum class MODE {
            DEFAULT,
            DIM
        }
    }
}

'android' 카테고리의 다른 글

RxJava 의 핵심 기술 - concat, merge, zip  (0) 2020.02.27
RxJava 란  (0) 2020.02.04
ViewPager를 가진 Dialog 구현 주의사항  (0) 2020.01.02
Architecture 설계  (0) 2019.12.30
Android UI Thread와 Custom Thread에서의 UI 처리  (0) 2019.08.05