header-img
Info :

๋ฉด์ ‘๊ธฐ๋ก์šฉ Custom View ์ •๋ฆฌ

 

์ปค์Šคํ…€ ๋ทฐ ์‚ฌ์šฉํ•˜๋Š” ๋ชฉ์ 

  1. ์™„์ „ ๋งž์ถค ๋ Œ๋”๋ง ๋œ ๋ทฐ ์œ ํ˜•์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ
  2. ๊ธฐ์กด view ๊ตฌ์„ฑ์š”์†Œ ๊ทธ๋ฃน์„ ์ƒˆ๋กœ์šด ๋‹จ์ผ ๊ตฌ์„ฑ์š”์†Œ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ
  3. ํ‚ค ๋ˆ„๋ฆ„๊ณผ ๊ฐ™์€ ๋‹ค๋ฅธ ์ด๋ฒคํŠธ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋งž์ถค ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
  4. ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ

 

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

attrs.xml ์ •์˜

: ์ปค์Šคํ…€ ๋ทฐ์—์„œ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ์†์„ฑ์ด ์žˆ๋‹ค๋ฉด attrs.xml์— ์ •์˜ํ•ด์คŒ

  1. res > values ํด๋”์— attrs.xml ์„ ์ƒ์„ฑ
  2. declare-styleable ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑ ํ›„ name์—๋Š” ์ปค์Šคํ…€๋ทฐ ํด๋ž˜์Šค๋ช…์„ ์ž…๋ ฅ
  3. attr ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•ด์„œ name์—๋Š” ์†์„ฑ๋ช…, format์—๋Š” ์†์„ฑ์˜ type์„ ์ž…๋ ฅํ•ด์คŒ
// ์˜ˆ์‹œ

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CustomViewProgress">
        <attr name="duration" format="integer"/>
        <attr name="textColor" format="color"/>
        <attr name="backgroundColor" format="color"/>
        <attr name="foregroundColor" format="color"/>
    </declare-styleable>

</resources>

 

์ปค์Šคํ…€ ๋ทฐ ํด๋ž˜์Šค ์ƒ์„ฑ

  1. View ํด๋ž˜์Šค๋ฅผ ์ƒ์† ๋ฐ›๋Š” ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑ, ์ด๋•Œ์˜ ํด๋ž˜์Šค๋ช…์€ ์œ„ attrs.xml ํŒŒ์ผ์—์„œ ์ •์˜ํ•œ ์ปค์Šคํ…€๋ทฐ์˜ ํด๋ž˜์Šค๋ช…๊ณผ ๋™์ผํ•ด์•ผ ๋จ
  2. ์ƒ์„ฑ์ž ์ •์˜
// xml ํŒŒ์ผ์ด ์•„๋‹Œ ์ฝ”๋“œ์ƒ์—์„œ ์ง์ ‘ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ํ˜ธ์ถœํ•˜๋Š” ์ƒ์„ฑ์ž์ž„
constructor(context: Context?) : super(context)

// ๋ ˆ์ด์•„์›ƒ xml์— ๋“ฑ๋กํ•œ View๊ฐ€ ์•ˆ๋“œ๋กœ์ด๋“œ์— ์˜ํ•ด Inflate ๋  ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ์ƒ์„ฑ์ž
// AttributeSet ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ปค์Šคํ…€ ์†์„ฑ์„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
constructor(context: Context?, attribute: AttributeSet?) : super(context, attribute)

 

attrs ์†์„ฑ ์‚ฌ์šฉ

: View๊ฐ€ inflate ๋  ๋•Œ xml์—์„œ ํŒŒ์‹ฑ๋œ ์†์„ฑ๋“ค์€ AttributeSet์œผ๋กœ ๋ฌถ์ž„. → ์ด๋ฅผ TypedArray๋ฅผ ์ด์šฉํ•ด์„œ AttributeSet์—์„œ styleable ๋ฆฌ์†Œ์Šค์— ์ •์˜๋œ ๊ฒƒ๋“ค์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Œ.

// ์˜ˆ์‹œ

private fun initAttribute(attributeSet: AttributeSet ? ) {
    if (attributeSet == null) return
    val attrs = context.theme.obtainStyledAttributes(
        attributeSet,
        R.styleable.CustomViewProgress,
        0,
        0
    )
    try {
        fgColor = attrs.getColor(
            R.styleable.CustomViewProgress_foregroundColor,
            fgColor
        )
        fgPaint.color = fgColor
        bgColor = attrs.getColor(
            R.styleable.CustomViewProgress_backgroundColor,
            bgColor
        )
        bgPaint.color = bgColor
        textColor = attrs.getColor(
            R.styleable.CustomViewProgress_textColor,
            textColor
        )
        textPaint.color = textColor
        remainingSecond = attrs.getInt(
            R.styleable.CustomViewProgress_duration,
            remainingSecond
        )
    } finally {
        attrs.recycle()
    }
    countDownProgress(remainingSecond.toLong())
}

