Foggy day

Android Custom View: Extending The Views 본문

Android

Android Custom View: Extending The Views

jinhan38 2020. 12. 27. 20:57

 

아래 링크의 글을 번역한 글입니다.

 

vladsonkin.com/android-custom-view-extending-the-views/

 

Android Custom View: Extending The Views - vladsonkin.com

Android has lots of different Views, and you can build almost any app with them. However, sometimes they will not fulfill your requirements, mostly if your design is filled with unusual elements and animations. To handle this case, Android gives us the pos

vladsonkin.com

 

 

 

Android Custom View: Extending The Views - vladsonkin.com

Android has lots of different Views, and you can build almost any app with them. However, sometimes they will not fulfill your requirements, mostly if your design is filled with unusual elements and animations. To handle this case, Android gives us the pos

vladsonkin.com

대부분 안드로이드에 있는 다양한 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)
  }
}

 

결과 이미지입니다.