안드로이드는 UI 를 구성하기 위해 사용되는 녀석이다. 우리가 XML 상으로 구성했던 거의 모든 UI 요소들의 조상 객체는 바로 View
인 것이다. 아래 이미지와 같이, '어 혹시 걔도?' 싶은 애들은 모두 View
의 서브 클래스들이다.
View
는 드로잉, 이벤트 처리를 담당하는 UI 구성요소의 기본 클래스이다. View
를 상속받아 구현하는 TextView
, Button
등 어떤 특수 목적을 가지고 있는 View
를 위젯, 컴포넌트라고 부르기도 한다.
눈치 챈 사람도 있겠지만, 새로운 위젯을 만들기 위해선 View
를 반드시 상속하여 구현해야 한다. 그리고 그러한 위젯들을 담는 부모 뷰, 즉 Layout 역시 View
를 상속받는 ViewGroup
을 상속받아 구현한다.
안드로이드에서 사용자에게 보여지는 화면, 즉 사용자와 인터랙션하는 컴포넌트는 Activity
라고 다들 알고 있을 것이다. 액티비티는 포커스를 받게 되면 Android 에게 View Hierarchy 의 루트 노드를 제공하여 레이아웃을 그리게 된다.
onCreate()
내에서setContentView()
를 통해 루트 노드 전달
레이아웃은 루트 노드부터 리프 노드까지 트리를 따라 순서대로 그려지게 된다. 부모가 먼저 그려지고 그 다음 자식들 순서대로 그려지는 형태이다. (Top-down 방식)
draw()
를 호출하여 화면에 지정된 형태로 자식 뷰들을 그려줄 것을 요청레이아웃을 그리는 과정은 Measure
, Layout
이렇게 크게 두 단계를 거친다.
measure()
메소드 호출을 통해 이루어짐measure()
가 반환되면, getMeasuredWidth()
및 getMeasuredHeight()
값을 자식 뷰들의 값과 함께 설정해야 함measure()
를 호출할 수도 있음이러한 측정 단계에서는 아래와 같은 두 클래스를 사용하곤 한다.
ViewGroup.LayoutParams
자식 뷰가 부모 뷰에게 '나 이렇게 측정해줭' 하고 알리는 수단이다.
- 정확한 숫자값 : DP 값 등 (
정해인 나오는 D.P 아님ㅈㅅ)- MATCH_PARENT : 부모 뷰 크기에 꽉 맞추겠다
- WRAP_CONTENT : 자신의 내용물 크기에 꽉 맞추겠다
ViewGroup.MeasureSpecs
부모 뷰가 자식 뷰의 크기 제한을 둘 때 사용한다.
- UNSPECIFIED : 자식 뷰 크기 제한 X
- EXACTLY : 자식 뷰의 정확한 사이즈 설정 (자식 뷰는 해당 사이즈 내에서 자신의 자식 뷰도 맞춰야 함)
- AT_MOST : 자식 뷰의 최대 사이즈 설정
layout()
메소드 호출을 통해 이루어짐이렇듯 뷰는 그려지기 전부터 화면에 온전히 표시되기까지 생명주기가 존재한다. 안드로이드에서 CustomView
를 직접 만들거나, 화면상 Layout 이 어떻게 그려지게 되는지에 대한 이해를 위해선 View
의 생명주기에 대해 빠삭하게 이해할 필요가 있다.
앱을 개발하다보면 기본적으로 제공되는 위젯에서 더 나아가 특색있는 기능을 갖고 있는 뷰를 만들거나, 특이한 형태의 뷰가 계속하여 재사용될 때 생산성을 위해 CustomView
를 자주 만들게 된다. 그러나 View
의 생명주기도 모른채 마구잡이로 만들었다간 어떤 대참사가 발생할지 모른다.
부모 뷰가 addView()
를 호출하게 되면, 뷰의 생애는 본격적으로 시작된다.
그럼 위 라이프사이클대로 하나씩 따라가며 각각의 메소드가 어떤 역할을 수행하게 되는지에 대해 알아보도록 하자.
constructor()
addView()
메소드를 갖게 됨onAttachedToWindow()
addView()
를 호출함으로써 View
가 윈도우에 붙을 때 호출된다 (말 그대로)View
에 접근 가능해짐surface
를 가짐onDetachedFromWindow()
호출 이후에는 surface
가 없음onDestroyed()
호출될 때, 혹은 부모 뷰에서 해당 뷰를 제거할 때 호출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()
를 호출함으로써 측정된 너비와 높이 값을 명시적으로 설정하는 모습을 확인해볼 수 있다.
onLayout()
layout()
에서 호출하는 콜백 메소드 (뷰의 크기와 위치 지정)dispatchToDraw()
ViewGroup
에 속한 메소드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)
}
invalidate()
View
를 다시 그리기 위해 호출하는 메소드requestLayout()
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/
좋아요❤