참고자료(https://developer.android.com/develop/ui/views/layout/custom-views/custom-drawing)
![](https://velog.velcdn.com/images/qure/post/190c7669-404e-4d01-958b-cffacf68a21f/image.png)
- 커스텀 뷰의 핵심: 도화지를 선택(onMeasure), 어느 위치에(onLayout), 어떤 그림을 그릴지(onDraw)를 설정해준다
Construnctor
- 최대 4개의 생성자를 가질 수 있음
- View(context: Context): 동적으로 뷰를 생성할 때 사용할 수 있는 간단한 생성자로 파라미터 context를 통해 현재 실행 중인 뷰의 리소스 등에 액세스 할 수 있음
- View(context: Context, attrs: AttributeSet): xml에서 생성할 떄
- View(context: Context, attrs: AtrributeSet, defStyleAttr: Int): ThemeStyle과 함께 뷰를 생성할 때 → 이 생성자는 API 21에 나옴
- View(context: Context, attrs: AtrributeSet, defStyleAttr: Int, defStyleRes: int): ThemeStyle 또는 Style로 xml에서 뷰를 생성할 때
onMeasure
-
해당 커스텀 뷰의 사이즈를 지정해줘야 함
-
xml에서 유저가 설정한 width, height의 정보가 파라미터로 넘어옴
-
MeasureSpec.getMode(~)를 통해 MATCH_PARENT, WRAP_CONTENT 또는 100dp와 같은 지정된 값인지 알 수 있음
-
여러 번 호출 가능함
-
부모가 자식들의 각 크기를 측정한 후 자식들의 크기 합이 크거나 작다면 다시 measure메소드가 호출하여 구체적인 값을 구함
-
child view를 가지는 커스텀 뷰면 child의 사이즈를 측정해서 자신의 사이즈를 재야할 수 있음 이때 onMeasure 메소드에서 설정해주면 됨
-
파라미터로 넘어오는 widthMeasureSpec과 hegihtMeasureSpec은 뷰의 모드와 사이즈를 조합한 값
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
-
이 코드에서 모드와 사이즈를 알 수 있음
-
모드
- MeasureSpec.AT_MOST: wrap_content. 해당 값보다 더 클 수 없음, 측정 과정이 다시 발생할 가능성이 있음
- MeasureSpec.EXACTLY: match_parent, 500dp와 같이 정해져 있는 값, 측정 과정이 발생하지 않음
- MeasureSpec.UNSPECIFIED: 정해 있지 않은 값, 원하는 값을 설정할 수 있음, 측정 과정이 다시 발생할 가능성이 있음
-
onMeasure() 끝나면 setMeasureDimension()을 통해 값을 설정해줌
-
뷰는 전위순회 방식으로 그려진다. 그런데 부모 뷰가 자식 뷰의 크기에 따라 자신의 크기를 조정한다면 아직 자식 뷰를 측정하지 않았는데 어떻게 자신의 사이즈를 측정할 수 있을까?
⇒ measureChildren(widthMeasureSpec: Int, heightMeasureSpec: Int) 메소드를 사용하면 됨
해당 뷰의 모든 자식 뷰들에게 지정된 MeasureSpec 조건과 패딩을 고려하여 각각 자체적으로 크기를 측정하도록 요청함
onLayout
- 뷰의 위치를 설정해주는 함수
- 뷰의 child들의 크기와 위치를 할당해야할 떄 호출됨
- 즉 child를 가지는 뷰라면 해당 메소드를 오버라이드를 해주어야 함
- 이때 넘어오는 값들은 앱 전체를 기준으로 넘어오는 위치 값
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
children?.forEachIndexed { index, view ->
view.layout(x, y x + view.measuredWidth, y + view.measuredHeight)
}
}
- 일반적으로 View가 현재 범위 내에서 더이상 맞지 않는다고 판단하면 자체적으로 호출됨
- 이 떄 requestLayout()을 호출하여 레이아웃을 시작할 수 있음
onDraw
- 뷰에 그림을 그리는 함수
- Paint 클래스를 사용할 수 있고, canvas에 텍스트를 추가할 수 있음
- 많은 시간 소요, 여러 번 호출 될 수 있기 때문에(초당 60번) 객체 선언, 할당을 피하고 기존 객체를 재사용하는 것이 좋음
- 가비지 컬렉터가 더 빨라서 GC 관련된 drop이 없을 수도 있지만, 별도의 스레드가 진행되므로 배터리 소모를 야기 할 수 있음
- 초기화되는 객체들은 주로 drawing object이고 많은 소멸자를 호출하기 때문에 성능에 영향을 줄 수 있음
invalidate()
- 뷰의 모양이나 내용이 변경되었을 때 화면을 다시 그리기 위해 사용
- invalidate() 메서드의 기능: invalidate()는 커스텀 뷰에서 UI를 갱신하기 위해 뷰를 다시 그리도록 요청하는 메서드입니다. 예를 들어, view.setText()를 호출할 때도 내부적으로 invalidate()가 호출됩니다. 이 메서드는 뷰가 속한 상위 ViewGroup에게 해당 뷰의 영역을 다시 그려야 한다고 요청합니다.
- invalidate()의 내부 처리: invalidate()가 호출되면, 이는 메인 Looper의 MessageQueue에 메시지로 추가되어 다음 화면 갱신 타이밍에 처리됩니다(onDraw).
- 연속적인 호출 시 처리 방식: invalidate()가 연속해서 호출되더라도, 실제로는 첫 번째 호출만 처리됩니다. 이는 View 내부에 있는 플래그 때문입니다. 예를 들어, 1초마다 텍스트뷰의 텍스트를 변경하는 코드에서 view.setText()가 여러 번 호출되어도, 실제로 화면에 반영되는 것은 마지막에 설정된 텍스트("value: " + 4)만입니다. 이는 View가 여러 번의 invalidate() 호출을 무시하고 처음 호출된 것만 처리하기 때문입니다.
requestLayout()
- 뷰의 크기나 레이아웃이 변경되었을 때 전체 레이아웃을 다시 계산해야 할 때 사용