๋”ฐ๋ผ์„œ ๋‚˜๋Š” ์œ„ ์ฝ”๋“œ์ฒ˜๋Ÿผ val attrs = context.theme.obtainStyledAttributes(attributeSet, R.styleable.์ปค์Šคํ…€๋ทฐํด๋ž˜์Šค๋ช…, 0, 0) ์œผ๋กœ ์ •์˜ํ•˜๊ณ  ์ด๋ฅผ ์ด์šฉํ•ด ๋ถˆ๋Ÿฌ์™”์Œ

 

onDraw() override

onDraw() ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•ด onDraw() ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ draw ๊ฐ์ฒด๋ฅผ ์ •์˜ํ•˜์—ฌ ๊ทธ๋ฆผ. ๋‹จ, draw๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉํ•  paint๋ฅผ ๋จผ์ € ์„ ์–ธํ•ด์ค˜์•ผ ํ•จ.

//Paint
val fgPaint = Paint().apply {
        flags = Paint.ANTI_ALIAS_FLAG
        style = Paint.Style.STROKE
        strokeWidth = mStrokeWidth
        color = fgColor
}

//ํ”„๋กœ๊ทธ๋ ˆ์Šค ์ง„ํ–‰์ด ๋˜๋Š” ๋ถ€๋ถ„ ๊ทธ๋ฆฌ๊ธฐ
canvas?.drawArc(
        viewBound.left + mStrokeWidth,
        viewBound.top + mStrokeWidth,
        viewBound.right - mStrokeWidth,
        viewBound.bottom - mStrokeWidth,
        -90f,
        progress,
        false,
        fgPaint
)

 

์‚ฌ์šฉ override ๋ฉ”์†Œ๋“œ

  1. onFinishInfalte() ๋ทฐ ๋ฐ ๋ทฐ์˜ ๋ชจ๋“  ํ•˜์œ„๊ฐ€ xml์—์„œ ํ™•์žฅ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ
  2. onMeasure(Int, Int) ์ด ๋ทฐ ๋ฐ ๋ชจ๋“  ํ•˜์œ„ ํฌ๊ธฐ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ฒฐ์ •ํ•  ๋•Œ ํ˜ธ์ถœ
  3. onLayout(boolean, int, int, int, int) ๋ทฐ๊ฐ€ ๋ชจ๋“  ํ•˜์œ„์— ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ํ• ๋‹นํ•ด์•ผ ํ•  ๋•Œ ํ˜ธ์ถœ
  4. onSizeChanged(int, int, int, int) ๋ทฐ์˜ ํฌ๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ
  5. onDraw(canvas) ๋ทฐ๊ฐ€ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•  ๋•Œ ํ˜ธ์ถœ
    1. ์ตœ์ ํ™” ์‹œ ์ฃผ์˜ํ•ด์„œ ๋ณผ ๋ฉ”์†Œ๋“œ. onDraw์—์„œ ํ• ๋‹น์„ ํ•˜๊ฒŒ ๋˜๋ฉด ์•ˆ๋จ. ๋˜ํ•œ invalidate() ํ•จ์ˆ˜๊ฐ€ onDraw()๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์— invalidate()์˜ ๋ถˆํ•„์š”ํ•œ ํ˜ธ์ถœ๋„ ์žˆ์–ด์„œ๋Š” ์•ˆ ๋จ.
  6. onKeyDown(int, KeyEvent), onKeyUp(int, KeyEvent) ํ‚ค ์ด๋ฒคํŠธ
  7. … ๋“ฑ๋“ฑ

 

ํ˜ธ์ถœ ์ˆœ์„œ

