Android Developer

RadarChartView 직접 만들기 : 2. Creating a View Class

졸려질려 2022. 10. 12. 14:40
반응형
 

RadarChartView 직접 만들기 : 1. CustomView Overview

 개발을 하다보면 외부 라이브러리를 쓰기 보다는 직접 필요한 라이브러리를 만들어서 쓰고 싶은 낭만(?) 같은게 생긴다. 특히, 필자는 나만의 CustomView 를 만들고 싶다는 생각이 Android 를 처음

choboit.tistory.com

지난 글에서는 CustomView 에 대한 이론적인 내용을 다뤄보았다. 이번에는 CustomView 를 만드는 기본적인 방법에 대해 기술한다.

출처(공식 문서) : https://developer.android.com/develop/ui/views/layout/custom-views/create-view


1. Overview

 잘 만들어진 CustomView 는 마치 잘 만들어진 Class 와 같다. 사용하기 쉽도록 특정 기능들이 모아져있고, CPU 와 메모리를 효과적으로 사용하기 때문이다. 거기에 더해 CustomView 는 다음 사항들이 있어야한다.

  1. Android 표준 준수
  2. Android XML 레이아웃에사 동작 가능한 속성들을 제공
  3. 접근성 이벤트 전송
  4. 여러 Android 플랫폼들과 호환 가능

 안드로이드 프레임워크가 위 사항들을 모두 충족시켜주면서, CustomView 를 만들기 쉽게 해줄 수 있는 기본적인 Class 와 XML 태그들을 제공한다. 이번 문서에서는 Android 프레임워크를 사용하여 View 클래스의 핵심 기능들을 만드는 방법에 대해 정리한다.


2. Subclass a View

 Android 프레임워크에서 정의된 모든 View 클래스들은 "View" 클래스를 상속 받는다. 필자가 만들고자 하는 CustomView 또한, View 클래스를 상속 받거나 View 클래스를 상속 받은 다른 위젯을 상속 받아야한다. 그리고 View 와 상호작용하기 위해서는 생성자에서 Context 와 AttributeSet 파라미터를 얻어야한다. 해당 생성자를 통해 Layout Editor 는 그리고자하는 View 를 생성하고 수정할 수 있게 해준다.

// PieChart.kt

import android.content.Context
import android.util.AttributeSet
import android.view.View

class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
    
}

3. Define Custom Attributes

 직접 구현한 CustomView 를 UI 의 일부로서 사용하고 싶다면, XML element 로 사용할 수 있고, 외형과 동작들을 element 의 속성값을 통해 조정할 수 있어야한다. 잘 그려진 커스텀 뷰들은 XML 을 통해 스타일이 변하고, 추가될 수 있다. 이러한 기능들을 사용하기 위해서는 다음 과정을 거치게 된다.

  1. <resource> element 내부에서 <declare-styleable> element 로 커스텀 뷰의 속성들을 정의한다.
  2. XML 레이아웃에서 속성들의 값(value)을 정의한다.
  3. 런타임에 속성값을 탐색한다.
  4. 탐색한 값들을 커스텀 뷰에 적용한다.

 이번 섹션은 커스텀 뷰의 속성을 정의하고, 속성의 값을 특정하는 방법에 대해 정리한다. 다음 섹션에서는 런타임에서 값을 탐색하고 적용하는 방법에 대해 다룬다. 먼저, <declare-styleable> element 를 추가하여, 만들고자 하는 커스텀 뷰의 속성을 정의해야한다. <declare-styleable> 은 상위에 <resource> element 가 존재하고, 해당 파일은 "res/values/attrs.xml" 로 만들어서 관리한다.

<!-- res/values/attrs.xml -->

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PieChart">
        <attr name="showText" format="boolean" />
        <attr name="labelPosition" format="enum">
            <enum name="left" value="0" />
            <enum name="right" value="1" />
        </attr>
    </declare-styleable>
