View가 그려지는 과정

7 분 소요

1. flow 간단정리

  • view는 Tree 구조를 가지며 Activity는 반드시 Root view가 있어야 한다.
  • view가 그렬질때 tree의 부모에서 자식 순서로 그려지도록 호출된다.
  • 그리는 과정은 크게 measure(크기 측정), layout(배치), draw(그리기) 단계로 이루어진다.
  • 부모 view가 measure될때 자식 view도 measure되며, 부모의 measure과정이 끝났다는 것은 자식의 measure 과정 역시 끝났다는 것이다.
  • layout 과정에서 부모 view는 measure가 완료된 자식 view의 크기를 이용해서 배치한다.
  • 크기를 조절하는 과정에서 measure()layout()은 여러번 호출될 수 있다.
  • 자식 view가 부모 view에게 자신의 크기를 전달할때 LayoutParams 가 사용된다.
  • 부모 view가 자식 view에게 요구사항을 전달할때 MeasureSpec 이 사용된다.
  • measure와 layout 단계가 끝났으면 draw 과정을 통해 실제 View 의 모습을 화면에 그린다.

2. View drawing 상세

2.1. View drawing을 위한 기본

2.1.1. Activity는 Root Node(View)를 제공해야 한다.

Activity가 focus를 받으면 레이아웃을 그리도록 요청된다.
안드로이드 프레임워크가 그리는 과정을 처리하는데 이때 Activity는 반드시 레이아웃 Hierarchy의 Root Node(View)를 제공해야 한다.

setContentView() 가 이 과정이라 할 수 있다.

2.1.2. 안드로이드에서 View는 Tree 구조로 구성된다.

Root View 아래에 다수의 자식 View가 존재할 수 있고, 각 자식 View 아래에 또 다른 자식 View들이 존재할 수 있다. 모든 View는 이렇게 Root View로부터 Tree 구조로 구성된다.

Tree 구조로 구성되므로 Root View로부터 모든 View에 대한 탐색이 가능하다.

2.1.3. Drawaing 3단계 과정 (Measure, Layout, Draw)

Drawaing은 크게 3단계로 구성되고 Drawaing cycle 상 순서는 아래와 같다.

  1. Measure - View 크기 측정 단계
  2. Layout - View 배치 단계
  3. Draw - View 그리기 단계

이 과정은 Tree 순서대로 진행된다.
즉, Root View로부터 시작하여 자식 View 순서대로 진행되는데 만약 자식이 여러개 (형제 관계)라면 순서대로 그려진다.

RelativeLayout이나 ConstraintLayout에서 자식 View를 정의할때 보면 코드 순서상 뒤에 있는 View가 앞에 있는 View를 덮을 수 있음. 형제 관계에서는 순서대로 그려지는데 코드상 뒤에 있기 때문이다.

2.1.4. Measure, Layout은 여러번 호출될 수 있다.

경우에 따라 View drawing 과정에서 크기측정(measure)과 배치과정(layout)은 여러번 호출 될 수 있다.

예를 들어 부모 View는 정확한 크기가 명시되어 있지 않은 자식 View에 대해 해당 자식 View가 얼마만큼의 크기를 원하는지 알아내기 위해 measure()를 호출할 수 있다.
이후 모든 자식의 크기 합이 너무 크거나 작으면 실제 명시적인 숫자로 measure()를 한번 더 호출할 수 있다.

즉, 자식 view가 차지하는 공간에 대해 동의하지 않아 두번째 과정에서 부모가 강제로 규칙을 설정하는 것이다.

2.2. Measure 단계

  • 크기를 측정하는 단계로 이 단계가 끝나면 View에는 측정된 값에 대한 정보가 저장되어 있어야 한다.
  • Measure 단게에서 부모 View와 자식 View간에 치수 정보를 주고받기 위해 2개의 Class(LayoutParams, MeasureSpec)가 사용된다.

2.2.1. onMeasure(Int, Int)

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

부모 레이아웃이 자식 레이아웃을 배치하기전 자식의 크기를 알아내기 위해 measure() 메서드를 호출한다. 그런데 measure()에는 강제 레이아웃, 크기 변경 빈도 최소화, 치명적인 에러 처리등의 중요한 역할을 담당하기 때문에 직접 override하지 않는다. 이 대신 measure()에서 크기 결정시 onMeasure()를 호출하기 때문에 보통 onMeasure()를 override하여 View의 크기를 결정하도록 한다.

