안드로이드 시스템은
XML에 정의된 View 태그와 속성을 기반으로 생성된
View 객체와 AttributeSet 객체를 이용해 화면에 View 를 그려준다.
좀더 풀어 쓰면,
레이아웃 XML에 정의된 모든 View 들은
화면에 출력 될 시점에 안드로이드에 의해 View 객체로 변환되어 메모리에 올라가며,
이 때, 각 뷰의 내부 속성(색상, 크기 등)은 AttributeSet 객체로 변환되어 View 클래스 생성자의 매개변수로 전달 된다.
이렇게 생성된 View객체를 안드로이드가 해석하여 화면에 그려준다.
참고로 XML 에 정의된 View 를 객체로 생성해 메모리에 올리는 과정을 Inflate 라고 한다.
해당 뷰 하단의 버튼들을 보면 모두 같은 레이아웃에 아이콘과 텍스트만 다르다는 것을 볼 수 있다. 이럴 때는 커스텀뷰를 사용해 뷰를 정의하면 버튼마다 레이아웃과 뷰를 만들어줄 필요 없이 커스텀 뷰 하나만 만들어주면 되기 때문에 상당히 편리하다.
커스텀뷰를 만드는 것은 세 단계를 거친다.
- 뷰 구현
- 속성 정의
- 뷰 클래스 구현
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) : LinearLayout(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)
try {
// Use the TypedArray object
} finally {
// Recycle the TypedArray object
typedArray.recycle()
}
//attrs.xml 에 속성을 가져오기 위해서는 (이름)_(속성이름)
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>
그리고 뷰에서 버튼과 속성을 입력해주면
'Android > 커스텀 뷰' 카테고리의 다른 글
디자인 시스템의 텍스트 크기 문제 해결 경험 (0) | 2024.03.23 |
---|---|
CustomBottomSheet (0) | 2023.10.14 |
하이라이트 튜토리얼 커스텀뷰를 만들어보자 (0) | 2023.05.20 |
커스텀 뷰에 Depth를 줄여보자 (0) | 2023.04.13 |
둥근 모서리를 가진 그라데이션 테두리 사각형 만들기 (0) | 2023.03.29 |