【Android】HorizontalScrollViewとScrollViewを同期的にスクロールさせてみた

# 作ったもの
こんなものを実装してみました。
![screen_record.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136368/09055fb6-d367-660f-1d92-9c5af67a5fcc.gif)

縦と横のScrollViewを同期させていて、縦がn％スクロールすれば同じく横もn％スクロールします。
縦と横どちらから操作しても同じように動くように実装しています。

# 処理の流れ

1. 各ScrolViewの最大スクロール位置を計算
2. 各ScrollViewのタップ時に、どちらのScrollViewをタップしているのかを保持
3. タップ中のScrollViewがスクロールされる度に、(1)で計算した最大スクロール位置のうち何%スクロールしたのかを計算
4. タップしていない側のScrollViewのスクロール位置を、(3)で算出した割合の位置に変更

# 実装
こちらの記事ではポイントとなるコードのスニペットのみ記載しています。
完全なコードは[Github](https://github.com/umechanhika/playground/tree/master/android/VerticalAndHorizontalSynchronousScrollView)にて公開しています。

## 1. 各ScrolViewの最大スクロール位置を計算
まず根本のロジックとして、縦横のスクロール割合が同じであればスクロール位置は同期すると考えました。
なので、現在のスクロール位置がどれくらいの割合なのかを計算するために、各ScrollViewの最大スクロール位置を計算します。

```kotlin:MainFragment.kt(一部抜粋)
override fun onCreateView(
  inflater: LayoutInflater, container: ViewGroup?,
  savedInstanceState: Bundle?
): View {
  return MainFragmentBinding.inflate(inflater).apply {

    horizontalScrollView.doOnLayout {
      it as ViewGroup
      binding.viewModel?.maxScrollX = it.children.last().right - it.width
    }

    verticalScrollView.doOnLayout {
      it as ViewGroup
      binding.viewModel?.maxScrollY = it.children.last().bottom - it.height
    }
  }.root
}
```

```kotlin:MainViewModel.kt(一部抜粋)
class MainViewModel : ViewModel() {
  var maxScrollX = 0
  var maxScrollY = 0
}
```

最大スクロール位置の計算に必要な値を図で表すとこんな感じです。
`bottom - height`が最大スクロール位置になります。
![Group 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136368/33a0a59c-6d53-c40a-aded-4f932422b43c.png)

## 2. 各ScrollViewのタップ時に、どちらのScrollViewをタップしているのかを保持
この後の処理(3),(4)で、どちらのScrollViewの値で割合を計算してどちらのScrollViewにその値を反映させるのかを判別するために、ユーザーがタップしたScrollViewがどちらなのかを保持しておきます。

```kotlin:MainViewModel.kt(一部抜粋)
fun onTouchScrollView(event: MotionEvent, type: ScrollType): Boolean {
  if (event.action == MotionEvent.ACTION_DOWN) {
    scrollType = type
  }
  return false
}

enum class ScrollType {
  HORIZONTAL,
  VERTICAL,
}
```

```kotlin:View.kt(一部抜粋)
@BindingAdapter("onTouch")
fun View.setOnTouch(l: View.OnTouchListener) = setOnTouchListener(l)
```

```xml:main_fragment.xml(一部抜粋)
<data>

    <import type="com.example.verticalandhorizontalsynchronousscrollview.ui.main.MainViewModel.ScrollType" />

    <variable
        name="viewModel"
        type="com.example.verticalandhorizontalsynchronousscrollview.ui.main.MainViewModel" />
</data>

<!-- 省略 -->

<HorizontalScrollView
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.HORIZONTAL)}">

<!-- 省略 -->

<ScrollView
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.VERTICAL)}">
```

## 3. タップ中のScrollViewがスクロールされる度に、(1)で計算した最大スクロール位置のうち何%スクロールしたのかを計算
スクロール位置の取得には`View.OnScrollChangeListener`を用います。

```kotlin:MainViewModel.kt(一部抜粋)
fun onScrollChanged(x: Int, y: Int) {
  when (scrollType) {
    ScrollType.HORIZONTAL -> {
      val percentage = x * 100 / maxScrollX
      // TODO: (4)で値を反映
    }
    ScrollType.VERTICAL -> {
      val percentage = y * 100 / maxScrollY
      // TODO: (4)で値を反映
    }
  }
}
```

```kotlin:View.kt(一部抜粋)
@BindingAdapter("onScrollChange")
fun View.setOnScrollChange(l: View.OnScrollChangeListener) = setOnScrollChangeListener(l)
```

```xml:main_fragment.xml(一部抜粋)
<HorizontalScrollView
    app:onScrollChange="@{(_, x, y, __, ___) -> viewModel.onScrollChanged(x, y)}"
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.HORIZONTAL)}">

<!-- 省略 -->

<ScrollView
    app:onScrollChange="@{(_, x, y, __, ___) -> viewModel.onScrollChanged(x, y)}"
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.VERTICAL)}">
```

## 4. タップしていない側のScrollViewのスクロール位置を、(3)で算出した割合の位置に変更
DataBindingを用いてタップしていない側のスクロール位置を設定しています。

```kotlin:MainViewModel.kt(一部抜粋)
val scrollX = MutableLiveData<Int>()
val scrollY = MutableLiveData<Int>()

fun onScrollChanged(x: Int, y: Int) {
  when (scrollType) {
    ScrollType.HORIZONTAL -> {
      val percentage = x * 100 / maxScrollX
      scrollY.value = maxScrollY * percentage / 100
    }
    ScrollType.VERTICAL -> {
      val percentage = y * 100 / maxScrollY
      scrollX.value = maxScrollX * percentage / 100
    }
  }
}
```

```xml:main_fragment.xml(一部抜粋)
<HorizontalScrollView
    android:scrollX="@{viewModel.scrollX}"
    app:onScrollChange="@{(_, x, y, __, ___) -> viewModel.onScrollChanged(x, y)}"
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.HORIZONTAL)}">

<!-- 省略 -->

<ScrollView
    android:scrollY="@{viewModel.scrollY}"
    app:onScrollChange="@{(_, x, y, __, ___) -> viewModel.onScrollChanged(x, y)}"
    app:onTouch="@{(_, event) -> viewModel.onTouchScrollView(event, ScrollType.VERTICAL)}">
```

# 参考リンク
ScrollViewの最大スクロール位置の計算式で参考にさせていただきました。
https://stackoverflow.com/a/34866634

# （余談）何故作ったのか
端的に言うと、面白そうと思ったからです。

私事ですが、ここ最近はお金を稼ぎたいという方向に思考が向きすぎていて、純粋に作ること自体を楽しむのを忘れていました。
久しぶりに利益に関係なく作りたいものを作ってみたらめちゃくちゃ楽しかったです！