[Android] View 의 한 평생 살펴보기
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
Author And Source
이 문제에 관하여([Android] View 의 한 평생 살펴보기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@haero_kim/Android-View-의-한-평생-살펴보기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)