Skip to content

안드로이드 커스텀 뷰 ‐ 반원 슬라이더

ootr47 edited this page Jun 1, 2024 · 2 revisions

기존 슬라이더를 반원 형태로 수정해 디자인적 요소를 추가하고자 한다.

수정 전

<수정 전>

수정 후

<수정 후>


View는 measure -> layout -> draw 순서로 그려지게 된다.

자식 뷰가 없으므로 onLayout은 생략하고 onMeasure 메서드를 통해 크기를 측정하고 측정된 크기에 알맞게 draw하면 될 것 같다.

크기 측정하기

width = if (viewWidthMode == MeasureSpec.EXACTLY) {
    viewWidthSize.toFloat()
} else if (viewHeightMode == MeasureSpec.EXACTLY) { // height만 크기 값이 설정된 경우 height에 맞게 width 설정
    (viewHeightSize.toFloat() - slideBarMargin) * 2
} else { // width, height 모두 wrap_content와 같이 설정된 크기 값이 없을 경우 700으로 고정
    700F
}

height = if (viewHeightMode == MeasureSpec.EXACTLY) {
  viewHeightSize.toFloat()
} else {
  width / 2 + slideBarMargin
}

MeasureSpec Mode

  1. UNSPECIFIED : 크기가 설정되지 않아 원하는 크기를 가질 수 있음
  2. EXACTLY : 크기가 정해져 있는 상태
  3. AT_MOST : 주어진 크기내 원하는 크기를 가질 수 있음

그리기

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    drawSlideBar(canvas)
    drawSliderBarTick(canvas)
    drawController(canvas)
    drawSlideValueText(canvas)
}

앞에서 측정된 크기를 가지고 알맞은 위치에 canvas.draw.. 메서드를 통해 그려준다. onDraw는 상당히 자주 호출되는 함수이므로 해당 함수 내에서 객체 생성을 자제하자.


value값 설정하기

처음에 단순히 x좌표와 정비례하게 value를 설정했는데 슬라이더가 원형이다 보니 x좌표에 따른 value의 증가량이 일정하지 않았다.

따라서 실제로 컨트롤러가 이동한 길이로 value를 설정하고자 하였고 이를 각도를 측정해 value값을 설정했다.

private var currentRad = pi
    set(value) {
        field = value.coerceIn(0.0..pi)
        updateValueWithRadian(field)
        updateControllerPointWithRadian(field)
        invalidate()
    }

.
.
.

private fun updateRadianWithTouchPoint(x: Float, y: Float) {
    val rad = calculateRadianWithPoint(x, y)
    currentRad = findCloseStepValueRadian(degreeToValue(rad.toDegree()))
}

private fun calculateRadianWithPoint(x: Float, y: Float): Double {
    val arcTanRadian = atan((x - slideBarPointX) / (y - slideBarPointY))
    return (pi / 2) + arcTanRadian
}

private fun updateControllerPointWithRadian(rad: Double) {
    controllerPointX = slideBarPointX + cos(rad).toFloat() * slideBarRadius
    controllerPointY = slideBarPointY - sin(rad).toFloat() * slideBarRadius
}

터치 좌표(x,y)를 가지고 측정된 각도로 invalidate()를 호출해 다시 그려준다.

여기서 findCloseStepValueRadian() 라는 함수는 컨트롤러를 통해 조작하는 value 값은 정해진 step 단위로만 조정할 수 있게 radian을 가장 가까운 step단위 값으로 수정한다.

private fun findCloseStepValueRadian(currentValue: Int): Double {
    var minDifference = maxPercentValue
    var prevDifference = maxPercentValue

    for (value in 0..maxPercentValue step sliderValueStepSize) {
        val difference = abs(currentValue - value)

        if (difference > prevDifference) { // difference 증가할 경우 이전 radian 반환
            return valueToDegree(value - sliderValueStepSize).toRadian()
        }

        if (difference < minDifference) {
            minDifference = difference
        }
        prevDifference = difference
    }
    return 0.0
}

버튼으로 value 값 설정하기

버튼은 슬라이더 내부에 있는 뷰가 아니다.

버튼을 클릭하면 슬라이더 내 value를 변경할 수 있게 public 함수를 만들어주자

fun setValue(value: Int) {
    currentRad = (180 / maxPercentValue.toDouble() * (maxPercentValue - value)).toRadian()
    sliderValue = value
}

변경된 value 값은 리스너를 등록해 value값이 변경될 때 동작을 정의할 수 있게 하였다.

var sliderValue = 0
    private set(value) { // 슬라이더 value가 변경되면 리스너에 변경된 값과 함께 이벤트 보내기
        if (field != value) {
            field = value
            handleValueChangeEvent(value)
        }
    }

private fun handleValueChangeEvent(value: Int) {
    sliderValueChangeListener?.invoke(value)
}

fun setSliderValueChangeListener(listener: (value: Int) -> Unit) {
    sliderValueChangeListener = listener
}
Clone this wiki locally