안드로이드 앱 개발을 하면서 layout이나 button 같은 UI 관련 클래스들은 모두 View를 상속 받아서 생성된다. 이 중에 ViewGroup은 button이나 ImageView 같은 것과는 다르게 자식 뷰를 가질 수 있으며 이를 배치하는 역할을 한다.

자신만의 button이나 view를 만들고 싶다면 View 클래스를 상속받아서 필요한 메소드를 오버라이드 하면된다.

자신만의 layout을 만들고 싶다면 ViewGroup 클래스를 상속받아서 필요한 메소드들을 오버라이드 하면된다.

  • Custom View는 View 클래스를 상속하여 만든다.
  • Custom Layout은 ViewGroup 클래스를 상속하여 만든다.

View가 그려지는 과정

뷰는 포커스를 얻으면 레이아웃을 그리도록 요청한다. 이때 레이아웃의 계층 구조 중 루트 뷰를 제공해야 한다. 따라서 그리기는 루트노드에서 시작되어 트리를 따라 전위 순회 방식으로 그려진다. [전위 순회 방식 : 루트->왼쪽->오른쪽] 부모 뷰는 자식 뷰가 그려지기 전에(즉, 자식 뷰 뒤에) 그려지며 형제 뷰는 전위 방식에 따라 순서대로 그려진다. 레이아웃을 그리는 과정은 측정(measure) 단계와 레이아웃(layout) 단계를 통해 그려지게 된다.

measure(int widthMeasureSpec, int heightMeasureSpec)

부모 노드에서 자식 노드를 경유하며 실행되며, 뷰의 크기를 알아내기 위해 호출된다. 이것은 뷰의 크기를 측정하는 것은 아니며 실제 크기 측정은 onMeasure(int, int)를 통해 이루어진다. measure(int, int)의 내부에서 onMeasure(int, int)를 호출함으로써 뷰의 크기를 알아낸다. 측정 과정에서는 부모 뷰와 자식 뷰간의 크기 정보를 전달하기 위해 2가지의 클래스를 사용한다.

  1. ViewGroup.LayoutParams

자식 뷰가 부모 뷰에게 자신이 어떻게 측정되고 위치를 정할지 요청하는데 사용된다. ViewGroup의 sub class에 따라 다른 ViewGroup.LayoutParams의 sub class가 존재할 수 있다. 예를 들어 ViewGroup의 sub class인 RelativeLayout의 경우 자신만의 ViewGroup.LayoutParams의 sub class는 자식 뷰를 수평적으로 또는 수직적으로 가운데 정렬을 할 수 있는 능력이 있다.

  • 숫자
  • MATCH_PARENT
  • WRAP_CONTENT
  1. ViewGroup.MeasureSpec

부모 뷰가 자식 뷰에게 요구 사항을 전달하는데 사용된다.

  • UNSPECIFIED : 부모 뷰는 자식 뷰가 원하는 치수대로 결정한다.
  • EXACTLY : 부모 뷰가 자식 뷰에게 정확한 크기를 강요한다.
  • AT MOST : 부모 뷰가 자식 뷰에게 최대 크기를 강요한다.

layout(int l, int t, int r, int b)

부모 노드에서 자식 노드를 경유하며 실행되며 뷰와 자식 뷰들의 크기와 위치를 할당할 때 사용된다. measure(int, int)에 의해 각 뷰에 저장된 크기를 사용하여 위치를 지정한다. 내부적으로 onLayout()을 호출하고 onLayout()에서 실제 뷰의 위치를 할당하는 구조로 되어있다.

measure()와 layout() 함수는 내부적으로 각각 onMeasure()와 onLayout() 함수를 호출한다. 이것은 final로 선언된 measure()와 layout() 대신 onMeasure()와 onLayout()을 구현(override)할 것을 장려하기 위해서이다.

뷰의 measure()함수가 반환할때, 뷰의 getMeasureWidth()와 getMeasureHeight()값이 설정된다. 만약 자식 뷰 측정값의 합이 너무 크거나 작을 경우 다시 measure() 함수를 호출하여 크기를 재측정한다.

View Lifecycle

  1. Constructor
    모든 뷰는 생성자에서부터 출발한다. 생성자에서 초기화를 진행하고, default 값을 설정한다. 뷰는 초기설정을 쉽게 세팅하기 위해서 AttributeSet이라는 인터페이스를 지원한다. 먼저, attrs.xml 파일을 만들고 이것을 부름으로써 뷰의 설정값을 쉽게 설정할 수 있다.
  1. onAttachedToWindow
    부모 뷰가 addView(childView) 함수를 호출하고 나서 자식 뷰는 윈도우에 붙게 된다.(attached) 이때부터 뷰의 id를 통해 접근할 수 있다.
  1. onMeasure
    뷰의 크기를 측정하는 단계이다. 매우 중요한 단계이며, 대부분의 경우 레이아웃에 맞게 특정크기를 가져야 한다. 여기에는 두 단계의 과정이 있다.

(1) 뷰가 원하는 사이즈를 계산한다.
(2) MeasureSpec에 따라 크기와 mode를 가져온다.

1
2
3
4
5
6
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}
  1. MeasureSpec의 mode를 체크하여 뷰의 크기를 적용한다.
1
2
3
4
5
6
7
8
int width;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
  1. onLayout
    이 단계에서 뷰의 크기와 위치를 할당한다.
  1. onDraw
    뷰를 실제로 그리는 단계이다. CanvasPaint객체를 사용하면 필요한 것을 그리게 된다. Canvas 객체는 onDraw 함수의 파라미터로 제공된다. Canvas 객체를 이용하여 뷰의 모양을 그리고 Paint 객체를 이용하여 뷰의 색을 그린다.

여기서 주의할 점은 onDraw() 함수를 호출시 많은 시간이 소요된다는 점이다. Scroll 또는 Swipe 등을 할 경우 뷰는 다시 onDraw()와 onLayout() 함수를 다시 호출하게 된다. 따라서 함수 내에서 객체 할당을 피하고 한 번 할당된 객체를 재사용할 것을 권장한다.

  1. View Update
    View Lifecycle을 보면 뷰를 다시 그리도록 유도하는 invalidate()requestLayout() 함수를 볼 수 있다. 이것은 런타임에 뷰를 다시 그릴 수 있게 한다. 각각의 사용 용도는 아래와 같다.
  • invalidate() : 단순히 뷰를 다시 그릴 때 사용한다. 예를 들어 뷰의 text 또는 color가 변경되거나 touch interactivity가 발생할 때 onDraw() 함수를 재호출하면서 뷰를 업데이트한다.
  • requestLayout() : onMeasure() 부터 다시 뷰를 그린다. 뷰의 사이즈가 변경될 때 그것을 다시 측정해야 하기 때문에 Lifecycle을 onMeasure() 부터 순회하면서 뷰를 그린다.
  1. Animation
    뷰의 animation은 frame 단위의 프로세스이다. 예를 들어 뷰가 점점 커질 때 뷰를 한 단계씩 차례대로 커지도록 할 것이다. 그리고 각 단계마다 invalidate()를 호출하여 뷰를 그릴 것이다. 대표적으로 애니메이션에 사용하는 클래스는 ValueAnimator이다.