作ったもの
縦と横のScrollViewを同期させていて、縦がn%スクロールすれば同じく横もn%スクロールします。
縦と横どちらから操作しても同じように動くように実装しています。
処理の流れ
- 各ScrolViewの最大スクロール位置を計算
- 各ScrollViewのタップ時に、どちらのScrollViewをタップしているのかを保持
- タップ中のScrollViewがスクロールされる度に、(1)で計算した最大スクロール位置のうち何%スクロールしたのかを計算
- タップしていない側のScrollViewのスクロール位置を、(3)で算出した割合の位置に変更
実装
こちらの記事ではポイントとなるコードのスニペットのみ記載しています。
完全なコードはGithubにて公開しています。
1. 各ScrolViewの最大スクロール位置を計算
まず根本のロジックとして、縦横のスクロール割合が同じであればスクロール位置は同期すると考えました。
なので、現在のスクロール位置がどれくらいの割合なのかを計算するために、各ScrollViewの最大スクロール位置を計算します。
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
}
class MainViewModel : ViewModel() {
var maxScrollX = 0
var maxScrollY = 0
}
最大スクロール位置の計算に必要な値を図で表すとこんな感じです。
bottom - height
が最大スクロール位置になります。
2. 各ScrollViewのタップ時に、どちらのScrollViewをタップしているのかを保持
この後の処理(3),(4)で、どちらのScrollViewの値で割合を計算してどちらのScrollViewにその値を反映させるのかを判別するために、ユーザーがタップしたScrollViewがどちらなのかを保持しておきます。
fun onTouchScrollView(event: MotionEvent, type: ScrollType): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
scrollType = type
}
return false
}
enum class ScrollType {
HORIZONTAL,
VERTICAL,
}
@BindingAdapter("onTouch")
fun View.setOnTouch(l: View.OnTouchListener) = setOnTouchListener(l)
<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
を用います。
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)で値を反映
}
}
}
@BindingAdapter("onScrollChange")
fun View.setOnScrollChange(l: View.OnScrollChangeListener) = setOnScrollChangeListener(l)
<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を用いてタップしていない側のスクロール位置を設定しています。
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
}
}
}
<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
(余談)何故作ったのか
端的に言うと、面白そうと思ったからです。
私事ですが、ここ最近はお金を稼ぎたいという方向に思考が向きすぎていて、純粋に作ること自体を楽しむのを忘れていました。
久しぶりに利益に関係なく作りたいものを作ってみたらめちゃくちゃ楽しかったです!