</resources>

 위 코드를 보면 두 개의 커스텀 속성들이 선언 된 것을 볼 수 있다. "showText" 와 "labelPosition" 이라는 이름의 속성들을 정의하고 있고, 해당 속성들은 "PieChart" 라는 이름의 <declare-styleable> element 안에 있다. "PieChart" 라고 값을 넣은 것은, View 를 상속 받은 커스텀뷰 클래스의 이름이 "PieChart" 이기 때문이다. 즉, <declare-styleable> 의 "name" 속성 값은 attr 을 사용하고자 하는 커스텀뷰 클래스의 이름과 같아야한다. 해당 컨벤션을 꼭 지킬 필요하는 없지만, 대부분의 코드 에디터가 동일한 이름의 커스텀뷰 코드 파일과 매칭하기 때문에 동일한 이름으로 지어주는 것이 좋다.

 이제 XML 에서 "PieChart" 뷰를 사용할 때, 위 코드에서 정의한 속성들을 바로 사용할 수 있다. 정의한 커스텀 속성들을 사용하기 위해서는 네임스페이스를 먼저 선언해줘야한다. 커스텀 뷰의 속성을 사용할 수 있는 네임스페이스는 "app" 이다. ConstraintLayout 을 주로 사용한다면 자주 만날 수 있는 네임스페이스(namespace)이다. 

<!-- res/layout/activity_main.xml -->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.study_customview.PieChart
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:showText="true"
        app:labelPosition="left" />

</LinearLayout>

 위 코드처럼 "app" 네임스페이스를 통해 "showText" 와 "labelPosition" 속성들을 사용할 수 있는 것을 볼 수 있다. 만약, "xmlns:app=~~~" 선언부를 직접 작성하기 귀찮다면, LinearLayout 안에서 "appNs" 만 치면 자동완성으로 간단하게 작성할 수 있다.

[appNs] 자동완성

 이제 XML 에서 초기화한 "showText" 와 "labelPostion" 의 속성값을 코드에서 어떻게 전달받고, 응용할 수 있는지에 대해 정리한다.


4. Apply Custom Attributes

 섹션 3에서 커스텀 뷰의 속성을 정의하고, XML 코드에서 정의한 속성의 값을 초기화하는 방법에 대해 정리했다. 이번에는 XML 에서 설정한 속성값을 Kotlin(Java) 코드로 어떻게 가져오고, 응용할 수 있는지에 대해 정리한다.

 View 가 XML 레이아웃에서 생성될 때, XML 태그에 있는 모슨 속성들(attributes)은 resource bundle 로부터 읽어지게 되고, "AttributeSet" 으로 해당 View 의 생성자를 통해 전달된다. 그렇다면 AttributeSet 타입의 변수로부터 속성값들을 바로 얻으면 될 것 같지만, 직접적으로 속성값을 얻으려고 할 경우, 다음의 불이점이 발생한다.

  • 속성 값 내의 리소스 참조가 인식되지 않음
  • Styles 이 적용되지 않음

 그래서 AttributeSet 으로부터 속성값을 바로 읽어내는 것 보다는, 전달받은 AttributeSet 을 인자값으로 하여 "obtainStyledAttributes()" 메소드를 호출한다. 해당 메소드는 호출된 후에 역참조되고 스타일이 지정된 값의 TypedArray 배열을 다시 전달한다.

 Android 리소스 컴파일러는 "obtainStyledAttributes()" 메소드를 더 쉽게 호출할 수 있도록 해준다. "res" 디렉토리에서 선언한 <declare-styleable> 리소스들을 R.java 에서 "속성 id 의 배열" 과 "배열에서 각 속성의 인덱스 값을 정의한 상수값의 집합" 을 정의하도록 해준다. 따라서, "obtainStyledAttributes()" 에서 반환해주는 TypedArray 로부터 속성값을 읽어내기 위해 미리 정의된 상수값들을 사용하면 된다. 설명으로는 이해가 잘 안될 수 있으니, 다음 예제 코드를 보도록 한다.

// PieChart.kt