์ƒ์„ฑ์ž → Measure → Layout → Draw → Visible to User

  1. ์ƒ์„ฑ์ž
    1. View(Context context) : ์ฝ”๋“œ์—์„œ View๋ฅผ ๋™์ ์œผ๋กœ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์„ฑ์ž
    2. View(Context context, @Nullable AttributeSet attrs) : xml์—์„œ View๋ฅผ inflationํ•  ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ์ƒ์„ฑ์ž
    3. View(Context context, @Nullable AttributeSet attrs, int defStyleAttr): xml์—์„œ View๋ฅผ inflationํ•˜๊ณ  ํ…Œ๋งˆ ์†์„ฑ์—์„œ ํด๋ž˜์Šค ๋ณ„ ๊ธฐ๋ณธ ์Šคํƒ€์ผ์„ ์ ์šฉ : defStyleAttr ๋งค๊ฐœ ๋ณ€์ˆ˜๋Š” View์˜ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋Š” Style ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ํฌํ•จํ•˜๋Š” ํ˜„์žฌ ํ…Œ๋งˆ์˜ ์†์„ฑ, ๊ธฐ๋ณธ๊ฐ’์„ ์ฐพ์ง€ ์•Š์œผ๋ ค๋ฉด 0์œผ๋กœ ์ง€์ •
    4. View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes): defStyleRes๋Š” View์˜ defStyleAttr์ด 0์ด๊ฑฐ๋‚˜ ํ…Œ๋งˆ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ์— ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋Š” Style ๋ฆฌ์†Œ์Šค ID์ž„, ๊ธฐ๋ณธ๊ฐ’์„ ์ฐพ์ง€ ์•Š์œผ๋ ค๋ฉด 0์œผ๋กœ ์ง€์ •
  2. onMeasure(int widthMeasureSpec, int heightMeasureSpec) : ๋ทฐ์˜ ํฌ๊ธฐ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ๋จ. onMeasure ์ž์ฒด๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ณ  setMeasureDimension()์„ ํ˜ธ์ถœํ•ด ๋ทฐ์˜ ๋„ˆ๋น„์™€ ๋†’์ด๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•จ : ๋งค๊ฐœ๋ณ€์ˆ˜์ธ width/heightMeasureSpec์€ ์ฝœ๋ฐฑ์œผ๋กœ ๋“ค์–ด์˜ค๋ฉฐ ์ด ๊ฐ’์„ ํ†ตํ•ด xml์—์„œ ์„ค์ •ํ•œ width, height์˜ ๋ชจ๋“œ์™€ ํฌ๊ธฐ๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Œ : ๋ชจ๋“œ MeasureSpec.getMode(measureSpec) → EXACTLY(match_parent, fill_parent, ํ˜น์€ ์‹ค์ œ ๊ฐ’์„ ์ž…๋ ฅํ•ด์ค€ ๊ฒฝ์šฐ), AT_MOST(wrap_content๋กœ ์„ค์ •ํ•œ ๊ฒฝ์šฐ), UNSPECIFIED(๋”ฐ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ): ํฌ๊ธฐ MeasureSpec.getSize(measureSpec)
  3. onLayout(): ๋ทฐ๋ฅผ ์ธก์ •ํ•˜์—ฌ ํ™”๋ฉด์— ๋ฐฐ์น˜ํ•œ ํ›„ ํ˜ธ์ถœ
  4. onDraw(): ๋ทฐ๊ฐ€ ๊ทธ๋ฆฌ๋Š” ๋‹จ๊ณ„

 

Greme์—์„œ ์‚ฌ์šฉํ•œ ๋ถ€๋ถ„

// attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    // ...

    <declare-styleable name="ChallengeButton">
        <attr name="menu_icon" format="reference"/>
        <attr name="menu_description" format="string"/>
    </declare-styleable>

</resources>
// button_challenge.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/menu"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:orientation="vertical"
        android:gravity="center">

        <ImageView
            android:id="@+id/icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:src="@drawable/ic_challenge_basic"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="์ฑŒ๋ฆฐ์ง€"
            android:textSize="@dimen/text10"
            android:layout_marginTop="@dimen/margin12"
            app:layout_constraintEnd_toEndOf="@+id/icon"
            app:layout_constraintStart_toStartOf="@+id/icon"
            app:layout_constraintTop_toBottomOf="@+id/icon" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
// ChallengeButton.xml

class ChallengeButton : ConstraintLayout {

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init(context, attrs)
    }

    private val binding : ButtonChallengeBinding = ButtonChallengeBinding.inflate(
        LayoutInflater.from(context), this, true
    )

    lateinit var listener: ChallengeMenuButtonClickInterface

    private fun init(context: Context, attrs: AttributeSet){
        binding
        setAttrs(context, attrs)
        setClickListener()

    }

    private fun setAttrs(context: Context, attrs: AttributeSet) {
        try {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ChallengeButton)
            val icon = typedArray.getResourceId(R.styleable.InterestButton_interest_icon, R.drawable.ic_challenge_main)
            binding.icon.setImageDrawable(AppCompatResources.getDrawable(context, icon))
            binding.description.text = typedArray.getString(R.styleable.InterestButton_interest_description)

            typedArray.recycle()
        } catch (exception: Exception) {
            exception.printStackTrace()
        }
    }

    private fun setClickListener() {
        binding.menu.setOnClickListener {
            if (this::listener.isInitialized) {
                listener.challengeMenuOnClick(binding.description.text.toString())
            }
        }
    }

    fun setCustomListener(listener: ChallengeMenuButtonClickInterface) {
        this.listener = listener
    }

    fun getMenuIconView(): ImageView {
        return binding.icon
    }

    fun getMenuDescView(): TextView {
        return binding.description
    }

}

this::listener.isInitialized : lateinit์œผ๋กœ ์„ ์–ธํ•œ ๊ฒƒ ํ• ๋‹น ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

// fragment_home.xml 
// custom view ์‚ฌ์šฉํ•œ ๋ถ€๋ถ„

<com.shootit.greme.ui.custom.ChallengeButton
		android:id="@+id/btnPopular"
		android:layout_columnWeight="1"
		android:layout_width="wrap_content"
		android:layout_gravity="center"
		android:layout_height="wrap_content"
		app:menu_icon="@drawable/ic_challenge_popular"
		app:menu_description="์ธ๊ธฐ ์ฑŒ๋ฆฐ์ง€"/>

 

๋ ˆํผ๋Ÿฐ์Šค

https://developer.android.com/guide/topics/ui/custom-components?hl=ko

https://www.charlezz.com/?p=29013

https://sungcheol-kim.gitbook.io/android-custom-view-programming/chapter01

๋”๋ณด๊ธฐ
CS/Android, Kotlin