커스텀 뷰란?
기본적으로 제공되는 뷰 컴포넌트를 사용자의 필요에 맞게 확장하거나 새로 정의하여 사용자 인터페이스를 정의하는 방법이다.
언제 사용되는 걸까?
- 기본적인 뷰들로는 표현하기 어려운 복잡한 사용자 인터페이스를 구현할 때 (복잡한 애니메이션 효과, 정밀한 조작이 필요할 때)
- 안드로이드 표준 뷰에서 제공하지 않는 사용자 인터렉션을 구현할 필요가 있을 때
ex) 멀티 터치를 사용하는 그림판, 복잡한 제스처를 인식하는 인터페이스를 구현할 때 - 기존 뷰에 없는 기능을 추가하거나 기존 뷰의 동작 방식을 변경해야할 때
ex) Buttom에 추가적인 그래픽 효과 적용, TextView에 특별한 텍스트 렌더링 로직을 추가하는 경우 - 일반적인 UI 요소를 여러 프로젝트에서 재사용하려는 경우 CustomView를 만들어 라이브러리 형태로 배포 할 수 있다.
커스텀 뷰 구현시 주로 사용하는 두 가지 방법
- 안드로이드에서 이미 제공하는 뷰 클래스 TextView, Button, ImageView 등을 상속 받아 해당 클래스를 확장한다.
- View 클래스 또는 그 서브 클래스를 직접 상속 받아 새로운 뷰를 구현한다.
onDraw() 메소드를 재정의하고, 뷰의 크기를 결정하는 onMeasure() 메소드를 재정의하는 것이 보통 일반적이다.
예시
간단한 예시로 Cirecular Progress Indicator를 만드는 방법으로 살펴보자
첫 번째: Custom View 클래스 만들기
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
class CircularProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint: Paint = Paint()
private var progress: Int = 0
init {
paint.apply {
color = Color.BLUE // 원의 색상 설정
strokeWidth = 10f // 선의 두께 설정
style = Paint.Style.STROKE // 선으로만 그릴 것인지 설정
isAntiAlias = true // 부드러운 선을 위한 안티 앨리어싱 설정
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width
val height = height
val radius = Math.min(width, height) / 2
canvas.drawCircle(width / 2f, height / 2f, radius.toFloat(), paint)
// 진행률에 따라 원형 부분 그리기
val rectF = RectF(
(width / 2 - radius).toFloat(),
(height / 2 - radius).toFloat(),
(width / 2 + radius).toFloat(),
(height / 2 + radius).toFloat()
)
canvas.drawArc(rectF, -90f, 360 * progress / 100f, false, paint)
}
fun setProgress(progress: Int) {
this.progress = progress
invalidate() // 뷰 다시 그리기
}
}
두 번째: XML 레이아웃에 커스텀 뷰 추가하기
만들어진 CircularProgressView를 XML에 추가한다.
<com.example.CircularProgressView
android:id="@+id/circularProgress"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true" />
onDraw()와 onMeasure()
커스텀뷰를 만들 때 오버라이딩 될 View 클래스의 함수중 제일 중요한 두 함수는 onDraw()와 onMeasure()이다
onDraw(Canvas canvas)
뷰를 그려주는 함수
onDraw() 메소드는 Canvas 라는것을 제공한다. 이 Canvas로 원하는 2D graphic을 구현 할 수 있다
만약 2D가 아닌 3D 그래픽을 구현하고 싶다면 View가 아닌 SurfaceView를 상속받아야 한다.
그리고 2D와 3D는 다른 스레드에서 그려진다.
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
뷰의 크기를 결정해주는 함수.
디폴트값으로 100x100 사이즈를 제공함
뷰의 크기를 계산하고 width와 height가 결정이 되면 이 함수 안에 setMeasuredDimension(int width, int height)를
반드시 호출해주어야 한다.
여러 함수들
onFinishInflate() | XML으로 파싱되어 뷰와 그 자식들이 inflated가 완료되면 호출됨 |
onMeasure(int, int) | 뷰와 뷰 자식들의 사이즈를 결정한다. |
onLayout(boolean, int, int, int, int) | 뷰의 자식들의 사이즈와 포지션을 결정할 때 호출된다. |
onSizeChanged(int, int, int, int) | 뷰의 사이즈가 바뀔 때 호출된다. |
onDraw(Canvas) | 뷰를 그릴 때 호출된다. |
onFocusChanged(boolean ,int, Rect) | 포커스를 얻을 때와 잃을 때 둘다 호출된다. |
onWindowFocusChanged(boolean) | 뷰를 가진 윈도우가 포커스를 얻을 때와 잃을 때 둘다 호출된다. |
onAttacedToWindow() | 뷰가 윈도우에 attach될 때 호출된다. |
onDetachedFromWindow() | 뷰가 윈도우에 detache될 때 호출된다. |
onWindowVisibilityChanged(int) | window의 visible이 바뀔 때 호출된다. |
Custom Attributes 정의, 맞춤 속성 정의
위 코드에서 해당 커스텀 뷰를 아래와 같이 정의 했었습니다.
class CircularProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs)
여기서 AttributeSet 타입의 매개 변수를 통해 xml로 뷰를 만들때,
XML에 정의된 태그 데이터들이 AttributeSet이라는 형태로 데이터가 전달된다.
클래스가 준비되어 있다면 Cusstom attributes의 구조를 <declare-styleable> 엘리먼트에서 정의한다.
<resources>
<declare-styleable name="CircularProgressView">
<attr name="progressColor" format="color" />
<attr name="progressWidth" format="dimension" />
<attr name="progress" format="integer" />
</declare-styleable>
</resources>
코드에 적용
class CircularProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint: Paint = Paint()
private var progress: Int = 0
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.CircularProgressView,
0, 0).apply {
try {
val progressColor = getColor(R.styleable.CircularProgressView_progressColor, Color.BLUE)
val progressWidth = getDimension(R.styleable.CircularProgressView_progressWidth, 10f)
paint.apply {
color = progressColor
strokeWidth = progressWidth
style = Paint.Style.STROKE
isAntiAlias = true
}
progress = getInt(R.styleable.CircularProgressView_progress, 0)
} finally {
recycle()
}
}
}
// ...
// onDraw 메소드와 setProgress 메소드는 이전 예제와 동일
// ...
}
xml로 ui를 정의하면 tag값에 적힌 데이터들은 AttributeSet으로 전달된다.
데이터를 AttributeSet에서 직접 읽어 올 수 있지만 다음과 같은 단점이 존재한다.
- 속성값에 대한 리소스 참조가 결정되지 않음
- 스타일이 적용되지 않음
공식문서에서는 맞춤속성 값을 읽어오기위해 obtainStyledAttributes() 메소드를 이용하라고 권장한다.
이 메소드는 리턴값으로 TypedArray를 넘겨주며 값이 dereferenced(값을 읽어왔다)가 되어있고, styled 되어있다고 합니다.
그리고 TypedArray를 다 사용하였디면 항상 recycle()함수를 호출하라고 공식문서에 명시되어 있다.
10. 커스텀뷰의 프로퍼티 노출
초기에 XML에서 커스텀 뷰를 정의할 때 속성을 사용하여 뷰의 프로터피에 값을 전달하였다.
하지만 이 속성들은 뷰가 그려질 때 딱 한번만 사용된다.
객체화된 View의 프로퍼티들의 값을 핸들링 하고 싶으면 어떻게 할까요? getter, setter가 필요할 것입니다.
var showText: Boolean = false
set(value) {
field = value
invalidate() // 뷰를 다시 그리도록 요청
requestLayout() // 레이아웃 재계산 요청
}
setter를 통해 mShowText에 값을 바꾸어도 화면에 보이는 UI는 바로 바뀌지 않는다.
UI를 바뀌게 하려면, UI에 객체의 프로퍼티값이 바뀌었다고 알려야 하기 때문
이를 가능하게 하는 것이 바로 invalidate()함수와 requestLayout() 함수이다.
invalidate는 무효화하다 라는 뜻이며,
이미 그려져 있는 View를 무효화해서 시스템에게 이 View가 다시 redrawn 되어야 함을 알리고,
requestLayout() 함수를 통해 UI를 다시 그려달라 요청하는 것이다.
이에 공식문서에서는 UI에 영향을 줄 수 있는 프로퍼티는 항상 값을 노출하도록(게터세터를 정의하도록) 하는것을 권장하고 있다.
'Android' 카테고리의 다른 글
Android - UseCase 추상화 (0) | 2024.03.15 |
---|---|
Android - Image Preloading Trouble Shooting (1) | 2023.12.30 |
안드로이드 - Jetpack Compose Navigation Test Code (0) | 2023.12.30 |
Android - JUnit 단위 테스트 (0) | 2023.07.04 |
Android - GitHub에 API Key, Hash 값 숨기기 (0) | 2023.01.05 |