본문 바로가기

안드로이드/View

[View] Progress Bullet Point

이전에 구현한 Bullet Point를 바탕으로 잔행 상태를 나타내는 Progress Bullet Point를 구현해보자.

https://snaildeveloper.tistory.com/145

 

[View] 글 머리 (Bullet Point) 만들기

TODO 앱 개인 프로젝트를 리팩토링하면서 나의 일정 목록을 좀 더 직관적으로 보여주고 싶었다.그래서 나만의 Bullet Point를 만들어 사용하기로 하였다. Bullet Point는 Canvas의 Circle과 Line을 사용하여

snaildeveloper.tistory.com

 

진행한 부분은 실선으로, 아직 미진행 부분은 점선 및 회색으로 표시하여 상태를 아래와 같이 나타낼 수 있다.

 

상태

해당 프로젝트에서의 상태는 4가지가 있다.

  1. 성공 - 초록색
  2. 실패 - 빨간색
  3. 진행 중 - 파란색
  4. 대기 - 회색

진행이 완료된 작업에 대해서는 초록색 실선을, 진행되지 않은 부분은 회색 실선을 적용한다.

 

Progress 상태를 나타내기 위해 고려해야할 부분은 다음과 같다.

  • View
    • View에서는 현재 Progress 상태 4가지 중 하나만 알면 된다.
    • 그 외의 어떻게 상태를 적용하는지는 View는 신경쓰지 않아도 됨
  • 상태 적용
    • 성공, 실패 유무는 true, false로 쉽게 적용할 수 있다.
    • 진행 중인 상태와 대기 상태는 어떻게 구분해야 할까?

 

상태 구분

프로젝트에서는 성공 실패 유무를 Boolean Type으로 구분하고 있었는데 현 상태로는 상태를 정확하게 구분하는 것이 불가능했다.

따라서 대기 중인 상태를 나타내기 위해 Nullable Type으로 변경하여 null 값을 대기 상태로 구분하였다.

 

그러나 진행 중인 상태 또한 Null 값이기 때문에 진행과 대기에 대한 구분이 추가적으로 필요했는데, RecyclerView의 경우 List를 Adapter에 전달하여 구현하는 점을 이용하여

  • 현재 Position의 상태가 Null이고, 이전 Position의 상태가 Null이 아니라면 - 진행 상태
  • 현재 Position의 상태가 Null이고, 이전 Position의 상태 또한 Null이라면 - 대기 상태

위와 같이 구분하기로 했다.


View 구현

ProgressBulletPoint View는 기존에 만들었던 BulletPoint View를 상속받아 구현된다.

class ProgressBulletPoint(context: Context, attrs: AttributeSet) : BulletPoint(context, attrs) {

}

 

추가해야 할 부분은 Progress의 상태와 상태에 따른 Circle과 Line의 모양 및 색상 변경이다.

 

Progress State

private var state: ProgressState? = null

enum class ProgressState {
    Fail, Success, Wait, Do
}

fun setProgressState(state: ProgressState) {
    this.state = state
    invalidate()
}

 

Bullet Interface Override

BulletPoint View에 선언 한 Bullet 인터페이스를 오버라이드하여 Porgress State에 따른 Circle과 Line을 그려준다.

override fun drawLineUp(canvas: Canvas) {
    when (state) {
        ProgressState.Do, ProgressState.Fail, ProgressState.Success -> {
            settingDrawLine(color = doneLineColor)
        }

        ProgressState.Wait -> {
            settingDrawLine(color = waitColor)
            paint.pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
        }

        else -> Unit
    }

    val x = width / 2F
    val y1 = height / 2F - circleSize
    val y2 = 0F

    canvas.drawLine(x, y1, x, y2, paint)
}

Circle 기준 Top Line

  • 대기 상태는 회색 점선
  • 대기 상태를 제외한 나머지 상태는 초록색

 

