[Android] 5(4). 뷰

6 minute read


뷰 (View)

뷰(View)는 화면을 구성하는 최소 단위의 컴포넌트라고 할 수 있습니다. 계층 구조로 나타낸다면 앱 > 액티비티 > (프래그먼트) > 뷰그룹 > 뷰 로 표시할 수 있습니다.

  • 뷰그룹(ViewGroup): 레이아웃이라고 할 수 있습니다.
  • 뷰(View): UI 편집기의 팔레트에 있는 모든 것들이 뷰라고 할 수 있습니다.

화면을 구성하기 위한 레이아웃과 텍스트뷰, 버튼 등은 모두 최상위 클래스인 View 클래스를 상속받아서 구현합니다.

이번 ‘뷰’ 포스팅에서는 모든 위젯의 기본이 되는 뷰를 직접 사용해보고, 레이아웃에서 태그로 사용되고 있는 TextView 클래스를 상속받아 커스텀 위젯을 만들고 사용해봅니다.


뷰 클래스 다루기


위젯과 레이아웃의 최상위 클래스인 View는 화면에 그림을 그리기 위한 메서드를 가지고 있습니다.

텍스트 뷰 등의 text 속성에 문자열을 지정하면 TextView는 부모 클래스인 View를 호출하고, View는 문자열을 받아 글자크기, 색상, 위치 등을 결정하고 onDraw() 메서드를 사용하여 화면에 그립니다.


뷰에 텍스트 출력하기

gradle에 뷰바인딩 설정을 하고 메인 액티비티에 바인딩을 설정합니다.

1. CustomView 클래스 만들기

MainActivity.kt 파일에 View를 상속받는 CustomView 클래스를 생성합니다. View는 컨텍스트를 생성자에서 입력받아야 하므로 CustomView에는 컨텍스트를 입력받는 생성자가 있어야 합니다.

// View 를 상속받는 클래스
class CustomView(context: Context): View(context){
    ...
}


2. onDraw 메서드 오버라이딩

뷰를 출력할 때 사용하는 onDraw 메서드를 오버라이딩합니다. 파라미터인 Canvas는 일종의 그리기 도구입니다. Canvas에는 그림판과 함께 그림을 그리기 위해서 draw로 시작하는 메서드들이 제공됩니다.

class CustomView(context: Context): View(context){
    // onDraw 메서드 오버라이딩
    override fun onDraw(canvas: Canvas?){
        super.onDraw(canvas)
    }
}


3. drawText 메서드 호출

텍스트 출력에는 Canvas.drawText 메서드가 사용됩니다.

drawText 메서드를 호출할 때는 출력할 문자열, x/y좌표, 글자 색상이나 두께 정보 등을 포함하는 Paint 인스턴스를 전달해야 합니다.

class CustomView(context: Context): View(context){

    override fun onDraw(canvas: Canvas?){
        super.onDraw(canvas)

        // 텍스트를 출력하는 drawText() 메서드는 출력할 문자열, 가로세로 좌표,
        // 글자 색상이나 두께 정보 등을 가지고 있는 Paint 인스턴스가 필요
        val paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 100f // 데이터 타입: float

        // drawText(출력할 문자열, x좌표, y좌표, Paint 인스턴스)
        // (x, y) 좌표는 뷰의 좌측 하단 기준
        canvas?.drawText("안녕하세요", 0f, 100f, paint)

    }
}

drawText 메서드의 파라미터

  • text: 출력할 문자열
  • x: 뷰의 x좌표 (좌측 기준)
  • y: 뷰의 y좌표 (하단 기준)
  • Paint: Paint 인스턴스

뷰의 (x, y) 위치는 좌측 하단을 기준으로 결정됩니다.


5. 레이아웃에 커스텀 뷰 삽입

이제 생성한 커스텀 뷰를 삽입합니다.

먼저 레이아웃을 생성합니다. 텍스트뷰의 text 속성을 Draw Text 로 지정한 뒤 상단에 배치하고, 그 아래에 id가 frameLayout인 프레임 레이아웃을 배치합니다.


레이아웃을 구성했으면 다시 MainActivity.kt 파일로 가서 커스텀 뷰 인스턴스를 생성하고 레이아웃에 삽입합니다.

소스 코드에서 생성된 커스텀 뷰를 레이아웃에 삽입할 때는 레이아웃.addView( ) 메서드를 사용합니다.

 class MainActivity : AppCompatActivity() {
 
     val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(binding.root)
 
         // 커스텀 뷰 생성
         val customView = CustomView(this)
         // 레이아웃.addview() 로 소스 코드에서 생성한 뷰를 레이아웃에 삽입
         binding.frameLayout.addView(customView)
     }
 }


[결과 화면]

image-20210806190319870

6. 출력할 문자열 설정하기

앞의 코드에서는 CustomView 클래스 내에서만 문자열 지정이 가능하지만, 이를 생성자 호출 시 문자열 지정이 가능하도록 코드를 조금 변경합니다.