2.2.2. LayoutParams

자식 View가 자신이 그려지길 원하는 크기나 위치를 부모 View에게 전달할때 ViewGroup.LayoutParams를 사용한다.

이 클래스의 기본 생성자를 통해서 얼마만큼의 width와 height를 가지길 원하는지에 대해서 전달할 수 있는데 아래 3가지가 가능하다.

  1. 명시적인 크기
  2. MATCH_PARENT : 부모 크기만큼 원함.
  3. WRAP_CONTENT : 자신의 content 공간이 끝나는 크기만큼을 원함.

ViewGroup에 따라서 각각 다른 ViewGroup.LayoutParams의 서브 클래스가 존재한다. 예를 들어 RelativeLayout은 RelativeLayout.LayoutParams 클래스가 있다.

(참고) LayoutParams 역할 생각해보기

LayoutParams 는 ViewGroup에 따라 각각 다른 subClass가 있는데, 이는 LayoutParams의 역할을 생각해보면 당연하다.

LayoutParams의 역할이 자식 View의 요구사항을 부모 View에게 전달하는 역할이라고 했다.
이 요구사항을 받아들이는건 부모 View이므로 당연히 부모 View가 들어줄 수 있는 요구사항이어야 한다.

부모 View란 ViewGroup을 말하는 것이고, 각 ViewGroup은 자신의 특성에 따라 들어줄 수 있는 요구사항이 달라지므로 각 ViewGroup 마다 LayoutParams가 따로 존재한다.

참고로 ViewGroup에서는 자식 View가 설정한 LayoutParams를 아래와 같이 읽어올 수 있다. 요구사항을 전달한다는 뜻은 이렇게 ViewGroup이 View의 설정을 읽을 수 있기 때문이다.

for(int i=0; i < getChildCount(); i++) {
	// Tree 순서에 따라 자신 하위에 있는 자식 View를 찾을 수 있음.
	View childView = getChildAt(i);
	
	// 자식 View의 getLayoutParams() 호출을 통해 자식 View가 설정한 요구사항을 볼 수 있음.
	LayoutParams params = childView.getLayoutParams();
}

2.2.3. MeasureSpec

부모 View는 자식 View에게 자식이 그려질 수 있는 여유 공간의 폭과 높이에 대한 정보를 제공하는데 이 때 사용하는 것이 MeasureSpec 이다.

즉, 부모가 자식에게 알려주면서 이 안에 그리라는 요구사항을 전달하는 것이다.

이 값은 두개의 값이 묶여있는데 하나는 Mode이며 다른 하나는 크기값이다. 값을 읽거나 다시 합칠때 View.MeasureSpec의 다음 메서드를 사용한다.

  1. int getMode(int measureSpec)
  2. int getSize(int measureSpec)
  3. makeMeasureSpec(int size, int mode)
(참고) MeasureSpec Mode 3가지
Mode 설명
UNSPECIFIED 부모 view가 자식 view를 제약하지 않는다. 자식 view가 희망하는 size로 그려질 수 있다.
EXACTLY 자식 view가 어느정도의 크기를 원하는지에 상관없이 부모 view가 자식 view의 size를 지정한다. 자식 view는 이 사이즈를 사용해야 하고 자식의 자식들 모두 이 범위 안에 있어야 한다.
AT_MOST 자식 view가 가질 수 있는 최대 size를 부과하는데 사용된다. 자식 view는 이 사이즈 이하로 크기를 결정해야 한다.

예를 들어 width는 AT_MOST 200이고 height는 EXACTLY 100으로 전달되었다면 View는 width에 대해 최대 200 픽셀 이하로 그려져야하고, height는 가급적 100 픽셀로 그려져야 한다.

2.2.4. onMeasure()의 결과

measure 단계가 끝나면 getMeasuredWidth(), getMeasuredHeight() 의 호출 결과로 측정된 값을 리턴할 수 있어야 한다.

즉, View에서 onMeasure() 의 결과값을 세팅하여 부모가 자식의 getMeasuredWidth(), getMeasuredHeight() 을 호출할때 값을 리턴해 줄 수 있어야 한다는 것이다.

