안드로이드는 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()
좋아요❤