グラフライブラリ 5. タッチ、インタラクション

*この記事は韓国語から翻訳したものです。不十分な部分があれば、いつでもフィードバックをいただければありがたいです！ ([オリジナル記事](https://github.com/boostcampwm2023/and09-PriceGuard/wiki/%EA%B7%B8%EB%9E%98%ED%94%84-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-5.-%ED%84%B0%EC%B9%98,-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9))*

グループプロジェクトでグラフライブラリを実装する過程をまとめてみました。今回の記事ではその中でタッチやインタラクション処理部分について説明します。([project repo](https://github.com/boostcampwm2023/and09-PriceGuard), [library repo](https://github.com/Taewan-P/material-android-chart))


### 問題 1.グラフの軸ですべてのデータを表示することができず、グラフのどの点が正確にどのような値を表しているのか確認するのが難しかった。


<img src="https://github.com/boostcampwm2023/and09-PriceGuard/assets/83055885/2ad37db8-b079-400e-a3b3-442f62a5b8bd" width="300" height="600"/>


この部分を解決するため、グラフをタッチ＆ドラッグした位置にデータのx座標値とy座標値を表示するようにしました。

このため、propertyでpointXという変数を生成してタッチイベントが発生するたびにpointXの値を更新しました。

```kotlin
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座標にタッチラベルを表示するようにしました。


```kotlin
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.スクロールビューの下部でグラフを使用する場合、ドラッグでタッチラベルを移動して上下に移動すると、スクロールビューのイベントが動作して画面が上下に移動する問題が発生しました。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/cfceef7f-c37b-ab42-fc04-c2dd791d527b.gif" width="300" height="600"/>


この問題を解決するため、下記のようなコードを追加して、グラフのタッチイベントが進行中(ドラッグ中)に親ビューのタッチイベントを動作させないようにしました。


```kotlin
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.グラフ領域をタッチすると、ドラッグ状況として認識し、グラフ領域全体ではスクロールビューのスクロールが全くできない問題が発生しました。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/a06e6dcd-4f7f-2545-29aa-05f6e48aceeb.gif" width="300" height="600"/>



最初はこの問題を解決するために、タッチイベントが発生した位置がグラフの軸と空白の領域である場合は、タッチラベルを表示しないように修正しました。

しかし、軸の領域よりグラフが描かれる領域がはるかに大きいのに、この部分でスクロールができないのはまだ不便でした。

それで、他の解決方法を探している途中、グラフを一定時間押している時、タッチラベルが見えるようにしようとしました。

その結果、タッチイベントコードを次のような形で修正しました。

```kotlin
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()
    }
```


これにより、親ビューのスクロールもスムーズに動作し、グラフのデータもタッチイベントで確認できるようにしました。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/ccb560f6-c674-89f4-2da1-fad3d547bec2.gif" width="300" height="600"/>


次はMaven Centralへライブラリを配布する方法について書きたいと思います。