Android - CustomView 만들기
CustomView
View를 Customizing 하는 것은 중요하다. 도표, 커스텀 원형 그래프. 뷰의 재사용등 아주 중요하다.
그래서 CutomView를 더 자세히 공부해 보려고 한다.
Flutter에서는 UI자체를 Widget으로 만들어 Stateful 혹은 Stateless하게 Custom이 가능 하다.
하지만 안드로이드의 경우 View를 상속받는 CustomView를 처리해주어야 깔끔하게 View를 만들 수 있다.
1) CustomView란?
CustomVeiw는 4개의 큰 패더라임을 기억해야 합니다. 일단 생성자인 Constructor를 생성 한 뒤 OnMeasure을 통한 View의 크기를 설정, OnLayout을 통한 어디에 그릴지 위치를 설정, OnDraw를 마지막으로 색상값, 모양등 어떤 그림을 그릴지 선택합니다.
CustomView는 흰 도화지에 그림을 그리듯이 만들어 나가는 재미가 있다.
1 - 1. Constructor
뷰는 최대 4개의 생성자를 가집니다.
- View(Context context) 코드에서 동적으로 뷰를 생성할 때 사용할 수 있는 간단한 생성자입니다. 파라미터 context를 통해 현재 실행중인 뷰의 리소스 등에 액세스 할 수 있습니다.
- View(Context context, AttributeSet attrs) : xml에서 생성할 때
- View(Context context, AttributeSet attrs, int defStyleAttr) : ThemeStyle과 함께 뷰를 생성할 때
- View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) : ThemeStyle 또는 Style로 xml에서 뷰를 생성할 때
1 - 2. onMeasure
이 메소드에서는 해당 커스텀 뷰의 사이즈를 지정해줘야 합니다. xml에서 유저가 설정한 width, height의 정보가 파라미터로 넘어옵니다. 우리는 MeasureSpec.getMode(~)를 통해 MATCH_PARENT, WRAP_CONTENT 또는 100dp와 같이 지정된 값인지 알 수 있습니다.
onMeasure은 여러 번 호출될 수 있습니다. 예를 들어 부모가 자식들의 각 크기를 측정한 뒤, 자식들의 크기의 합이 너무 크거나 작다면 다시 measure() 메소드를 호출하여 구체적인 값을 구합니다.
child view를 가지는 커스텀 뷰라면 child의 사이즈를 측정해서 자신의 사이즈를 재야 할 수도 있는데, 이 메소드에서 설정해주면 됩니다.
1-3. onLayout
뷰의 위치를 설정해주는 함수입니다. 뷰의 child들의 크기와 위치를 할당해야 할 때 호출됩니다. 즉, child를 가지는 뷰라면 해당 메소드를 오버라이드 해주어야 합니다. 이때 파라미터로 넘어오는 값들은 어플리케이션 전체를 기준으로 넘어오는 위치값임을 알아야 합니다.
1-4. onDraw
뷰에 그림을 그리는 메소드입니다. Paint 클래스를 통해 도형을 그릴 수도 있고, canvas에 텍스트를 추가할 수도 있습니다.
onDraw()에서는 많은 시간이 소요되거나 여러 번 호출될 수 있기 때문에(초당 60번) 되도록 객체 선언, 할당을 피하고 기존 객체를 재사용하는 것이 좋습니다. 이보다 가비지 컬렉터가 더 빨라서 GC 관련된 drop이 없을 수도 있지만, 이 동작 역시 별도의 스레드에서 진행되므로 배터리 소모를 야기할 수 있습니다. 또한, onDraw에서 초기화되는 객체들은 주로 drawing object인데, 이들은 많은 소멸자를 호출하기 때문에 성능에 영향을 줄 수 있습니다.
참고자료
http://labs.brandi.co.kr/2021/10/14/jeonhs.html
2) 실 습
- CustomView.kt
// View Class를 상속 받고 생성자를 정의 해준다.
// View 클래스에는 4개의 생성자가 정의되어 있으며, 아래의 2개 생성자는 항상 정의해 두는 편이 좋다
class CustomView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
// Paint 객체를 생성해 뷰의 속성(색상, 크기 등)을 정의 한다.
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val paint = Paint()
paint.color = Color.BLUE
paint.style = Paint.Style.FILL
// Canvas객체로 도형(원) 그리기 매개변수: 중심의 X좌표, 중심의 Y좌표, 반지름, Paint객체
canvas?.drawCircle(CXSIZE, CYSIZE, 150f, paint)
paint.color = Color.YELLOW
paint.textSize = 40f
canvas?.drawText("커스텀 뷰", (CXSIZE *0.7).toFloat(), CYSIZE, paint)
}
companion object{
val CXSIZE = 300f
val CYSIZE = 300f
}
}
첫번째 CustomView는 간단한 원을 그리는 View이다 우선 View를 상속 받아 constructor 생성자를 선언 해주었다. 위에 나온 4개의 생성자중 2개를 선언 해주었다. 간단하게 @JvmOverloads를 사용하여 생성자를 가질 수 있다.
Canvas객체에는 다양한 그리기 메서드가 지원된다.
- drawText() : 텍스트 그리기
- drawCircle(): 원 그리기
- drawArc(): 호 그리기
- drawRect(): 사각형 그리기
- drawLine(): 선 그리기
- drawBitmap(): 비트맵 이미지 그리기
- 더많은 메서드는 레퍼런스 참고: https://developer.android.com/reference/kotlin/android/graphics/Canvas
- XML을 이용하여 CustomView를 추가하는 방법 이 가장 많이 사용 된다.
- InValidateCustomView.kt
class InValidateCustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
// 터치 X좌표 값을 저장할 변수
var coords: PointF? = null
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas) // 텍스트 사이즈 설정
val paint = Paint()
paint.textSize = 70f // coordX 변수에 저장된 값을 텍스트로 그린다.
canvas?.drawText("${coords?.x} / ${coords?.y}", 100f, 600f, paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// 1. 터치 좌표 취득
coords = PointF(event!!.x, event!!.y)
var action = ""
when (event.action) {
MotionEvent.ACTION_DOWN -> {
action = "ACTION_DOWN"
}
MotionEvent.ACTION_MOVE -> {
action = "ACTION_MOVE"
}
MotionEvent.ACTION_UP -> {
action = "ACTION_UP"
}
MotionEvent.ACTION_CANCEL -> {
action = "ACTION_CANCEL"
}
}
Log.d("this", "Action : ${action}")
// 화면 다시그리기 !! 중요
invalidate()
return true
}
}
OnTouchEvent를 통해 좌표 값을 UI에 반영 가능하다.
여기서 MotinEvent는 DOWN, UP, MOVE, CANCLE 등 눌렀을 때, 놓았을 때, 움직였을 때, 취소됐을 때 와같이 다양한 경우의 이벤트 동작 처리를 해줄 수 있다.
- AttrCustomView.kt
class AttrCustomView : View {
// 커스텀 속성을 참조하기 위한 변수
private var myShapeColor: Int? = null
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
if (attrs != null && context != null) { // attr.xml파일 declare-styleable이 CustomView로 정의된 attr(속성)을 typeArray객체로 받아온다.
val typedArr = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
// format을 구분하여 속성값 참조
myShapeColor = typedArr.getColor(R.styleable.CustomView_myShapeColor, Color.YELLOW)
}
}
// context.obtainStyledAttributes() 메서드를 호출하면 attr.xml에 정의된 속성 정보들이 typedArray 객체로 반환된다.
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val paint = Paint()
paint.color = myShapeColor ?: Color.BLACK
canvas!!.drawRect(100f,100f,350f,350f,paint)
}
}
CustomView를 그릴 때 View마다 다른 속성을 가지는 View를 그릴 때 가 있다 그 때 사용하는 것이 attrs 속성 타입 설정이다.
res - menu - attrs에 resourse값을 추가해주어 Customizing된 속성값을 적용시켜 줄 수 있다.
- context.obtainStyledAttributes() 메서드를 호출하면 attr.xml에 정의된 속성 정보들이 typedArray 객체로 반환된다.
- 속성의 format에 따라 typedArray 객체에서 getString(), getInt(), getColor() 등을 호출해주면 속성 값이 반환된다.
출처: https://curryyou.tistory.com/408?category=961282 [카레유]
- attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="myShapeColor" format="color" />
<attr name="myTextColor" format="color" />
<attr name="myTextString" format="string" />
<attr name="myTextSize" format="string" />
<attr name="myStrokeColor" format="color" />
<attr name="myStrokeWidth" format="string" />
</declare-styleable>
</resources>
Conclusion
커스텀 뷰는 개인의 조건에 맞게 onMeasure(), onLayout(), onDraw()를 작성하면 더 효율적인 View가 완성 될 것 같다.
그래프, 원형 뷰 등 다양한 뷰를 onMeasure(), onLayout(), onDraw()조건에 맞게 커스터마이징 하는 실습을 해보면서 CustomView를 더욱더 이해를 하도록 해야겠다.