class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private var showText: Boolean = false
    private var textPosition: Int = -1

    init {
        context.theme.obtainStyledAttributes(attrs, R.styleable.PieChart, 0, 0).apply {
            try {
                showText = getBoolean(R.styleable.PieChart_showText, false)
                textPosition = getInteger(R.styleable.PieChart_labelPosition, 0)
            } finally {
                recycle()
            }
        }
    }
}

 이전에 <declare-styleable> 태그 안에 정의했던 <attr> 의 이름들은 "showText", "labelPosition" 이었으나, 커스텀 뷰의 이름인 "PieChart" 와 속성명을 결합하여 R.styleable 의 하위에서 id 값을 사용할 수 있도록 해준다. 위 코드를 통해 XML 에서 설정한 "showText" 와 "labelPosition" 값을 Kotlin 코드에서 사용할 수 있다.


5. Add Properties and Events

 View 의 속성(Attribute) 는 View 의 외형과 동작을 제어할 수 있는 가장 큰 역할을 한다. 하지만, 해당 값들은 View 가 초기화 되었을 때만 읽을 수 있다. 좀 더 다이나믹한 동작을 위해 외부 클래스에서 View 의 속성값을 접근하거나 수정하고 싶다면, View 클래스의 속성 값에 대한 getter 와 setter 메소드를 사용하면 된다. 예를 들어 다음 코드와 같다.

// PieChart.kt

class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private var showText: Boolean = false
    private var textPosition: Int = -1

    init {
        context.theme.obtainStyledAttributes(attrs, R.styleable.PieChart, 0, 0).apply {
            try {
                showText = getBoolean(R.styleable.PieChart_showText, false)
                textPosition = getInteger(R.styleable.PieChart_labelPosition, 0)
            } finally {
                recycle()
            }
        }
    }
    
    fun isShowText(): Boolean = showText
    
    fun setShowText(showText: Boolean) {
        this.showText = showText
        invalidate()
        requestLayout()
    }
}

 아래에 "isShowText()" 라는 이름의 getter 와 "setShowText()" 라는 이름의 setter 를 PieChart 클래스 안에 정의한다. 특히, setter 같은 경우에는 단순히 프로퍼티의 값을 수정하는 것 뿐만 아니라, "invalidate()" 와 "requestLayout()" 메소드를 호출해야한다. 해당 메소드들의 호출은 View 를 안정적으로 동작하는데에 중요한 역할을 한다. View 의 외형에 영향을 주는 property 에 대한 변경이 이루어진 후에는 View 를 invalidate 해줘야한다. 그래야 시스템이 View 를 다시 그려야할 지 알 수 있기 때문이다. 즉, View 의 형태나 크기에 영향을 주는 property 값의 변화가 이루어지면, 새로운 레이아웃을 바로 요청해야한다는 것이다. 위 두 메소드의 호출을 잊어버리게 된다면, 찾기도 힘든 버그가 발생한다.

 또한, 커스텀 뷰는 중요한 이벤트를 연동할 수 있는 이벤트 리스너를 지원해야한다. 예를 들어, "PieChart" 는 사용자가 PieChart 를 회전하여 새로운 파이 부분에 포커스 되었음을 리스너에게 알려주는 "OnCurrentItemChanged()" 라는 이름의 커스텀 이벤트를 노출시킨다.

 이처럼 property 와 event 를 노출시키는 것은 View 의 사용성에 직결되는 중요한 요소이다. 그와 동시에 두 요소를 외부에서 사용할 수 있도록 노출시키는 것을 잊기도 쉽다. 따라서, 시간이 조금 걸리더라도 신중하게 View 의 인터페이스를 정의하는 것이 추후에 있을 유지 비용을 줄이는 데에 도움이 된다


6. Design For Accessibility

 이제 넓은 범위로 커스텀뷰를 사용하게 되면, 많은 사람들이 사용하게 된다. 그 중에서 터치스크린을 보거나 사용하지 못하는 어려움을 가진 사용자도 있을 수 있다. 그런 사용자들을 위해 조치를 취할만한 것이 아래와 같이 있다.

  • "android:contentDescription" 속성을 사용하여 입력 필드를 라벨링 한다.
  • 적절한 때에 "sendAccessibilityEvent()" 메소드를 호출하여 접근성 이벤트를 전송한다.
  • D-pad 나 Trackball 같은 대체 컨트롤러를 지원한다.

 접근성에 대한 더 자세한 내용은 링크의 문서에서 확인할 수 있다.

반응형