๋ฉด์ ๊ธฐ๋ก์ฉ Custom View ์ ๋ฆฌ
์ปค์คํ ๋ทฐ ์ฌ์ฉํ๋ ๋ชฉ์
- ์์ ๋ง์ถค ๋ ๋๋ง ๋ ๋ทฐ ์ ํ์ ๋ง๋ค ์ ์์
- ๊ธฐ์กด view ๊ตฌ์ฑ์์ ๊ทธ๋ฃน์ ์๋ก์ด ๋จ์ผ ๊ตฌ์ฑ์์๋ก ๋ง๋ค ์ ์์
- ํค ๋๋ฆ๊ณผ ๊ฐ์ ๋ค๋ฅธ ์ด๋ฒคํธ๋ฅผ ํ์ธํ๊ณ ๋ง์ถค ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ ์ ์์
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
๊ตฌํ ๋ฐฉ๋ฒ
attrs.xml ์ ์
: ์ปค์คํ ๋ทฐ์์ ์ฌ์ฉํ๊ณ ์ ํ๋ ์์ฑ์ด ์๋ค๋ฉด attrs.xml์ ์ ์ํด์ค
- res > values ํด๋์ attrs.xml ์ ์์ฑ
- declare-styleable ํ๊ทธ๋ฅผ ์์ฑ ํ name์๋ ์ปค์คํ ๋ทฐ ํด๋์ค๋ช ์ ์ ๋ ฅ
- 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>
์ปค์คํ ๋ทฐ ํด๋์ค ์์ฑ
- View ํด๋์ค๋ฅผ ์์ ๋ฐ๋ ํด๋์ค๋ฅผ ์์ฑ, ์ด๋์ ํด๋์ค๋ช ์ ์ attrs.xml ํ์ผ์์ ์ ์ํ ์ปค์คํ ๋ทฐ์ ํด๋์ค๋ช ๊ณผ ๋์ผํด์ผ ๋จ
- ์์ฑ์ ์ ์
// 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 ๋ฉ์๋
- onFinishInfalte() ๋ทฐ ๋ฐ ๋ทฐ์ ๋ชจ๋ ํ์๊ฐ xml์์ ํ์ฅ๋์์ ๋ ํธ์ถ
- onMeasure(Int, Int) ์ด ๋ทฐ ๋ฐ ๋ชจ๋ ํ์ ํฌ๊ธฐ ์๊ตฌ์ฌํญ์ ๊ฒฐ์ ํ ๋ ํธ์ถ
- onLayout(boolean, int, int, int, int) ๋ทฐ๊ฐ ๋ชจ๋ ํ์์ ํฌ๊ธฐ์ ์์น๋ฅผ ํ ๋นํด์ผ ํ ๋ ํธ์ถ
- onSizeChanged(int, int, int, int) ๋ทฐ์ ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋์์ ๋ ํธ์ถ
- onDraw(canvas) ๋ทฐ๊ฐ ์ฝํ
์ธ ๋ฅผ ๋ ๋๋งํ ๋ ํธ์ถ
- ์ต์ ํ ์ ์ฃผ์ํด์ ๋ณผ ๋ฉ์๋. onDraw์์ ํ ๋น์ ํ๊ฒ ๋๋ฉด ์๋จ. ๋ํ invalidate() ํจ์๊ฐ onDraw()๋ฅผ ์คํ์ํค๊ธฐ ๋๋ฌธ์ invalidate()์ ๋ถํ์ํ ํธ์ถ๋ ์์ด์๋ ์ ๋จ.
- onKeyDown(int, KeyEvent), onKeyUp(int, KeyEvent) ํค ์ด๋ฒคํธ
- … ๋ฑ๋ฑ
ํธ์ถ ์์
์์ฑ์ → Measure → Layout → Draw → Visible to User
- ์์ฑ์
- View(Context context) : ์ฝ๋์์ View๋ฅผ ๋์ ์ผ๋ก ๋ง๋ค ๋ ์ฌ์ฉํ๋ ์์ฑ์
- View(Context context, @Nullable AttributeSet attrs) : xml์์ View๋ฅผ inflationํ ๋ ํธ์ถ๋๋ ์์ฑ์
- View(Context context, @Nullable AttributeSet attrs, int defStyleAttr): xml์์ View๋ฅผ inflationํ๊ณ ํ ๋ง ์์ฑ์์ ํด๋์ค ๋ณ ๊ธฐ๋ณธ ์คํ์ผ์ ์ ์ฉ : defStyleAttr ๋งค๊ฐ ๋ณ์๋ View์ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํ๋ Style ๋ฆฌ์์ค์ ๋ํ ์ฐธ์กฐ๋ฅผ ํฌํจํ๋ ํ์ฌ ํ ๋ง์ ์์ฑ, ๊ธฐ๋ณธ๊ฐ์ ์ฐพ์ง ์์ผ๋ ค๋ฉด 0์ผ๋ก ์ง์
- View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes): defStyleRes๋ View์ defStyleAttr์ด 0์ด๊ฑฐ๋ ํ ๋ง๋ฅผ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ์ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํ๋ Style ๋ฆฌ์์ค ID์, ๊ธฐ๋ณธ๊ฐ์ ์ฐพ์ง ์์ผ๋ ค๋ฉด 0์ผ๋ก ์ง์
- 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)
- onLayout(): ๋ทฐ๋ฅผ ์ธก์ ํ์ฌ ํ๋ฉด์ ๋ฐฐ์นํ ํ ํธ์ถ
- 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' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] Base Activity, Fragment (0) | 2023.04.16 |
---|---|
[Android] OKHttp3๋ฅผ ์ฌ์ฉํ ํค๋ ์ถ๊ฐ, Retrofit2๋ฅผ ์ฌ์ฉํ http ํต์ (0) | 2023.04.16 |
[Android] Data Binding (0) | 2023.04.15 |
[Android] MVVM (0) | 2023.04.13 |
[Android] ์๋๋ก์ด๋ ๋ฆฌ์์ค (0) | 2023.04.07 |