class MainActivity : AppCompatActivity() {

    val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 커스텀 뷰 생성
        val customView = CustomView("커스텀 뷰 만들기", this)
        binding.frameLayout.addView(customView)
    }
}

// 생성자 호출 시 문자열 지정이 가능하도록 파라미터 추가
class CustomView(text: String, context: Context): View(context){
    // init 블록에서 텍스트 초기화
    val text: String
    init{
        this.text = text
    }
  
    override fun onDraw(canvas: Canvas?){
        super.onDraw(canvas)

        val paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 100f 
				
        // text 프로퍼티 전달
        canvas?.drawText(text, 0f, 100f, paint)

    }
}


[결과 화면]

image-20210806191039702



뷰에 그림 그리기

텍스트 뿐 아니라 일반적인 도형도 뷰에 그릴 수 있습니다. 먼저 도형을 그리는데 필요한 Paint의 프로퍼티에 대해 살펴보고 도형들을 그리는 코드에 대해 살펴보겠습니다.

Paint의 프로퍼티

  • color: 도형의 색상을 정의합니다.
  • style: 도형의 형태. 외곽선을 그리거나 면을 채우는 등의 모양을 정의합니다. 사용할 수 있는 Style은 다음과 같이 상수로 정의되어 있습니다.
    • Style.STROKE, Style.FILL, Style.STROKE_AND_FILL
  • strokeWidth: 외곽선을 그릴 경우 외곽선의 두께를 정의합니다.


1. drawCircle(cx, cy, radius, paint): 원 그리기

val blue = Paint()
blue.style = Paint.Style.FILL
blue.color = Color.BLUE

canvas?.drawCircle(150f, 300f, 100f, blue)

image-20210806210943751


2. drawArc(RectF, startAngle, sweepAngle, useCenter, paint): 원호 그리기

val red = Paint()
red.style = Paint.Style.STROKE
red.color = Color.RED
// RectF 인스턴스 생성
var rect = RectF(160f, 140f, 360f, 340f) // left, top, right, bottom

canvas?.drawArc(rect, 0f, 90f, true, red) // startAngle = 0: 3시 방향

image-20210806211151886


3. drawRect(RectF, paint): 사각형 그리기

val green = Paint()
green.style = Paint.Style.STROKE
green.strokeWidth = 20f
green.color = Color.GREEN
val rect = RectF(50f, 450f, 250f, 650f)

canvas?.drawRect(rect, green)

image-20210806211250730


4. drawRoundRect(RectF, rx, ry, paint): 라운드 사각형 그리기

val cyan = Paint()
cyan.style = Paint.Style.FILL
cyan.color = Color.CYAN

val rect = RectF(300f, 450f, 500f, 650f)

canvas?.drawRoundRect(rect, 50f, 50f, cyan)

image-20210806211329028



커스텀 위젯 만들기


회사에서 프로젝트를 진행하면 보통 기본 위젯을 상속받아 접두어를 붙여(KakaoTextView 등) 커스텀 위젯으로 사용합니다.

커스템 위젯에 사용할 접두어를 정하고 나면 위젯의 커스터마이징은 크게 3단계로 진행됩니다.


1. attrs.xml 파일 생성

새로운 위젯을 생성하고 사용할 때 위젯 이름뿐만 아니라 속성의 이름과 입력되는 값의 타입을 정의하고 사용할 수 있도록 해줍니다.

<declare-styleable name="CustomWidget">
		<attr name="새로운 속성" format="string"/>
</declare-styleable>

레이아웃 파일에서는 태그 속성의 prefix가 android가 아닌 custom을 사용해서 attrs.xml에 정의된 새로운 속성값을 사용할 수 있습니다.

<CustomWidget>
		android: id="@+id/button"
		custom: 새로운 속성="값"
    android: text="새로 만든 위젯이에요"
</CustomWidget>


2. 커스텀 위젯 클래스 생성

커스터마이징을 하기 위한 위젯 클래스를 상속받아 클래스를 생성하고 위에서 새롭게 정의한 속성을 처리하는 코드를 작성합니다.

class CustomWidget: TextView{
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): 
  							super(context, attrs, defStyleAttr){
                  
    }				
}


3. 레이아웃에 태그 적용

생성된 커스텀 클래스를 레이아웃 파일에 태그로 적용하여 삽입합니다. 커스텀 위젯은 컨스트레인트 레이아웃처럼 위젯 클래스의 패키지 경로명도 함께 입력해서 사용합니다.

<패키지명.CustomWidget>
    android:id = "@+id/button"
    custom: 새로운 속성="값"
    android: text = "새로 만든 위젯이에요"/>


👍 예시