이 때문에 onMeasure() 의 마지막에는 반드시 void setMeasureDimension(int measuredWidth, int measuredHeight) 메서드를 호출해줘야 한다.

만약 호출하지 않으면 runtime 중에 IllegalStateException이 발생한다. 측정 이후 배치를 하는 단계에서 부모가 자식에게 크기를 물었는데 자식이 이를 알려주지 않기 때문이다.

이 값은 당연히 View의 부모에 의해 설정된 제약사항(measureSpec)을 따라야 한다.

2.3. Layout 단계

두번째 과정은 크기가 측정된 View를 배치하는 단계이다.
이 과정은 보통 View가 ViewGroup 일 때 많은 역할을 수행한다. 즉, 부모 View는 크기 측정단계(Measure)에서 계산된 자식 View의 사이즈를 이용하여 자식 View를 배치하게 된다.

2.4. Draw 단계

세번째 단계는 크기가 측정되고 위치가 확정된 View를 그리는 단계이다.

3. View의 draw cycle 살펴보기

view life cycle

카테고리 메서드 설명
Creation 생성자 생성자로 code에 의해서 호출되거나 layout file에 의해 view가 inflate 될때 호출된다.
  onFinishInflate() View 및 자기 자식 View가 XML로 부터 inflate 완료되었을때 호출된다.
Layout onMeasure(int, int) View 및 자기 자식 View의 사이즈 결정을 위해 호출된다.
  onLayout(boolean, int, int, int, int) View가 자기 자식들에게 크기와 위치를 할당할때 호출된다.
  onSizeChanged(int, int, int, int) view의 크기가 변경되었을때 호출된다.
Drawing onDraw(Canvas) view가 자기 content를 렌더링 할때 호출된다.
Event processing onKeyDown(int, KeyEvent) 하드웨어 키 down이 발생했을때 호출된다.
  onKeyUp(int, KeyEvent) 하드웨어 키 up이 발생했을 때 호출된다.
  onTrackballEvent(MotionEvent) trackball 모션 이벤트가 발생했을 때 호출된다.
  onTouchEvent(MotionEvent) screen 모션 이벤트가 발생했을때 호출된다.
Focus onFocusChanged(boolean, int, Rect) View가 focus를 획득하거나 잃었을때 호출된다.
  onWindowFocusChanged(boolean) View를 가지고 있는 Window가 focus를 획득하거나 잃었을때 호출된다.
Attaching onAttachedToWindow() view가 window에 attach에 되었을때 호출된다.
  onDetachedFromWindow() view가 window에 detached 되었을때 호출된다.
  onWindowVisibilityChanged(int) view를 가지고 있는 window의 visibility가 변경되었을때 호출된다.

4. View의 생성자

View의 생성자로 총 4개가 있는데 각각이 어떤걸 의미하는지, 왜 4개로 나누져 있는지, 각 파라미터가 어떤것인지, 어떤 생성자를 구현해야 하는지 알아본다.

4.1 View의 생성자

View(Context)

View(Context, AttributeSet)

View(Context, AttributeSet, defStyleAttr)

View(Context, AttributeSet, defStyleAttr, defStyleRes) 

마지막 생성자는 API 21에서 추가되었다. 만약 하위버전에서 defStyleRes를 사용하고자 한다면 obtainStyledAttributes() 를 통해 얻을 수 있다.

생성자는 cacade하게 호출되므로 하나를 부르면 결국 super를 통해 나머지 생성자가 호출된다. 즉, 일반적으로는 위 4개중 2개의 생성자 (View(Context), View(Context, AttributeSet)) 만 재정의해서 사용하면 된다. 첫번째는 code에서 직접 View를 생성할때이고 두번째는 XML에서 inflate 될때이다.

4.2. 생성자 Parameter

Parameter 설명
Context -
AttributeSet XML 속성이다.(XML에서 inflating 될 때)
int defStyleAttr view에 설정된 default style 이다.(theme에서 설정되어 있음)
int defStyleResource view에 설정된 default style 이다.(defStyleAttr이 사용중이지 않을때)

4.2.1. Attributes

아래와 같이 layout_width, layout_height, src 등이 XML Attribute 이다.

