[Android] Custom View

핑구·2023년 5월 28일
0

Android

목록 보기
3/8
post-thumbnail

서론

이번 장바구니 미션을 하며 반복되는 뷰(상품 수량 선택)를 재활용할 수 있는 방법을 고민해본다.라는 요구사항에 따라 커스텀 뷰를 접하게 되었다.
미션을 할 당시에는 다른 기능 요구사항에 허덕이느라 다른 크루의 코드를 거의 가져다 쓰는 수준으로 작성하게 되었는데,

LinearLayout의 생성자는 몇 가지 종류가 있을까요? @JvmOverloads는 어떤 역할일까요?

하는 피드백이 들어오게 되어 이 기회에 Custom View에 대해 좀 더 알아보기로 했다.

본론

커스텀 뷰를 왜 쓰냐

위에서 말한 것처럼 반복되는 뷰를 재활용하기 위해 사용한다.
매번 복붙복붙하며 뷰를 만들기도 힘들고, 만들었다고 해도 (너비, padding 등)수정이 생긴다면 모든 뷰를 일일히 수정해주어야해서 커스텀 뷰로 관리하는 것이 훨씬 편하다.

내가 만든 커스텀 뷰

[layout_count_view.xml]

<?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>


위와 같이 플러스 버튼을 누르면 가운데 값이 커지고 마이너스 버튼을 누르면 값이 작아지는 간단한 뷰를 만들어 보았다.

[attrs.xml]

<?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을 통해 추가하고 스타일을 지정할 수도 있는 속성)

[activity_main]

<?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)을 정해주었다.

[CountView]

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()에서 각 값에 따라 화면을 구성해주고 있다.

@JvmOverloads

위의 코드를 보면 생성자 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) {...}

결론

아직 풀리지 않은 의문

  • 직접 실험해 본 결과, 두번째 생성자까지만 호출되고 세번째 생성자는 호출되지 않는데 왜 다른 블로그들은 세번째 생성자까지 오버로딩하고 있는 걸까?
  • 각 생성자는 내부적으로 자신의 다음번째 생성자를 부른다.
    두번째 생성자는 세 번째 생성자를 부르고 세 번째 생성자는 네 번째 생성자를 부른다. 그렇다면 4개의 생성자 모두 오버로딩해야하는 것 아닌가? 왜 두개만 해도 괜찮은걸까?

출처
링크1
링크2
링크3
링크4

profile
발전중

0개의 댓글