이번에는 위의 과정을 바탕으로 ‘20210101’ 이 입력되면 연월일을 구분하기 위해 연월일 사이에 구분값(delimeter)으로 ‘- (하이픈)’을 자동으로 입력해서 화면에 출력하는 위젯을 만들겠습니다. 부가적으로 delimeter 속성을 하나 만들어 구분자를 직접 받을 수 있도록 하겠습니다.

커스텀 TextView 설계

1. attrs.xml 파일 생성

[app] - [res] - [values] - 마우스 우클릭 - [New] - [Value Resource File] 에서 이름에 attrs를 입력하고 [OK]를 눌러 파일을 생성합니다.

그리고 생성된 파일의 < resources > 태그 사이에 delimeter 속성을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomText">
        <attr name="delimeter" format="string"/> // 구분자 속성
    </declare-styleable>
</resources>

이렇게 커스텀 속성 정보를 정의하면 activity_main.xml과 같은 레이아웃 파일에서 새로운 태그로 사용할 수 있습니다.


2. CustomText 클래스 생성

[app] - [java] - 패키지명 우클릭 - [New] - [Kotlin File/Class]를 선택하여 클래스명으로 ‘ CustomText’를 입력하고 클래스를 생성합니다.

CustomText 클래스는 AppCompatTextView를 상속받아 생성자를 3개 구현합니다. 각각에 대한 설명은 아래 소스코드를 참조합니다.

// 버전 호환성을 위해 기본 위젯인 TextView가 아니라 AppCompatTextView를 상속
class CustomText: AppCompatTextView {

    // 소스코드에서 사용할 때 호출
    constructor(context: Context): super(context){

    }
    // 레이아웃에서 사용할 때 호출
    constructor(context: Context, attrs: AttributeSet): super(context, attrs){
		
    }
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) {

    }


}

커스텀 위젯은 레이아웃에서도 사용되지만 코드에서도 직접 사용할 수 있기 때문에 항상 3개의 생성자를 모두 작성해두는 것이 좋습니다.


3. 속성 처리 관련 코드 작성

레이아웃에서 사용할 것이기 때문에 두번째 생성자에 다음 코드를 작성합니다.

// 레이아웃에서 사용할 때 호출
constructor(context: Context, attrs: AttributeSet): super(context, attrs){
    // res/values/attrs.xml에 정의된 어트리뷰트를 가져온다.
    val typed = context.obtainStyledAttributes(attrs, R.styleable.CustomText)
    val size = typed.indexCount

    for (i in 0 until size){
        when(typed.getIndex(i)){
            // 현재 속성을 확인하고 delimeter와 같으면
            R.styleable.CustomText_delimeter->{
                // XML에 입력된 delimeter 값을 꺼내고
                val delimeter = typed.getString(typed.getIndex(i)) ?: "-"
                // 꺼낸 값을 process 메서드에 전달하여 호출한다.
                process(delimeter)
            }
        }
    }
}


그리고 delimeter를 전달하여 문자열을 처리할 process 메서드를 생성합니다.

class CustomText: AppCompatTextView {

    // delimeter와 입력된 값을 조합해서 처리
    fun process(delimeter: String){
        // 텍스트 자르기
        var one = text.substring(0, 4)
        var two = text.substring(4, 6)
        var three = text.substring(6)

        setText("$one $delimeter $two $delimeter $three")
    }
    ...


레이아웃에서 CustomText 사용

4. 레이아웃에 커스텀 위젯 가져다 놓기

activity_main.xml 파일로 가면 팔레트의 가장 아래에 프로젝트(Project) 카테고리가 생성되어 있고, 우리가 생성한 커스텀 위젯이 들어있습니다.

커스텀 위젯을 레이아웃에 배치한 후 text 속성에 20210101을 입력합니다.

image-20210807103953174

5. delimeter 속성 지정

이제 커스텀 위젯의 All Attributes를 보면 delimeter 속성이 추가되어 있습니다. delimeter 속성에 ‘-‘을 입력하고 에뮬레이터를 실행하면 다음과 같은 결과가 나옵니다.

image-20210807104232945



정리


  • View: 화면에 보이는 모든 요소의 최상위 클래스입니다. 화면에 무엇인가를 그리기 위해서는 View 클래스가 상속받아져 있어야 합니다.

  • onDraw( ) 메서드: View 클래스가 화면에 텍스트를 출력하거나 그림을 그릴 때 호출하는 메서드입니다.

  • Canvas: onDraw( ) 메서드를 통해 전달되는 그리기 도구입니다. drawText( ), drawCircle( ) 등의 그리기 메서드를 이용하여 화면에 그릴 수 있습니다.

  • Paint: 화면에 그려지는 요소들의 색상, 스타일, 굵기 정보 등을 정의하는 클래스입니다.

  • attrs.xml: 내가 만든 위젯에 새로운 속성을 정의할 때 사용되는 리소스 파일입니다.

  • custom: attrs.xml에 정의한 새로운 속성을 custom이라는 Prefix로 레이아웃에서 사용할 수 있습니다.

Categories:

Updated:

Leave a comment