버튼 수집상

[안드로이드] 커스텀 뷰 Custom View 만드는 법 본문

TIL - 안드로이드

[안드로이드] 커스텀 뷰 Custom View 만드는 법

cocokaribou 2023. 8. 11. 13:12

배경

Compose를 아직 도입하지 않은 xml 베이스의 프로젝트에서도 코드로 뷰를 생성해서 쓸 때가 있다.

그럴 때 커스텀뷰를 만들면 반복되는 코드를 은닉하면서 코드 가독성이 좋아진다는 장점이 있다.

커스텀뷰를 만들면서 겪었던 시행착오들과 기억해야할 사항들을 정리해보겠다.

 

뷰 생성자를 오버라이딩 해준다.

class CustomDropDown @JvmOverloads constructor(
    mContext: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(mContext, attrs, defStyle) {
    //...
}

@JvmOverloads 어노테이션을 붙이면 mContext, attrs, defStyle로 만들 수 있는 생성자들을 자동으로 만들어준다.

이 때 클래스 인자에 디폴트 값을 넣어줘도 반영되지 않는다.

class CustomDropDown @JvmOverloads constructor(
    mContext: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = R.style.CustomDropDown // ❌ 반영 안 됨!
) : LinearLayout(mContext, attrs, defStyle) {
    //...
}

style을 적용시키는 두 가지 방법이 있다.

1. xml 사용처에서 선언

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <com.example.app.views.CustomDropDown
        android:id="@+id/dropDown"
        style="@style/CustomDropDown"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
        
    <!-- 아래로 생략 -->

보통 style의 이름은 뷰와 같은 이름으로 지정한다.

 

2. 코드로 생성할 때 ContextThemeWrapper 인자로 넘겨줌

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        val dropDown = CustomDropDown(ContextThemeWrapper(this, R.style.CustomDropDown)).apply {
            id = View.generateViewId() // ConstraintLayout에 add할 때 필요
        }
        binding.root.addView(dropDown)
        
        // ... 생략
    }
}

ContextThemeWrapper는 스타일을 적용시키면서 뷰에 Context로 전달된다.

 

뷰 UI를 초기화할 때 두 가지 방법이 있다.

1. 코드로 바로 생성

CustomDropDown.kt

// 예시 데이터
val itemList = listOf("item1", "item2", "item3", "item4")

class CustomDropDown @JvmOverloads constructor(
    val mContext: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(mContext, attrs, defStyle) {

    // UI 초기화 로직
    init {
        itemList.forEachIndexed { index, name ->
            // 0번째 텍스트만 스타일 다르게 적용
            val style = if (index == 0) R.style.ItemSelected else R.style.ItemDefault

            addView(TextView(ContextThemeWrapper(mContext, style)).apply {
                text = name
                if (index != 0) post { updateLayoutParams<MarginLayoutParams> { topMargin = 24.toPx } }
            })
        }
    }
}

위 코드의 TextView에 적용된 style

<style name="ItemText" parent="Widget.AppCompat.TextView">
    <item name="android:textSize">16dp</item>
</style>

<style name="ItemSelected" parent="ItemText">
    <item name="android:textStyle">bold</item>
    <item name="android:textColor">#000000</item>
</style>

<style name="ItemDefault" parent="ItemText">
    <item name="android:textColor">#929292</item>
</style>

 

2. xml로 생성한 뷰를 바인딩

bottom_bar_button_view.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="56dp"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <ImageView
        android:id="@+id/iconImg"
        android:layout_width="wrap_content"
        android:layout_height="24dp"
        android:layout_marginTop="9dp"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="@id/title"
        app:layout_constraintStart_toStartOf="@id/title"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/iconImg"
        android:layout_marginTop="2dp"
        android:ellipsize="none"
        android:maxLines="1"
        android:textColor="#929292"
        android:textSize="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iconImg"
        tools:text="Category"/>

</merge>

BottomBarButtonView.kt

class BottomBarButtonView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
    private var binding: BottomBarButtonViewBinding

    init {
        val view = inflate(context, R.layout.bottom_bar_button_view, this)
        binding = BottomBarButtonViewBinding.bind(view)
    }
    // ...
}

 

728x90