이번 장바구니 미션을 하며 반복되는 뷰(상품 수량 선택)를 재활용할 수 있는 방법을 고민해본다.
라는 요구사항에 따라 커스텀 뷰를 접하게 되었다.
미션을 할 당시에는 다른 기능 요구사항에 허덕이느라 다른 크루의 코드를 거의 가져다 쓰는 수준으로 작성하게 되었는데,
LinearLayout의 생성자는 몇 가지 종류가 있을까요?
@JvmOverloads
는 어떤 역할일까요?
하는 피드백이 들어오게 되어 이 기회에 Custom View에 대해 좀 더 알아보기로 했다.
위에서 말한 것처럼 반복되는 뷰를 재활용하기 위해 사용한다.
매번 복붙복붙하며 뷰를 만들기도 힘들고, 만들었다고 해도 (너비, padding 등)수정이 생긴다면 모든 뷰를 일일히 수정해주어야해서 커스텀 뷰로 관리하는 것이 훨씬 편하다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:padding="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_minus"
android:layout_width="70dp"
android:layout_height="50dp"
android:text="-"
android:textSize="22sp" />
<TextView
android:id="@+id/tv_count"
android:layout_width="60dp"
android:layout_height="50dp"
android:background="@color/white"
android:gravity="center"
android:textSize="22sp"
tools:text="1" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_plus"
android:layout_width="70dp"
android:layout_height="50dp"
android:text="+"
android:textSize="22sp" />
</LinearLayout>
</layout>
위와 같이 플러스 버튼을 누르면 가운데 값이 커지고 마이너스 버튼을 누르면 값이 작아지는 간단한 뷰를 만들어 보았다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CountView">
<attr name="isCountMinus" format="boolean"/>
<attr name="text" format="reference|integer" />
<attr name="textBgColor" format="reference|integer" />
<attr name="textColor" format="reference|integer" />
<attr name="minusColor" format="integer"/>
<attr name="plusColor" format="integer"/>
</declare-styleable>
</resources>
declare-styleable
을 사용하여 내가 만든 뷰의 속성들을 만들어 준다. (XML을 통해 추가하고 스타일을 지정할 수도 있는 속성)
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.mvpapplication.CountView
android:id="@+id/first_counter"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
app:isCountMinus="true"
app:layout_constraintBottom_toTopOf="@id/second_counter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:minusColor="@color/teal_200" />
<com.example.mvpapplication.CountView
android:id="@+id/second_counter"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
app:isCountMinus="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:plusColor="@color/teal_700" />
<com.example.mvpapplication.CountView
android:id="@+id/third_counter"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/second_counter"
app:minusColor="@color/btnColor"
app:plusColor="@color/btnColor"
app:text="50"
app:textBgColor="@color/black"
app:textColor="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
attrs.xml 에서 정의했던 속성들을 사용하여
음수로 내려갈 수 있는지 없는지 여부(isCountMinus
), 시작 숫자(text
), 가운데 숫자의 배경색(textBgColor
), 숫자의 글자색(textColor
), minusBtn의 배경색(minusColor
), plusBtn의 배경색(plusColor
)을 정해주었다.
package com.example.mvpapplication
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import com.example.mvpapplication.databinding.LayoutCountViewBinding
class CountView : LinearLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
getAttrs(attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
getAttrs(attrs, defStyleAttr)
}
private var binding: LayoutCountViewBinding = LayoutCountViewBinding.inflate(
LayoutInflater.from(context),
this,
true,
)
var count: Int = 1
set(value) {
field = value
binding.tvCount.text = value.toString()
}
init {
binding.btnPlus.setOnClickListener {
count++
}
}
private fun getAttrs(attrs: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CountView)
setTypeArray(typedArray)
}
private fun getAttrs(attrs: AttributeSet?, defStyle: Int) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CountView, defStyle, 0)
setTypeArray(typedArray)
}
private fun setTypeArray(typedArray: TypedArray) {
val isCountMinus = typedArray.getBoolean(R.styleable.CountView_isCountMinus, false)
binding.btnMinus.setOnClickListener {
if (!isCountMinus && count > 0) {
count--
} else if (isCountMinus) {
count--
}
}
val text = typedArray.getInt(R.styleable.CountView_text, 0)
count = text
binding.tvCount.text = text.toString()
val bgColor = typedArray.getColor(R.styleable.CountView_textBgColor, Color.WHITE)
binding.tvCount.setBackgroundColor(bgColor)
val textColor = typedArray.getColor(R.styleable.CountView_textColor, Color.BLACK)
binding.tvCount.setTextColor(textColor)
val plusColor = typedArray.getColor(R.styleable.CountView_plusColor, Color.GRAY)
binding.btnPlus.setBackgroundColor(plusColor)
val minusColor = typedArray.getColor(R.styleable.CountView_minusColor, Color.GRAY)
binding.btnMinus.setBackgroundColor(minusColor)
typedArray.recycle()
}
}
import android.R
가 되어있으면 styleable
을 인식 못한다..!XML 레이아웃에서 뷰를 만들면 XML 태그의 모든 속성을 읽어 뷰 생성자에 AttributeSet
으로 전달하게 된다. 생성자에서 obtainStyledAttributes()
를 통해 넘겨받은 AttributeSet
에서 값을 추출할 수 있다.
위의 코드에서는 getAttrs()
에서 값을 추출한 후에 setTypeArray()
에서 각 값에 따라 화면을 구성해주고 있다.
위의 코드를 보면 생성자 3개를 볼 수 있다.
왜 저렇게 주렁주렁 여러 개를 써야하는 걸까?
View의 생성자는 위와 같이 총 4가지가 있다.
첫번째 생성자는 코드 상에서 View를 생성할 때, 두번째 생성자는 xml에서 View를 inflate할 때 호출된다. 세번째 생성자의 defStyleAttr는 뷰의 기본적인 속성을 말한다.
defStyleAttr은 현재 테마에 있는 스타일 리소스에 대한 참조를 나타내는 속성입니다. 반면에, defStyleRes는 스타일 리소스의 리소스 식별자를 나타냅니다.
테마의 스타일 리소스는 애플리케이션 전체에 적용되는 일반적인 스타일을 정의하고, 특정 스타일 리소스는 특정 구성 요소에 대한 스타일을 정의하고 변경 가능성과 상속 가능성을 제공합니다. (by chatGPT)
첫번째와 두번째 생성자는 필수적으로 있어야 제대로 동작한다.
어쨌튼 이렇게 하나하나 생성자를 작성할 필요가 없게 해주는 것이 바로 @JvmOverloads
이다!
코틀린은 이 어노테이션을 통해 생성자 오버로딩을 자동으로 생성해 준다.
어노테이션은 컴파일러에게 인수를 default값으로 대체하는 생성자를 만들도록 지시한다.
class CountView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {...}