[Android] View 의 한 평생 살펴보기

H43RO·2021년 10월 15일
9

Android 와 친해지기

목록 보기
19/26
post-thumbnail

View

안드로이드는 UI 를 구성하기 위해 사용되는 녀석이다. 우리가 XML 상으로 구성했던 거의 모든 UI 요소들의 조상 객체는 바로 View 인 것이다. 아래 이미지와 같이, '어 혹시 걔도?' 싶은 애들은 모두 View 의 서브 클래스들이다.

View드로잉, 이벤트 처리를 담당하는 UI 구성요소의 기본 클래스이다. View 를 상속받아 구현하는 TextView, Button 등 어떤 특수 목적을 가지고 있는 View 를 위젯, 컴포넌트라고 부르기도 한다.

눈치 챈 사람도 있겠지만, 새로운 위젯을 만들기 위해선 View 를 반드시 상속하여 구현해야 한다. 그리고 그러한 위젯들을 담는 부모 뷰, 즉 Layout 역시 View 를 상속받는 ViewGroup 을 상속받아 구현한다.


View 는 이렇게 그려져요

안드로이드에서 사용자에게 보여지는 화면, 즉 사용자와 인터랙션하는 컴포넌트는 Activity 라고 다들 알고 있을 것이다. 액티비티는 포커스를 받게 되면 Android 에게 View Hierarchy 의 루트 노드를 제공하여 레이아웃을 그리게 된다.

onCreate() 내에서 setContentView() 를 통해 루트 노드 전달

레이아웃은 루트 노드부터 리프 노드까지 트리를 따라 순서대로 그려지게 된다. 부모가 먼저 그려지고 그 다음 자식들 순서대로 그려지는 형태이다. (Top-down 방식)

  • 부모 뷰 (ViewGroup, Layout) 은 자식 뷰들의 draw() 를 호출하여 화면에 지정된 형태로 자식 뷰들을 그려줄 것을 요청
  • 모든 각각의 뷰들은 지 알아서 스스로 그려질 책임이 있음 (마마보이 금지)

레이아웃을 그리는 과정은 Measure, Layout 이렇게 크게 두 단계를 거친다.

1. Measure 단계

  • measure() 메소드 호출을 통해 이루어짐
  • 모든 뷰들은 각각 자신의 크기 측정값을 저장함 (너비와 높이)
  • 뷰의 measure() 가 반환되면, getMeasuredWidth()getMeasuredHeight() 값을 자식 뷰들의 값과 함께 설정해야 함
  • 부모 뷰는 자식들에게 두 번 이상의 measure() 를 호출할 수도 있음
    (자식 뷰들의 크기 합이 너무 크거나, 너무 작을 때와 같은 상황)

이러한 측정 단계에서는 아래와 같은 두 클래스를 사용하곤 한다.

ViewGroup.LayoutParams

자식 뷰가 부모 뷰에게 '나 이렇게 측정해줭' 하고 알리는 수단이다.

  • 정확한 숫자값 : DP 값 등 (정해인 나오는 D.P 아님 ㅈㅅ)
  • MATCH_PARENT : 부모 뷰 크기에 꽉 맞추겠다
  • WRAP_CONTENT : 자신의 내용물 크기에 꽉 맞추겠다

ViewGroup.MeasureSpecs

부모 뷰가 자식 뷰의 크기 제한을 둘 때 사용한다.

  • UNSPECIFIED : 자식 뷰 크기 제한 X
  • EXACTLY : 자식 뷰의 정확한 사이즈 설정 (자식 뷰는 해당 사이즈 내에서 자신의 자식 뷰도 맞춰야 함)
  • AT_MOST : 자식 뷰의 최대 사이즈 설정

2. Layout 단계

  • layout() 메소드 호출을 통해 이루어짐
  • 부모 뷰는 Measure 단계에서 측정된 크기를 사용하여 모든 자식 뷰들의 위치를 배정함
  • 즉, Measure 때 모아놓은 크기 수치값을 기준으로 전체적인 레이아웃을 딱 그리는 과정

이렇듯 뷰는 그려지기 전부터 화면에 온전히 표시되기까지 생명주기가 존재한다. 안드로이드에서 CustomView 를 직접 만들거나, 화면상 Layout 이 어떻게 그려지게 되는지에 대한 이해를 위해선 View 의 생명주기에 대해 빠삭하게 이해할 필요가 있다.

앱을 개발하다보면 기본적으로 제공되는 위젯에서 더 나아가 특색있는 기능을 갖고 있는 뷰를 만들거나, 특이한 형태의 뷰가 계속하여 재사용될 때 생산성을 위해 CustomView 를 자주 만들게 된다. 그러나 View 의 생명주기도 모른채 마구잡이로 만들었다간 어떤 대참사가 발생할지 모른다.


View Lifecycle

부모 뷰가 addView() 를 호출하게 되면, 뷰의 생애는 본격적으로 시작된다.

그럼 위 라이프사이클대로 하나씩 따라가며 각각의 메소드가 어떤 역할을 수행하게 되는지에 대해 알아보도록 하자.

1. constructor()

  • 모든 뷰는 생성자에 의해 생명 주기가 시작됨 (AttributeSet 을 갖게 됨)
  • addView() 메소드를 갖게 됨

2. onAttachedToWindow()

  • 부모 뷰가 addView() 를 호출함으로써 View 가 윈도우에 붙을 때 호출된다 (말 그대로)
  • 고유 ID 를 통해 View 에 접근 가능해짐
  • 이 순간부터는 뷰를 그리기 위한 surface 를 가짐
    • 단, onDetachedFromWindow() 호출 이후에는 surface 가 없음
      액티비티 onDestroyed() 호출될 때, 혹은 부모 뷰에서 해당 뷰를 제거할 때 호출
  • 따라서 이 순간부터는 리소스 할당 및 리스너 설정 등이 가능해짐

