커스텀뷰에 대해서 정리를 하려고 한다.
커스텀 뷰 란?
안드로이드 앱을 개발하다 보면 일반적인 뷰로는 내가 원하는 결과를 만들 수 없는 경우가 발생하는데, 이런 경우 직접 ‘커스텀 뷰’를 만들어야 한다.
안드로이드의 UI 요소는 모두 View를 상속한다.
아래에 그림을 참고하자.
View를 상속하여 UI 요소의 외형을 그리고 UI이벤트를 처리한다.
CustomView를 사용하는 이유는 View의 재사용을 위해서다. 디자인이나 React 같은 프레임워크에서는 이런 개념을 컴포넌트라고 부른다. 디자인과 UX의 통일성을 유지하기 위해 동일한 형태의 UI를 여러 군데에서 사용하게 되는데 이런 UI를 사용하는 곳마다 각각 구현하기에는 코드의 길이도 길어지고, 수정 및 관리하기에도 어렵다.
CustomView의 핵심은 onMeasure, onDraw, onLayout이다. 도화지 크기를 선택하고(onMeasure), 어느 위치에(onLayout) 어떤 그림을 그릴지(onDraw) 설정해주면 커스텀 뷰는 완성된다.
예제)
class MyView : View {
var rect = Rect(10, 10, 110, 110)
var color = Color.BLUE
private var paint = Paint()
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = color
canvas.drawRect(rect, paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN ||
event.action == MotionEvent.ACTION_MOVE) {
rect.left = event.x.toInt()
rect.top = event.y.toInt()
rect.right = rect.left + 100
rect.bottom = rect.top + 100
invalidate()
return true
}
return super.onTouchEvent(event)
}
}
• onDraw() : 시스템이 onDraw를 호출하여 외형을 그린다.
• onTouchEvent() : DOWN, UP, MOVE 등의 이벤트를 처리할 수 있으며 클릭 이벤트(performClick)를 만들 수 있음
• onMeasure() : 뷰의 크기를 결정해주는 함수이다. 디폴트 값으로 100x100 사이즈를 제공한다. 뷰의 크기를 계산하고 width와 height가 결정이 되면 이 함수 안에 setMeasuredDimension(int width, int height)를 반드시 호출해 주어야 한다. 이 함수를 call 하는데 실패하면 exception이 난다.
캔버스와 페인트
View의 onDraw()에서 시스템이 Canvas 객체를 인자로 전달
Canvas 메서드
- drawColor() - 배경색을 지정
- drawPoint() - 점 그리기
- drawLine(0 - 선 그리기
- drawRect(), drawRoundRect() - 사각형, 둥근사각형 그리기
- drawCircle() - 원 그리기
- drawOval() - 타원 그리기
- drawPath() - 경로 그리기
- drawText() - 글자 출력
- drawBitmap() - 비트맵 출력
그래픽 요소를 그릴때, 색, 두께, 폰트 등의 모양을 결정하는 Paint 객체를 인자로 전달하여 다르게 그릴 수 있음
Paint 속성, 메서드
- reset() - 초기화
- color - 색 지정
- isAntiAlias - 안티앨리어싱
- style - 스타일, Style.STROKE(테투리만), Style.FILL(채우기만), Style.FILL_AND_STROKE(테투리와 채우기 모두)
- strokeWidth - 테두리 굵기
- textSize - 글자 크기
- textAlign - 글자 정렬
- typeface - 폰트, Typeface.SERIF, Typeface.SANS_SERIF 등
Invalidate()
언제 onDraw가 호출되는가? : view가 그려질 필요가 있을 때 시스템이 알아서 호출
원하는 때에 다시 그리도록 하고 싶다면? : view의 invalidate() 호출
예를 들어 터치할 때 마다 다른 모양을 그리고 싶으면 터치할 때마다 invalidate() 호출 onDraw()에서는 매번 다른 도형을 그리도록 함
onTouchEvent()
VIew의 onTouchEvent()를 재정의하여 뷰를 터치 이벤트를 처리할 수 있음.
setOnClickListener
myView에 setonClickListener를 등록해보면?
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val v = findViewById<View>(R.id.view)
v.setOnClickListener {
println("Click #########################") }
}
}
myView를 클릭해도 아무런 출력이 없음 왜냐 onTouchEvent에서 true를 리턴하기 때문에 클릭 이벤트가 발생되지 않음
어떻게 하면 클릭 이벤트가 발생할까?
- performClick()을 호출
- onTouchEvent에서 true를 리턴하지 않게 하면, 시스템이 해당 터치 이벤트를 계속 처리하여 결국 클릭 이벤트를 발생 시킴
결론
사실 커스텀 뷰는 개인의 조건에 맞게 onMeasure(), onLayout(), onDraw()를 작성해야하기 때문에 구글에 의존하기에는 많이 어렵다. 그래서 커스텀 뷰의 과정을 잘 이해하는 것이 중요하다고 생각한다.
실습 코드
터치할 때 사각형만 그리는 것이 아니라 원이나 삼각형 등 다양한 도형을 그리도록 한다. 이때 어떤 도형을 그릴 지는 랜덤하게 결정한다.
MainActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
MyView(CustomView)
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.random.Random
class MyView : View {
private var rect = Rect(10, 10, 110, 110)
private var circleX = 100F
private var circleY = 100F
private var circleR = 50F
private var triangleX = 100F
private var triangleY = 100F
private var triangleW = 100F
private var color = Color.BLUE
private var paint = Paint()
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = color
when (Random.nextInt(3) + 1) { //난수생성
1 -> { //1일경우 사각형
canvas.drawRect(rect, paint)
}
2 -> { //2일경후 원
canvas.drawCircle(circleX, circleY, circleR, paint)
}
3 -> { //3일경우 삼각형
val hw = triangleW / 2
val path = Path()
path.moveTo(triangleX, triangleY - hw)
path.lineTo(triangleX - hw, triangleY + hw)
path.lineTo(triangleX + hw, triangleY + hw)
path.lineTo(triangleX, triangleY - hw)
path.close()
canvas.drawPath(path, paint)
}
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN ||
event.action == MotionEvent.ACTION_MOVE
) {
rect.left = event.x.toInt()
rect.top = event.y.toInt()
rect.right = rect.left + 100
rect.bottom = rect.top + 100
circleX = event.x
circleY = event.y
triangleX = event.x
triangleY = event.y
invalidate()
return true
}
return super.onTouchEvent(event)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<view
android:id="@+id/view"
class="com.example.myapplication.MyView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1" />
</androidx.appcompat.widget.LinearLayoutCompat>
실행 결과
'Android > Study' 카테고리의 다른 글
[Android] MVP 패턴이란? (0) | 2023.01.01 |
---|---|
[Android] MVC 패턴이란? (0) | 2023.01.01 |
[Android] Notification(노티피케이션) (0) | 2022.10.18 |
[Android] CustomView 추가 실습 (0) | 2022.09.28 |
[Android] 안드로이드 취업시 회사에서 요구하는 기본적인 사항 (0) | 2022.09.04 |