본문 바로가기

Programming/Android_Kotlin

Android Custom View 만들기

해당 뷰 하단의 버튼들을 보면 모두 같은 레이아웃에 아이콘과 텍스트만 다르다는 것을 볼 수 있다. 이럴 때는 커스텀뷰를 사용해 뷰를 정의하면 버튼마다 레이아웃과 뷰를 만들어줄 필요 없이 커스텀 뷰 하나만 만들어주면 되기 때문에 상당히 편리하다.

커스텀뷰를 만드는 것은 세 단계를 거친다.

  1. 뷰 구현
  2. 속성 정의
  3. 뷰 클래스 구현

1. 뷰 구현

우선 커스텀뷰의 xml 파일을 만들어준다. 데이터바인딩을 사용하기 위해 layout 태그로 감싸는 것도 잊지 말자.

<!-- view_emoji_button.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center">
        <TextView
            android:id="@+id/tv_emoji_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="36sp"
            android:text="❄️"/>

        <TextView
            android:id="@+id/tv_title_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="추워요"
            android:layout_marginBottom="4dp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/tv_count_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0"/>
    </LinearLayout>
</layout>

대략 원하는 형태의 뷰가 만들어졌다.

2. 속성 정의

이제 해당 뷰에서 사용하는 속성을 정의해야한다. 해당 뷰에서는 이모지, 타이틀, 카운트가 다르므로 세 개의 속성을 받을 것이다.

values 폴더 내에 attrs.xml 파일을 만들고 속성과 자료형을 입력해준다.

<!-- attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="EmojiButton">
        <attr name="emoji" format="string|reference"/>
        <attr name="title" format="string|reference"/>
        <attr name="count" format="integer"/>
    </declare-styleable>
</resources>

이 때 declare-styleable의 name은 만들려는 View의 Class Name과 일치해야한다.

emoji와 title에 사용된 format인 string|reference는 string과 reference로 받는다는 것인데, reference란 @string/emoji_snow와 같이 values를 참조하는 형태를 의미한다.

또 주의해야하는 점이 아래와 같은 경우인데

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="EmojiButton">
        <attr name="emoji" format="string|reference"/>
        <attr name="title" format="string|reference"/>
        <attr name="count" format="integer"/>
    </declare-styleable>

    <declare-styleable name="TextButton">
        <attr name="title" format="string|reference"/>
    </declare-styleable>
</resources>

커스텀뷰를 여러 개 만들 경우 속성의 이름과 포맷이 일치한다면 오류가 나버린다. 이럴 때는 포맷을 빼고

<resources>
    <declare-styleable name="EmojiButton">
        <attr name="emoji" format="string|reference"/>
        <attr name="title" format="string|reference"/>
        <attr name="count" format="integer"/>
    </declare-styleable>

    <declare-styleable name="TextButton">
        <attr name="title"/>
    </declare-styleable>
</resources>

이처럼 name만 지정해주면 위에서 사용한 것과 같은 포맷임을 인식하고 올바로 작동한다.

3. 뷰 클래스 구현

이제 마지막으로 뷰를 실제로 구현해주면 된다. 클래스에서는 init 내에서 뷰를 그려주면 된다.

아래와 같이 코드를 작성해보자

// EmojiButton.kt
class EmojiButton(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
    private var binding: ViewEmojiButtonBinding = ViewEmojiButtonBinding.inflate(LayoutInflater.from(context), this, true)

    init {
        binding
    }
}

핵심은 데이터바인딩 inflate시에 attachToRoot를 true로 주는 것이다. 이를 false로 주게 되면 커스텀뷰가 작동하지 않는다.

작성 후 build를 해보면 아까 작성한 Custom View가 정상적으로 표시되는 것을 볼 수 있다.

이제 속성을 불러와 Custom View에 넣어줘야한다.

// EmojiButton.kt
class EmojiButton(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
    private var binding: ViewEmojiButtonBinding = ViewEmojiButtonBinding.inflate(LayoutInflater.from(context), this, true)

    init {
        // attrs.xml에서 View의 속성 목록을 가져온다.
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.EmojiButton)

        val emoji = typedArray.getString(R.styleable.EmojiButton_emoji)
        binding.tvEmojiButton.text = emoji

        val title = typedArray.getString(R.styleable.EmojiButton_title)
        binding.tvTitleButton.text = title

        val count = typedArray.getInt(R.styleable.EmojiButton_count, 0)
        binding.tvCountButton.text = count.toString()

        // 데이터를 캐싱해두어 가비지컬렉션에서 제외시키도록 하는 함수
        // typedArray 사용 후 호출해야하나, 커스텀뷰가 반복 사용되는 것이 아니라면 호출하지 않아도 무방하다.
        typedArray.recycle()
    }
}

만약 이미지를 받는 속성이라면 아래와 같이 불러올 수 있다.

val iconImageResource = typedArray.getResourceId(R.styleable.IconButton_icon, R.drawable.ic_drop)
binding.ivIcon.setImageResource(iconImageResource)

이제 뷰와 연결시키자. 뷰에서는 클래스 이름으로 다른 뷰를 사용하는 것과 같이 속성을 지정해주면 된다.

커스텀 뷰의 수정사항은 빌드를 해야 반영되니 우선 빌드를 해준다.

<!-- activity_main.xml -->
<LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="14dp"
            app:layout_constraintEnd_toEndOf="@+id/cardView"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="@+id/cardView"
            app:layout_constraintTop_toBottomOf="@+id/cardView">
            <com.nalc.android.nalc.view.custom.EmojiButton
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                app:emoji="❄️"
                app:title="추워요"
                app:count="0" />

            <com.nalc.android.nalc.view.custom.EmojiButton
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                app:emoji="🌬"
                app:title="쌀쌀해요"
                app:count="0"/>

            <com.nalc.android.nalc.view.custom.EmojiButton
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                app:emoji="😀"
                app:title="적당해요"
                app:count="0"/>

            <com.nalc.android.nalc.view.custom.EmojiButton
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                app:emoji="☀️"
                app:title="따뜻해요"
                app:count="0"/>

            <com.nalc.android.nalc.view.custom.EmojiButton
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                app:emoji="🔥"
                app:title="더워요"
                app:count="0"/>
        </LinearLayout>

그리고 뷰에서 버튼과 속성을 입력해주면

프리뷰에서도 잘 나타나는 모습을 볼 수 있다.