카테고리 없음

[Android] Custom View를 만들어보자

Jun.LEE 2024. 4. 2. 19:36

 

위와 같이 인스타그램에서 사용하는 모양의 이미지뷰 클래스를 만들어 보려고 한다.

안드로이드에서 제공하는 뷰들을 적절히 조합해서 만들어 보자.

 

먼저 Layout에 CardView를 배치하고 layout길이의 절반에 해당하는 radius를 주면 원 형태를 만들 수 있다.

CardView에 Gradient Color의 xml을 background로 하는 layout을 배치하고 원형 CardView를 배치하고

내부에 ImageView를 scaleType: centerCrop하여 배치해 주면 완성되시겠다.

 

먼저, View를 상속받은 클래스를 만들면 총 4개의 contructor를 마주치게 된다.

View Class를 들어가 주석을 읽어보자.

Context만을 파라미터로 하는 constructor는 코드에서 뷰를 생성할때 사용하는

가장 기본적인 형태이다.

 

Context와 AttributeSet을 파라미터로 하는 constructor의 경우

XML 파일에서 뷰를 인플레이팅, 즉 메모리에 올릴때 사용된다고 한다.

 

XML 파일으로 부터 뷰가 생성되었을때 이 constructor를 사용한다고 하니

XML에 정의하면 이 생성자가 불림을 알 수 있다.

 

단, default style을 사용함으로 기본 테마를 사용해야 합니다.

 

직전의 constructor와 같지만 다른 테마를 적용할 수 있다.

xml에서 스타일을 적용하면 이 생성자를 사용하여 뷰를 생성한다.

 

4번째 생성자는 3번째 생성자에 추가적으로 스타일 리소스를 적용할 수 있다.

 

뷰는 onMeasure -> onLayout -> onDraw의 과정을 거치며 

각 과정에서 크기를 측정, 위치를 측정, 그리기의 역할을 담당하게 된다.

 

내가 만들 뷰의 경우 gravity는 항상 중앙으로 설정, nested한 구성으로 설정되어 있어 

기존 뷰들을 이용하여 간단하게 구성해 보려고 한다.

 

위에서 설계한 것과 같이 만들어보려고 한다.

FrameLayout을 통해 겹겹히 뷰들을 쌓아줄 예정이므로 해당뷰를 상속받겠다.

xml에서 입력한 attribute 중 width, height, src가 이 커스텀 뷰의 핵심 속성이므로

해당 attribute의 값을 받기 좋게 custom attribute를 정의해주자.

 

res/value/attrs.xml에 아래와 같이 구성해주자.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleImageView">
        <attr name="android:src" format="reference|color"/>
        <attr name="android:layout_width" format="dimension"/>
        <attr name="android:layout_height" format="dimension"/>
    </declare-styleable>
</resources>

 

위와 같이 구성하면 styleable을 생성해주면 아래에서 확인할 수 있듯이

attribute에서 위에서 정의한 값들을 손쉽게 가져올 수 있다.

 

아래는 코드이다.

class CircleImageView : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {

        val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView)

        val imageResourceId = a.getResourceId(
            R.styleable.CircleImageView_android_src, -1)
        val attrsWidthPx = a.getDimension(
            R.styleable.CircleImageView_android_layout_width, 0f)
        val attrsHeightPx = a.getDimension(
            R.styleable.CircleImageView_android_layout_height, 0f)

        val outerBorderSize = min(attrsWidthPx, attrsHeightPx)
        val innerBorderSize = outerBorderSize - outerBorderSize / 15
        val imageBorderSize = outerBorderSize * 5 / 6

        val outerLayoutParams = LayoutParams(
            outerBorderSize.toInt(),
            outerBorderSize.toInt()
        ).apply {
            gravity = Gravity.CENTER
        }

        val innerLayoutParams = LayoutParams(
            innerBorderSize.toInt(),
            innerBorderSize.toInt()
        ).apply {
            gravity = Gravity.CENTER
        }

        val imageLayoutParams = LayoutParams(
            imageBorderSize.toInt(),
            imageBorderSize.toInt()
        ).apply {
            gravity = Gravity.CENTER
        }

        val outerBorderCardView = CardView(context).apply {
            layoutParams = outerLayoutParams
            radius = outerBorderSize / 2 + 1f
        }

        val outerLayout = FrameLayout(context).apply {
            layoutParams = outerLayoutParams
            background = resources.getDrawable(R.drawable.gradient_color_2)
        }

        val innerBorderCardView = CardView(context).apply {
            layoutParams = innerLayoutParams
            setCardBackgroundColor(Color.WHITE)
            radius = innerBorderSize / 2 + 1f
        }

        val imageBorderCardView = CardView(context).apply {
            layoutParams = imageLayoutParams
            setCardBackgroundColor(Color.TRANSPARENT)
            radius = imageBorderSize / 2 + 1f
        }

        val imageView = ImageView(context).apply {
            layoutParams = imageLayoutParams
            if(imageResourceId != -1) setImageResource(imageResourceId)
            scaleType = ImageView.ScaleType.CENTER_CROP
        }

        imageBorderCardView.addView(imageView)
        innerBorderCardView.addView(imageBorderCardView)
        outerLayout.addView(innerBorderCardView)
        outerBorderCardView.addView(outerLayout)
        addView(outerBorderCardView)

        a.recycle()
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )
}

 

xml에서 만든 후에 별도의 스타일을 적용해 주지 않을 것이므로 2번째 생성자만 구현해 주었다.

 

잘 작동하는 것을 확인할 수 있다.