3. onMeasure()

  • measure() 에서 호출하는 콜백 메소드 (View 의 크기를 측정하기 위해 호출됨)
    • 부모 뷰의 경우에는 모든 자식 뷰들의 measure() 를 호출한 뒤 자신의 크기 결정
    • setMeasuredDimenstion() 호출하여 명시적으로 너비와 높이 설정

글의 서두에서 다뤘던 내용을 다시 살펴보자.

ViewGroup.MeasureSpecs

onMeasure() 단계에서 부모 뷰가 자식 뷰의 크기 제한을 둘 때 사용한다.

  • UNSPECIFIED : 자식 뷰 크기 제한 X
  • EXACTLY : 자식 뷰의 정확한 사이즈 설정 (자식 뷰는 해당 사이즈 내에서 자신의 자식 뷰도 맞춰야 함)
  • AT_MOST : 자식 뷰의 최대 사이즈 설정

해당 MeasureSpecs 를 활용하여 자식 뷰들의 크기 제한을 명시한다.

아래는 실제 onMeasure() 코드인데, 파라미터 두 개가 각각

  • widthMeasureSpec : 부모 뷰에 의해 적용된 수평 공간 제약사항
  • heightMeasureSpec : 부모 뷰에 의해 적용된 수직 공간 제약사항

이렇게 정의된다. 코드를 잠시 살펴보자.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    // 지정한 MeasureSpec 에 따라 Mode 를 가져옴
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    // 가져온 Mode 를 체크하여 뷰의 크기를 적용함
    val width = when (widthMode) {
        MeasureSpec.EXACTLY -> widthSize
        MeasureSpec.AT_MOST -> (paddingLeft + paddingRight + suggestedMinimumWidth)
            .coerceAtMost(widthSize)
        else -> widthMeasureSpec
    }

    val height = when (heightMode) {
        MeasureSpec.EXACTLY -> heightSize
        MeasureSpec.AT_MOST -> (paddingTop + paddingBottom + suggestedMinimumHeight)
            .coerceAtMost(heightSize)
        else -> heightMeasureSpec
    }

    setMeasuredDimension(width, height)  // 명시적으로 너비와 높이 설정
}

마지막에는 어떤 값을 반환하는게 아닌, setMeasuredDimension() 를 호출함으로써 측정된 너비와 높이 값을 명시적으로 설정하는 모습을 확인해볼 수 있다.

4. onLayout()

  • layout() 에서 호출하는 콜백 메소드 (뷰의 크기와 위치 지정)
  • 즉, 뷰의 크기와 위치를 지정하여 화면에 배치한 후에 호출함 (주로 부모 뷰일 때 호출)
  • 아직 뷰가 그려지는 단계는 아님 (헷갈리지 말자!)

5. dispatchToDraw()

  • ViewGroup 에 속한 메소드
  • 뷰가 다시 그려져야 할 경우에 자식 뷰들도 싹 다 다시 그려지도록 함

6. onDraw()

  • 실제로 뷰를 그리는 단계
    • Canvas : 뷰의 모양을 그리는 객체
    • Paint : 뷰의 색상을 칠하는 객체
  • 크기와 위치는 이전에 계산되기 때문에 그것들을 기준으로 뷰를 그리게
  • 해당 콜백 메소드는 언제든 다시 호출될 수 있기 때문에, 이 안에서 객체 생성은 하면 안 됨
    • 스크롤, 스와이프 등 인터랙션이 발생하면 언제든 호출될 수 있음
override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

    val width = measuredWidth + 0.0f
    val height = measuredHeight + 0.0f

    val circle = Paint()
    circle.color = this.lineColor
    circle.strokeWidth = 10f
    circle.isAntiAlias = false
    circle.style = Paint.Style.STROKE

    canvas?.drawArc(
        RectF(
            10f, 10f, width - 10f, height - 10f
        ), -90f,
        (this.curValue + 0.0f) / (this.maxValue + 0.0f) * 360, false, circle
    )

    val textp = Paint()
    textp.color = Color.BLACK
    textp.textSize = 30f
    textp.textAlign = Paint.Align.CENTER

    if (System.currentTimeMillis() / 1000 % 2 == 0L) {
        canvas?.drawText(
            "${this.curValue} / ${this.maxValue}",
            (width / 2),
            (height / 2),
            textp
        )
    }

    Observable.interval(1, TimeUnit.SECONDS)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
            invalidate()
        }, {

        })
        .addTo(disposable)
}

6-1. invalidate()

  • 글자나 색상 등 크기 변화는 없이 단순히 뷰의 속성 등이 변경되어 다시 그려야하는 경우 View 를 다시 그리기 위해 호출하는 메소드

6-2. requestLayout()

  • 위에서 크기 변화 없이라고 했는데, 만약 뷰의 크기 변화가 발생할 경우 레이아웃의 배치도 달라질 수 있기 때문에 해당 메소드를 호출함으로써 뷰들의 크기 측정부터 다시하게 됨

참고자료

https://www.charlezz.com/?p=29013

profile
어려울수록 기본에 미치고 열광하라

2개의 댓글

comment-user-thumbnail
2022년 5월 26일

좋아요❤

답글 달기
comment-user-thumbnail
약 12시간 전

Understanding the lifecycle of a View in Android is crucial for building efficient and responsive user interfaces. This overview covers the key stages—from creation and layout to rendering and destruction—helping you manage resources and user interactions effectively. https://thatsnotmyneighbor.pro/

답글 달기