Foggy day
Android Custom View: Extending The Views 본문
아래 링크의 글을 번역한 글입니다.
vladsonkin.com/android-custom-view-extending-the-views/
대부분 안드로이드에 있는 다양한 View를 이용해 앱을 개발합니다. 하지만 특이한 디자인과 애니메이션들을 구현하기에는 부족합니다. 이런 경우를 위해 안드로이드는 커스텀 뷰로 모든 것을 만들 수 있도록 했습니다.
Custom View Android
커스텀 뷰는 뷰의 서브클래스이며, 기존의 view나 viewGroup들이 요구사항을 충족하지 모할 때 사용됩니다.
커스텀뷰를 두가지 카테고리로 나눠볼 수 있습니다.
1. 혼합된 커스텀뷰(Compound Custom View), 기존의 뷰들을 하나의 요소로 합친 뷰
2. 커스텀뷰(Custom View), 기존에 있는 뷰의 기능을 확장하거나 완전히 새로 만든 뷰
Compound View
Compound View는 다른 뷰들을 하나로 만든 뷰입니다. 장점은 다음과 같습니다.
- 재사용성. 이 컴포넌트(Compound View)를 한번 구현하면 다시 재사용할 수 있습니다.
- 커스텀 API. 커스텀 뷰에 따라 설정된 편리하게 API를 제공할 수 있습니다.
간단한 Compound View를 만들어보겠습니다.
TextView와 ImageView가 있는 xml layout을 만들어주세요
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".customView.CompoundViewActivity">
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Success" />
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_success" />
</androidx.constraintlayout.widget.ConstraintLayout>
그리고 이 커스텀 뷰를 inflate하겠습니다.
class StatusView : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var ui: ActivityCompoundViewBinding
init {
ActivityCompoundViewBinding.inflate(LayoutInflater.from(context), this, true).also {
ui = it
}
}
}
compound custom view를 만들 때 일반적으로 FrameLayout, ConstraintLayout, LinearLayout 같은 ViewGroup을 상속받습니다. 이는 onDraw(), onMeasure()같은 몇몇 기능들을 간현하게 구현하도록 도와줍니다.
Note : 여기서 XML로 레이아웃을 지정했지만 코드로 TextView나 ImageView를 만들어서 레이아웃에 추가할 수 있습니다.
이제 위에서 만든 View를 모든 화면에서 재사용할 수 있습니다. 하지만 아직 어떤 기능도 하지 않기 때문에 StatusView에 몇몇 요소들을 추가해보겠습니다.
fun setStatus(status: Status) = when (status) {
Status.SUCCESS -> {
ui.status.text = context.getString(R.string.success)
ui.icon.setImageResource(R.drawable.ic_success)
}
Status.ERROR -> {
ui.status.text = context.getString(R.string.error)
ui.icon.setImageResource(R.drawable.ic_error)
}
}
enum class Status {
SUCCESS,
ERROR
}
compound view의 또다른 장점은 유연한 API입니다. client에서 setStatus()함수를 호출하기만 하면 compound view가 알아서 작업을 수행합니다.
class CompoundViewActivity : AppCompatActivity() {
lateinit var ui: ActivityCompoundViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ui = ActivityCompoundViewBinding.inflate(layoutInflater).apply {
setContentView(root)
}
ui.statusView.setStatus(StatusView.Status.SUCCESS)
}
}
Android Custom View Example
두번째 카테고리는 단일 커스텀뷰입니다. 기존의 뷰를 상속받는다면 쉽게 만들 수 있습니다. 만약 비슷한 뷰가 이미 있다면 상속받아서 추가 및 수정 작업을 하면 됩니다. 가장 큰 이점은 더 전문화된 클래스(구체화된 클래스)로 커스텀 뷰를 만들고 이를 재사용하 수 있다는 것입니다.
아래와 같은 체크박스를 만들어보겠습니다.
AppCompatCheckBox를 상속받아서 원하는 동작들을 구현하고, 부족한 부분들을 추가해보겠습니다.
class IndeterminateCheckBox : AppCompatCheckBox {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
init { setState(State.UNCHECKED) }
fun setState(state: State) = setButtonDrawable(
when (state) {
State.UNCHECKED -> R.drawable.ic_checkbox_unchecked
State.INDETERMINATE -> R.drawable.ic_checkbox_indeterminate
State.CHECKED -> R.drawable.ic_checkbox_checked
}
)
enum class State { UNCHECKED, INDETERMINATE, CHECKED }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.jinhanexample.customView.compoundView.StatusView
android:id="@+id/statusView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.jinhanexample.customView.compoundView.IndeterminateCheckBox
android:id="@+id/indeterminate_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp" />
<com.jinhanexample.customView.compoundView.IndeterminateCheckBox
android:id="@+id/indeterminate_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="25dp" />
<com.jinhanexample.customView.compoundView.IndeterminateCheckBox
android:id="@+id/indeterminate_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp" />
</LinearLayout>
class CompoundViewActivity : AppCompatActivity() {
lateinit var ui: ActivityCompoundViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ui = ActivityCompoundViewBinding.inflate(layoutInflater).apply {
setContentView(root)
}
ui.statusView.setStatus(StatusView.Status.ERROR)
ui.indeterminate1.setState(IndeterminateCheckBox.State.INDETERMINATE)
ui.indeterminate2.setState(IndeterminateCheckBox.State.UNCHECKED)
ui.indeterminate3.setState(IndeterminateCheckBox.State.CHECKED)
}
}
아래는 메모장 앱에서 사용할 수 있는 LinedEditText 예제입니다. onDraw를 오버라이딩하여 변화를 주었습니다.
class LinedEditText(context: Context, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
private val rect: Rect = Rect()
private val paint: Paint = Paint()
init {
paint.style = Paint.Style.STROKE
paint.color = -0x7fffff01
}
override fun onDraw(canvas: Canvas) {
val count = lineCount
// Draws one line in the rectangle for every line of text in the EditText
for (i in 0 until count) {
val baseline = getLineBounds(i, rect)
canvas.drawLine(
rect.left.toFloat(),
(baseline + 1).toFloat(),
rect.right.toFloat(),
(baseline + 1).toFloat(),
paint
)
}
super.onDraw(canvas)
}
}
결과 이미지입니다.
'Android' 카테고리의 다른 글
Android(kotlin, java) - LayoutParams, Viewgroup to change margin, width, and height in code (0) | 2020.12.31 |
---|---|
Android(Java, kotlin) - Appbar Animation with ViewPager inside Fragment (0) | 2020.12.29 |
Android(kotlin) - custom circle progressBar (0) | 2020.12.27 |
Android - 최고의 커스텀 뷰 가이드 (0) | 2020.12.27 |
Android(Java) - composite design pattern example (0) | 2020.12.26 |