-
Notifications
You must be signed in to change notification settings - Fork 1
그래프 라이브러리 5. 터치, 상호작용
이 부분을 해결하기 위해 그래프를 터치 및 드래그한 위치에 데이터의 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값을 받아와 새로운 좌표에 터치 라벨을 보여주게끔 하였다.
동작 순서를 정리하면 다음과 같다.
- 터치한 x좌표에 위치한 데이터의 x값을 구한다. (좌표는 320.7 같이 터치 좌표를 의미하고 데이터의 x값은 20:32와 같이 실제 데이터 값을 의미한다.)
- 데이터셋의 리스트를 돌면서 터치한 x좌표가 어느 데이터에 속하는지 구한다.
- 속한 범위를 구했다면 터치한 x좌표와 데이터 y값을 좌표 위치로 변환한다.
- 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)
}
}
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()
}
이로써 부모 뷰의 스크롤도 원할하게 동작하면서 그래프의 데이터도 터치 이벤트로 확인할 수 있게끔 하였다.