View 그려지는 과정

  • UI 를 그리는 기본 구성요소
  • CustomView 를 만들기 위함

 

안드로이드는 유저 인터페이스인 UI 를 통해서

사용자와 앱이 인터랙션을 하게 된다.

 

따라서 UI 를 그리는 View 와

View 가 그려지는 과정이 중요하다.

 

 

 

How Android Draws View

 

0) 전위순회 방식

전위순회 방식을 쓰기 때문에, 부모 부터 자식 뷰 순서로 그려지게 됨

 

현재 Depth 는 2

 

Depth 가 깊어질 수록

확인해야하는 내용이 타고 타고 타고 들어가면서 많아진다.

 

이러한 이유로, View 를 Flat 한 구조로 가져가라고 한다.

그래야 렌더링 속도가 빨라지기 때문


1) measure

  • 뷰의 크기를 계산
  • 모든 뷰는 각각 자신의 width, height 를 계산
  • measure 과정에서, 부모 - 자식 뷰간의 크기 정보 전달을 위해 2가지 클래스 사용
  • ViewGroup.LayoutParams
    • DP, PX.. : 자식뷰가 원하는 사이즈
    • MATCH_PARENT : 부모 뷰 사이즈와 똑같이 자식뷰 사이즈 지정
    • WRAP_CONTENT : 부모 뷰 안에서, content 를 표현할 수 있는 fit 한 사이즈 지정
  • 자식 뷰가 부모 뷰에게 자신이 어떻게 측정되고 위치를 정할지 요청 할 때 사용, (how big)
  • ViewGroup.MeasureSpecs
    • UNSPECIFIED : 부모 뷰는 자식 뷰가 원하는 사이즈로 결정
    • EXACTLY : 부모 뷰가 자식 뷰의 사이즈를 정확히 지정할 때
    • AT_MOST : 부모 뷰가 자식 뷰의 최대 사이즈를 지정할 때
  • 부모 뷰가 자식 뷰에게 요구사항을 전달할 때 사용

2) layout

  • 뷰의 크기와 위치를 할당
  • 부모기준의 상대적 위치 (left, top, right, bottom) 을 계산

3) draw

  • 뷰를 그리는 단계
    • Canvas : 뷰의 모양을 그리는 객체
    • Paint : 뷰의 색상을 칠하는 객체
  • measure, layout 에서 측정한 크기와, 계산한 위치에 뷰를 그림
  • 이 콜백은 언제든 다시 호출 될 수 있음
    • scroll 이나 swipe 를 하게 되면 뷰는 onDraw 다시 호출
    • 객체 할당과 같이 리소스가 많이 소모되는 로직은 추가하지 말 것
  • ViewUpdate : 런타임에 뷰를 다시 그리게 하는 함수
    • invalidate : view 에 변화가 생겨서 다시 그려야 할 때
      • color 변화 등
    • requestLayout : view 를 처음부터 그려야 할 때
      • 크기가 변화해서 measure 부터 다시 해야할 때

실습

1) CustomLinearLayout.kt

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.util.Log
import android.widget.LinearLayout

class CustomLinearLayout @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        Log.i("그리기", "CustomLinearLayout - onMeasure")
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        Log.i("그리기", "CustomLinearLayout - onLayout")
        super.onLayout(changed, l, t, r, b)

    }

    override fun onDraw(canvas: Canvas?) {
        Log.i("그리기", "CustomLinearLayout - onDraw")
        super.onDraw(canvas)

    }
}

2) CustomTextView.kt

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.util.Log
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView

class CustomTextView  @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    AppCompatTextView(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        Log.i("그리기", "CustomTextView - onMeasure")
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        Log.i("그리기", "CustomTextView - onLayout")
        super.onLayout(changed, l, t, r, b)

    }

    override fun onDraw(canvas: Canvas?) {
        Log.i("그리기", "CustomTextView - onDraw")
        super.onDraw(canvas)
    }

    override fun requestLayout() {
        Log.i("그리기", "CustomTextView - requestLayout")
        super.requestLayout()
    }

    override fun invalidate() {
        Log.i("그리기", "CustomTextView - invalidate")
        super.invalidate()
    }
}

3) MainActivity.kt

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<TextView>(R.id.textView).setOnClickListener { (it as TextView).setTextColor((resources.getColor(R.color.purple_200, null)))  }
        findViewById<TextView>(R.id.textView).setOnLongClickListener {
            (it as TextView).text = "test"
            true
        }
    }
}

 

4-1) 실행 결과

부모뷰인 LinearLayout 에서 Measure 이 호출된 다음,

TextView 에서 호출됨

 

onLayout 위치 정보, 크기를 지정하는 부분까지 호출된 다음

마지막으로 onDraw 가 호출된다.

onDraw 단계에서 화면에 그림을 그린다.

 

LinearLayout 에서 onDraw 가 호출되지 않는 이유는

LinearLayout 은 그림을 그리는 것이 아니라

그림을 그릴 “틀” 역할을 하기 때문이다.


4-2) 색이 변경될 때 ( invalidate )

클릭했을 때, text 색이 변경되는 기능

 

invalidate

view 에 변화가 생겨서 다시 그려야 할 때

color 변화 등


4-3) 뷰의 크기와 위치가 변경될 때

롱클릭 했을 시, hello world 글씨를 test 로 바꾸는 기능

뷰의 크기와 위치가 바뀐다.

 

 

텍스트뷰의 내용이 변경되고

안에 들어가있는 width 나 height 관련 정보가 모두 wrapcontent 이기 때문이다.

 

 

requestLayout

view 를 처음부터 그려야 할 때

크기가 변화해서 measure 부터 다시 해야할 때

 

 

invalidate

view 에 변화가 생겨서 다시 그려야 할 때

color 변화 등

 

 

measure

뷰의 크기를 계산

모든 뷰는 각각 자신의 width, height 를 계산

 

 

layout

뷰의 크기와 위치를 할당

부모기준의 상대적 위치 (left, top, right, bottom) 을 계산

 

+ Recent posts