안드로이드는 뷰를 어떻게 그릴까

홍석규·2022년 3월 14일
0

안드로이드

목록 보기
1/2

뷰는 어떻게 그려질까

액티비티가 포커스를 갖게 되면 레이아웃을 그리는 과정을 거치게 된다. 안드로이드 프레임워크가 뷰의 레이아웃을 그리는 과정을 다뤄주기에 액티비티는 레이아웃 계층의 루트 노드를 제공해줘야한다.

뷰를 그리는 과정은 레이아웃의 루트 노드부터 시작 되는데 레이아웃 트리를 measure하고 draw하라는 요청을 받게된다.

  • 안드로이드의 뷰는 뷰그룹을 상속받는 layout내에 그려지게 된다. 그래서 자연스럽게 뷰들은 트리 형태의 hierarchy한 구조를 띄게 된다.

각 뷰그룹들은 자식 뷰들이 그려질 수 있게 draw를 요청해야하며 요청 받는 뷰들은 draw를 호출해서 직접 그리는 과정을 거쳐야한다. 뷰 트리는 전위 순회 방식을 사용하기 때문에 (루트 노드부터 방문하는 방식) 부모뷰가 자식뷰 보다 먼저 그려지게 되며 같은 level에 있는 뷰들은 순차적으로 그려지게 된다.

뷰가 그려지는 로직은 우선 3가지 과정을 거친다.

Measure

  • 시스템은 Top-down traversal 방식으로 뷰트리를 위에서 부터 순차적으로 탐색해서 각 뷰그룹과 뷰의 요소들의 크기를 결정한다. (디바이스별로 뷰의 사이즈가 다 달라짐) 뷰그룹의 크기가 측정되면 자식뷰들의 크기도 함께 측정된다.

Layout

  • measure 단계에서 측정한 사이즈를 이용해서 각 뷰그룹은 자식뷰의 위치를 결정하는 새로운 Top-down traversal 과정을 진행한다.

Draw

  • 실제로 뷰를 그리는 과정을 진행한다.

Measure

measure(int widthMeasureSpec, int heightMeasureSpec)

measure 메서드는 뷰의 크기를 결정 하기 위해 호출된다. measure 단계에서 top-down traversal 방식으로 뷰 트리를 순회 하기에 부모뷰에서 자식뷰의 measure를 호출하게 된다. 부모뷰는 measure 호출 시에 파라미터로 width와 height 크기에 대한 제약조건 정보를 제공해준다. 즉 부모 레이아웃에서 자식뷰에게 제공할 수 있는 여유공간의 정보를 전달 해주는것이다. 32bit의 int 타입으로 전달 해주게 되는데 상위 2bit는 Mode에 대한 지정. 하위 30bit는 크기에 대한 정보를 제공해준다.

  • 부모뷰에서 파라미터로 넘겨주는 widthMeasureSpec과 heightMeasureSpec은 총 3가지 종류가 있다.
    • UNSPECIFIED
      • 부모뷰가 자식뷰의 크기 측정을 요청할 때 아무런 제약조건을 붙이지 않는 것이다. 원하는 사이즈 그대로 측정이 가능하다.
    • EXACTLY
      • 정확한 크기를 지정 해야한다. 자식뷰의 크기와 관계 없이 부모뷰의 제약조건 크기에 걸려버리게 된다. 자식뷰가 match_parent, 또는 정확한 사이즈가 정해진 경우에 할당된다.
    • AT_MOST
      • 주어진 사이즈에서 자식 뷰가 가질 수 있는 최대 크기. 자식뷰가 wrap_content인 경우 AT_MOST가 할당된다.
  • 반대로 자식뷰에서 부모뷰에게 자신이 원하는 크기와 위치를 알려주는 값으로 ViewGroup.LayoutParams를 사용한다. LayoutParams의 경우 width와 height에 대해 뷰가 크기를 얼마나 크게 설정할지 정하는 용도로 각 dimension에 대해 다음 중 한가지를 사용할 수 있다.
    • an exact number
    • MATCH_PARENT
    • WRAP_CONTENT

부모뷰에서 자식뷰의 measure()를 호출하면 몇가지 과정을 거치고 onMeasure() 메서드를 호출한다.

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

실제로 뷰의 크기를 측정하는 메서드다. 커스텀 뷰를 사용 할 경우 뷰의 크기를 측정하기 위해서 onMeasure() 메서드를 오버라이딩 해서 사용 해야한다. 크기를 측정할 때 특별히 해줄게 없다면 super.onMeasure()를 호출해서 사용할 수도 있는데 만약 호출하지 않는다면 setMeasuredDimension() 메서드가 호출되지 않았다는 런타임 에러가 발생하게 된다.

onMeasure()메서드에서는 setMeasuredDimension()만 호출하고 있는데 super.onMeasure()를 호출하지 않는다면 setMeasuredDimension()을 명시적으로 호출 해줘야한다.

setMeasuredDimension() 메서드는 파라미터로 넘겨받은 measuredWidth, measuredHeight에 inset이 있는경우 추가해주는 로직을 진행하고 있는데 파라미터로 넘기기 위한 getDefaultSize()가 어떻게 동작 하는지 확인 해보자.

getDefaultSize(int size, int measureSpec)