<ImageView  
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@drawable/icon" />

이러한 속성을 사용할 수 있도록 <declare-styleable> 에 정의되어 있어야 한다. 예를 들어 아래와 같다.

<declare-styleable name="ImageView">  
  <!-- Sets a drawable as the content of this ImageView. -->
  <attr name="src" format="reference|color" />

</declare-styleable>  

<declare-styleable>는 하나의 R.styleable.[name]와 함께 개별속성에 따라 R.styleable.[name]_[attribute] 를 생성한다.
예를 들어 위 예제에서는 R.styleable.ImageViewR.styleable.ImageView_src 가 생성된다.

R.styleable.[name]는 모든 attribute 리소스의 배열로 시스템이 attribute를 찾는데 사용된다. 각 R.styleable.[name]_[attribute]는 배열 속에 있는 각 아이템이다. 그렇기 때문에 모든 attribute를 한번에 검색한 다음 각 상세 값을 개별 조회할 수 있다.

4.2.2. AttributeSet

위에서 사용한 Attribute는 View의 AttributeSet을 통해 제공된다.

AttributeSet에서 속성값을 읽을 수 있긴하지만 보통 바로 사용하지 않고 Resources.Theme.obtainStyledAttributes()에 파라미터로 넘겨준뒤 전달받은 결과값인 TypedArray를 사용한다.

init {
    context.theme.obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0
    ).apply {
	    try {
	        mShowText = getBoolean(R.styleable.PieChart_showText, false)
	        textPos = getInteger(R.styleable.PieChart_labelPosition, 0)
	    } finally {
	    	// TypedArray는 반드시 recycle 해줘야 함.
	    	recycle()
	    }
    }
}

TypedArray obejct는 공유되는 resoure이므로 반드시 recycle 해줘야 한다.

AttributesSet을 바로 사용하지 않는 것은 아래 두가지 문제가 있기 때문이다.

  1. 속성값이 resource 참조로 되어있는 경우 해당 resourece의 값을 가져올 수 없다.
  2. Theme과 Style이 적용되어 있지 않다.
(ex1) string resource

참조값으로 @string/my_label 가 정의되있을때 my_label 에 정의된 string 값으로 변환해준다. 만약 AttributeSet을 직접 사용하게 된다면 AttributeSet.getAttributeResourceValue(int, int) 을 이용하여 리소스 참조값을 직접 찾아야 한다.

(ex2) style

XML에서 style=@style/MyStyle 와 같이 스타일 적용을 한 경우 Theme.obtainStyledAttributes() 메서드는 MyStyle을 찾아 적용을 한다.

4.2.3. Default Style Attribute

이전 예에서 obtainStyledAttributes() 를 사용할때 마지막 2개의 parameter로 0을 넘겼다. 실제로 이 두개는 defStyleAttr, defStyleRes 이다.

defStyleAttr의 경우 쉽게 말하면 이 View에 기본 설정되는 default style을 말한다.
view를 쓸때마다 매번 이 view가 가져야 하는 기본 style을 지정해주기는 귀찮으니 default를 지정하는 것이다.
(default style을 theme에 만들어두고 사용하는 방식으로 쓴다.)

4.2.4. Defalt Style Resource

defStyleRes 는 간단하다. 단지 스타일 리소스(ex- @style/Widget.TextView)를 가리킨다.

defStyleRes 스타일 속성은 defStyleAttr이 정의되지 않은 경우에만 적용된다. (0으로 설정되거나 테마에 설정되어 있지 않음)

좀 더 찾아봐야 함.

4.2.5. 파라미터 관련 우선순위

다음 순서로 적용된다.

  1. AttributeSet에 정의된 설정 값.
  2. AttributeSet에 정의된 style resource (ex-style=@style/blah)
  3. defStyleAttr로 명시된 default style attribute.
  4. defStyleResource로 명시된 default style resource. (defStyleAttr이 없는 경우)
  5. Theme에 있는 값

즉, XML에서 직접 설정한 Attribute는 우선적으로 적용되고 설정하지 않았을 경우 이러한 속성을 검색할 수 있는 다양한 요소가 있다.


[참고 문서]

  1. View의 생성자
  2. view 라이프사이클
  3. 안드로이드 문서

댓글남기기