override fun drawLineDown(canvas: Canvas) {
    when (state) {
        ProgressState.Success, ProgressState.Fail -> {
            settingDrawLine(color = doneLineColor)
        }

        ProgressState.Do, ProgressState.Wait -> {
            settingDrawLine(color = waitColor)
            paint.pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
        }

        else -> Unit
    }

    val x = width / 2F
    val y1 = height / 2F + circleSize
    val y2 = height.toFloat()

    canvas.drawLine(x, y1, x, y2, paint)
}

Circle 기준 Bottom Line

  • 대기 및 진행 상태는 회색 점선
  • 나머지 상태는 초록색

 

override fun drawCircle(canvas: Canvas) {
    paint.color = when (state) {
        ProgressState.Fail -> failColor
        ProgressState.Success -> successColor
        ProgressState.Do -> doColor
        else -> waitColor
    }
    canvas.drawCircle(width / 2F, height / 2F, circleSize.toFloat(), paint)

    settingBackgroundCircle(canvas)
}

Circle

  • 실패는 빨강색
  • 성공은 초록색
  • 진행 상태는 파랑색
  • 대기 상태는 회색

View 전체 코드

class ProgressBulletPoint(context: Context, attrs: AttributeSet) : BulletPoint(context, attrs) {
    private var state: ProgressState? = null
    private var doColor: Int = Color.BLUE
    private var successColor: Int = Color.GREEN
    private var failColor: Int = Color.RED
    private var waitColor: Int = Color.GRAY
    private var doneLineColor: Int = Color.GREEN

    enum class ProgressState {
        Fail, Success, Wait, Do
    }

    fun setProgressState(state: ProgressState) {
        this.state = state
        invalidate()
    }

    override fun drawLineUp(canvas: Canvas) {
        when (state) {
            ProgressState.Do, ProgressState.Fail, ProgressState.Success -> {
                settingDrawLine(color = doneLineColor)
            }

            ProgressState.Wait -> {
                settingDrawLine(color = waitColor)
                paint.pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
            }

            else -> Unit
        }

        val x = width / 2F
        val y1 = height / 2F - circleSize
        val y2 = 0F

        canvas.drawLine(x, y1, x, y2, paint)
    }

    override fun drawLineDown(canvas: Canvas) {
        when (state) {
            ProgressState.Success, ProgressState.Fail -> {
                settingDrawLine(color = doneLineColor)
            }

            ProgressState.Do, ProgressState.Wait -> {
                settingDrawLine(color = waitColor)
                paint.pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
            }

            else -> Unit
        }

        val x = width / 2F
        val y1 = height / 2F + circleSize
        val y2 = height.toFloat()

        canvas.drawLine(x, y1, x, y2, paint)
    }

    override fun drawCircle(canvas: Canvas) {
        paint.color = when (state) {
            ProgressState.Fail -> failColor
            ProgressState.Success -> successColor
            ProgressState.Do -> doColor
            else -> waitColor
        }
        canvas.drawCircle(width / 2F, height / 2F, circleSize.toFloat(), paint)

        settingBackgroundCircle(canvas)
    }
}

 

주의할 점

ProgressBulletPoint View의 경우 RecyclerView의 Item Layout에서 사용된다.

그러나 하나의 상태가 변경됨에 따라 해당 Position 외의 ViewHolder 또한 Update가 이루어져야 상태 변화가 제대로 이루어지기 때문에 ListAdapter 대신 RecyclerView.Adapter를 사용하여 전체적인 ViewHolder를 Update해줘야 한다.

'안드로이드 > View' 카테고리의 다른 글

[EditText] 날짜 입력하기 with Flow  (0) 2025.08.03
[View] 글 머리 (Bullet Point) 만들기  (0) 2025.07.29
SearchView  (0) 2024.07.14
TimePicker  (0) 2024.07.14
[Layout] CoordinatorLayout  (0) 2024.07.13