첫번째 파라미터로 size와 두번째 파라미터로 measureSpec이 넘어온다. 코드 내부 구현을 살펴보면

MeasureSpec이 UNSPECIFIED인 경우 size값을 사용하고 그렇지 않은 경우 measureSpec의 size를 추출해서 사용한다.

첫번째 인자로 넘겨주는 size는 getSuggestedMinimumWidth(), getSuggestedMinimumHeight() 메서드의 반환값인데 뷰에서 사용해야하는 최소 너비값을 반환해준다.

  • measureSpec이 UNSPECIFIED인 경우 onMeasure에 넘겨주는 크기값과 관계없이 최소 크기로 할당되게 되는데 다시 재측정을 진행 해야함을 알 수 있다.
  • measureSpec이 EXACTLY가 되지 않는 경우 정확한 크기 측정을 위해 재측정을 진행 할 수도있다.

예시로 뷰가 부모 뷰가 각 자식뷰들이 원하는 크기를 알아내기 위해 unspecified dimensions을 이용해서 measure를 한 번 요청할 수 있다. 그런다음 자식 뷰들의 unconstrainted sized 총합이 너무 크거나 너무 작을 때 measure()메서드를 다시 요청하게 된다. 다시 measure를 요청하는 과정에서 부모뷰는 자식뷰의 measure()를 호출 하면서 크기 제약 조건을 추가해서 호출할 수 있게 되는 것이다.

onMeasure()단계에서 우리는 setMeasuredDimension()메서드를 호출해서 크기를 지정해 줄 수있다. 만약 부모뷰의 크기보다 자식뷰의 크기를 더 크게 지정 하더라도 뷰트리의 탐색과 재측정 과정을 거쳐서 뷰모 뷰의 크기를 벗어 날 수 없게된다.

이렇게 onMeasure()까지 호출해서 Measure 단계를 다 거치면 view의 getMeasuredWidth(), getMeasuredHeight() 메서드의 값이 셋팅이 되어 있어야 하며 크기가 정해진 뷰들의 위치를 결정하는 Layout 단계를 거치게 된다.

Layout

Layout단계는 뷰 레이아웃 매커니즘의 두번째 단계다. (첫번째는 Measuring). 각 부모 뷰들은 자식뷰의 위치를 결정하기 위해 모든 자식뷰의 layout()메서드를 호출하게 된다. 보통 이 단계는 measure 과정을 거쳐 저장된 자식뷰의 측정값을 활용해서 수행한다. 역시 Measure단계와 동일하게 topDown 방식으로 진행된다. 본 단계에서 부모 뷰들은 measure pass에서 측정 된 사이즈를 이용해서 자식뷰들의 위치를 결정 시켜야한다.

Measure 재정의가 필요한 경우 onMeasure()메서드를 오버라이딩 해줬다. Layout단계에서는 커스텀 뷰를 생성할 때 재정의가 필요한 경우 onLayout()을 사용할 수 있다.

  • protected로 선언되어 override가 가능함을 알 수 있는데 onLayout()의 경우 내부가 비어있다. (Todo Layout단계에 대해서 좀더 공부해서 정리하기)

Draw

실제로 뷰를 그리는 과정이다. measure → layout → draw 과정으로 순차적으로 진행되며 Draw의 진행 로직은 다음과 같다.

  1. background를 그린다.
  2. 필요하다면, fading 되는 경우를 고려해서 canvas 영역을 저장해둔다.
  3. view의 컨텐츠를 그린다. (onDraw 호출 과정)
  4. 자식뷰를 그릴 수 있게 호출 (dispatchDraw 호출)
  5. 필요하다면, fading된 edges영역을 그리고 저장해둔 canvas layer를 복구시킨다.
    1. canvas를 저장하고 복구시키는 예시로 canvas.clipRect()를 호출하거나 기존의 canvas가 가지고 있던 left, top, right, bottom 좌표가 아닌 새로운 값을 기반으로 그려야 할 때 기존 캔버스 정보를 canvas.save() → 변경된 로직 수행 → canvas.restore() 하는 경우가 존재한다.
  6. scrollBar와 같은 decorations영역의 view를 그린다.
  7. 필요하다면, drawDefaultFocusHighlight() 호출

view의 draw(Canvas canvas) 메서드를 확인 해보면 위와같은 순서로 진행하고 있다. 특히 2, 5번의 경우 skip이 되는 경우가 일반적이고 가끔 uncommon 한 case일 때 2번과 5번의 로직을 거쳐 뷰를 그리게 된다.

Custom View에서 뷰를 그릴 때 우리는 onDraw(Canvas canvas) 를 오버라이딩 해서 canvas 객체를 이용해서 그리고 싶은 내용을 그릴 수 있게 된다.

  • onDraw 내부에서도 canvas 객체를 이용해서 원하는 뷰를 그릴 수 있기 때문에 canvas 객체를 잘 활용해야 한다.
  • 그리고 onDraw의 경우 invalidate() 또는 postInvalidate()를 호출하거나, requestLayout()을 호출하는 등 뷰를 다시 그리거나, 뷰를 그리는 전체 로직을 다시 진행하는 경우 상당히 빈번하게 호출된다. 그래서 가급적이면 onDraw()내부에서 객체를 할당하는 로직은 피하는것이 좋다.
profile
학습한 내용을 공유하고 기록합니다.

0개의 댓글