Skip to content

그래프 라이브러리 5. 터치, 상호작용

ootr47 edited this page Dec 13, 2023 · 1 revision

문제 1. 그래프 축에서 모든 데이터를 표시할 수 없어서 그래프의 어느 지점이 정확히 무슨 값을 나타내는지 확인하기 어려웠다.

이 부분을 해결하기 위해 그래프를 터치 및 드래그한 위치에 데이터의 x좌표 값과 y좌표 값을 보여주고자 하였다.

이를 위해서 프로퍼티로 pointX라는 변수를 생성하고 터치 이벤트가 발생할 때마다 pointX값을 갱신하였다.

private var pointX = 0F

override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (event == null) {
            return false
        }
        pointX = event.x

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isDragging = true
                pointX = event.x
                invalidate()
            }

            MotionEvent.ACTION_MOVE -> {
                if (isDragging) {
                    pointX = event.x
                    invalidate()
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                isDragging = false
                invalidate()
            }
        }
        return true
    }

이 때, pointY를 생성하지 않은 이유는 터치한(x,y)좌표에 라벨이 나타나는게 아니라 터치한 x좌표에 있는데이터의 y좌표에 라벨이 나타나야되기 때문이다.

따라서 y좌표를 터치 시 갱신하지 않고 x좌표만 갱신한 뒤 데이터 셋에 x좌표에 해당하는 y값을 받아와 새로운 좌표에 터치 라벨을 보여주게끔 하였다.

동작 순서를 정리하면 다음과 같다.

  1. 터치한 x좌표에 위치한 데이터의 x값을 구한다. (좌표는 320.7 같이 터치 좌표를 의미하고 데이터의 x값은 20:32와 같이 실제 데이터 값을 의미한다.)
  2. 데이터셋의 리스트를 돌면서 터치한 x좌표가 어느 데이터에 속하는지 구한다.
  3. 속한 범위를 구했다면 터치한 x좌표와 데이터 y값을 좌표 위치로 변환한다.
  4. isDragging == true일 때 위에서 구해진 x,y 좌표에 터치 라벨을 표시하도록 하였다.
val pointXData = spaceX * ((pointX - graphSpaceStartX.value) / graphWidth.value) + minX

chartData.forEachIndexed { index, data ->
    if (index < size - 1) {
        val next = chartData[index + 1]

        // Calculate position of each data
        val startX = Px((data.x - minX) / spaceX) * graphWidth + graphSpaceStartX
        val startY = Px(1 - (data.y - minY) / spaceY) * graphHeight + graphSpaceStartY
        val endX = Px((next.x - minX) / spaceX) * graphWidth + graphSpaceStartX

        if (startX.value < pointX && endX.value > pointX) {
    	    canvas.drawCircle(pointX, startY.value, circleSize.value / 2, circlePaint)
	    ...
	}
    }
}

이러한 구현을 통해 그래프에서 특정 위치를 터치할 때, 해당 위치의 x좌표에 따른 데이터의 y좌표 값을 터치 라벨로 표시할 수 있다. 코드에서는 터치한 x좌표를 기준으로 데이터셋의 범위를 확인하고, 해당 범위 내에 속하는 데이터의 y값을 가져와서 터치한 위치에 라벨을 표시하도록 구현했다.

문제 2. 스크롤 뷰 하위에서 그래프를 사용할 경우 드래그를 통해 터치 라벨을 움직이다가 위 아래로 이동시 스크롤 뷰의 이벤트가 동작해서 화면이 위 아래로 움직이는 문제가 발생했다.

Screen_Recording_20231213_151620_Chart.Sample.mp4

이 문제를 해결하기 위해서 아래와 같은 코드를 추가해 그래프의 터치 이벤트가 진행중일 때(드래그 중) 부모 뷰의 터치 이벤트를 동작하지 않도록 하였다.

MotionEve

nt.ACTION_MOVE -> {
    if (isDragging) {
         parent.requestDisallowInterceptTouchEvent(true)
         pointX = event.x
         invalidate()
    }

MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
        isDragging = false
        invalidate()
        parent.requestDisallowInterceptTouchEvent(false)
    }
}

문제 3. 그래프 영역을 터치하면 드래그 상황으로 인식해서 그래프 영역 전체에서는 스크롤 뷰의 스크롤이 아예 되지 않는 문제가 발생했다.

Screen_Recording_20231213_152203_Chart.Sample.mp4

처음에는 이를 해결하기 위해 터치 이벤트가 일어난 위치가 그래프의 축과 빈 공간은 영역일 경우에는 터치 라벨을 표시하지 않도록 수정하였다.

하지만 축의 영역보다 그래프가 그려지는 영역이 훨씬 큰데 이 부분에서 스크롤이 안되는 것이 여전히 불편했다.

그래서 다른 해결 방법을 찾는 도중 그래프를 일정 시간 누르고있을 때 터치 라벨이 보이도록 하고자 하였다.

결과적으로 터치 이벤트 코드를 다음과 같은 형태로 수정하였다.

override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (dataset?.isInteractive != true || event == null) {
            return false
        }
        pointX = event.x

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                setLongClickHandler(event.x)
            }

            MotionEvent.ACTION_MOVE -> {
                if (isDragging) {
                    pointX = event.x
                    invalidate()
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                isDragging = false
                invalidate()
                parent.requestDisallowInterceptTouchEvent(false)
                longClickHandler?.removeCallbacksAndMessages(null)
            }
        }
        return true
    }

    private fun setLongClickHandler(x: Float): Boolean {
        longClickHandler = Handler(Looper.getMainLooper())
        longClickHandler?.postDelayed({
            if (!isDragging) {
                parent.requestDisallowInterceptTouchEvent(true)
                isDragging = true
                pointX = x
            }
        }, longClickDelayMillis)
        return super.performClick()
    }

이로써 부모 뷰의 스크롤도 원할하게 동작하면서 그래프의 데이터도 터치 이벤트로 확인할 수 있게끔 하였다.

Screen_Recording_20231213_153433_Chart.Sample.mp4
Clone this wiki locally