こんにちは。delyでAndroidのエンジニアをしているkenzoです。
この記事はdely Advent Calendar 2018の13日目の記事です。
Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535
昨日はkurashiruのwebグロース全般を担当しているinternet_ghostがこちらの記事を書きました。
クラシルでのSEO施策についてや、外部の方が気になりそうなポイントについて書かれています。ぜひご覧ください!
はじめに
今日は先日の記事に引き続き、AndroidアプリのViewPagerをいい感じの動きにしていく際にやったことをご紹介します。
今回はページ切り替え時の動きをいい感じにしていきたいと思います。
※ 注意: 今回の記事を読んで試しに実装する場合は一旦下まで読んでから実装してください。上の方のコード使わなくていいかもなので。
ViewPagerのページ切り替えをいい感じに
先日の記事で作成したサンプルアプリに少し機能を追加します。
ページを切り替えるボタン「←」「→」の設置と★のタップではじめに戻るようにします。
ページを切り替える処理を実装
ViewPager
のsetCurrentItem
を使います。
star.setOnClickListener { viewPager.currentItem = 0 }
これでページが切り替わるようになりました。
もう少しゆっくりページを切り替えたい
ページが切り替わるようになりましたが、ちょっと切り替わるスピード早いですよね?ズビュンって感じ。
なので、少しゆっくり切り替わるようにします。
ViewPager
を継承してCustomViewPager
を作成します。
class CustomViewPager @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ViewPager(context, attrs) { companion object { private const val CUSTOM_DURATION: Int = 1000 } init { ViewPager::class.java.getDeclaredField("mScroller").run { isAccessible = true set(this@CustomViewPager, CustomScroller(context)) } } private class CustomScroller(context: Context) : Scroller(context, FastOutSlowInInterpolator()) { override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { super.startScroll(startX, startY, dx, dy, CUSTOM_DURATION) } } }
ViewPager
で使用されるScroller
を自分で作成してセットします。
duration
を1000msにしたので、1秒でページが切り替わるようになります。
これでページがゆっくり切り替わるようになりました。
スワイプするときは今までどおりにしたい
ページがゆっくり切り替わるようになったと思ったら、今度はスワイプする際におかしな挙動をするようになってしまいました。
ボタンを押した時はゆっくり切り替わってほしいけど、スワイプする時は今までどおりに動いてほしいですよね。
なので、先程作成したCustomViewPager
に手を入れてsetCurrentItem
を呼ぶ時だけゆっくり切り替わるようにします。
class CustomViewPager @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ViewPager(context, attrs) { companion object { private const val CUSTOM_DURATION: Int = 1000 } private val interpolator: CustomInterpolator = CustomInterpolator() private val scroller: CustomScroller = CustomScroller(context, interpolator) init { ViewPager::class.java.getDeclaredField("mScroller").run { isAccessible = true set(this@CustomViewPager, scroller) } addOnPageChangeListener(object : OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { when (state) { SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING -> { // ページ切り替えが終わったら、また、切り替え中にスワイプした際に元の挙動で切り替わるように interpolator.isCustom = false scroller.isCustom = false } } } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit override fun onPageSelected(position: Int) = Unit }) } override fun setCurrentItem(item: Int) { interpolator.isCustom = true scroller.isCustom = true super.setCurrentItem(item) } private class CustomScroller(context: Context, interpolator: Interpolator) : Scroller(context, interpolator) { var isCustom: Boolean = false override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { super.startScroll(startX, startY, dx, dy, if (isCustom) CUSTOM_DURATION else duration) } } private class CustomInterpolator : FastOutSlowInInterpolator() { private val default: Interpolator = Interpolator { input -> (input - 1).pow(5) + 1 } // ViewPager内で作成されているInterpolatorをここで再実装 var isCustom: Boolean = false override fun getInterpolation(input: Float): Float = if (isCustom) super.getInterpolation(input) else default.getInterpolation(input) } }
ちょっと長くなってしまいましたが、こんな感じになるよう変更しています。
- ゆっくりとデフォルト両方の動きができる
Scroller
とInterpolator
を作成(isCustom
がtrue
でゆっくり) - ボタンで切り替えを行うタイミングで
isCustom
をtrue
に - 切り替えが終わったら
isCustom
をfalse
に - ゆっくり切り替え中にスワイプした場合にもデフォルトの動きができるように
isCustom
をfalse
に
これで、ボタンで切り替える時はゆっくり、スワイプした時は今までどおりにページが切り替わるようになりました。
この実装でいいのか
アプリの挙動だけ見ると、うまいこと動くようになりました。
しかし、今回作成したCustomViewPager
はリフレクションでmScroller
にアクセスしています。
ViewPager
の中身の実装が変わってmScroller
がなくなる可能性も0ではないので、できれば違う方法で実現したいところです。*1
隣のページに移動するだけでいいなら
今度は先程作成したCustomViewPager
ではなくViewPager
に戻し、別の実装をしてみます。
ViewPager
のfakeDragBegin
fakeDragEnd
fakeDragBy
を用いてページを切り替えます。
これを用いると、隣のページへのスワイプを模した動きをさせることができます。(本記事ではfake dragと呼ぶことにします)
private var prevDragPosition = 0 override fun onCreate(savedInstanceState: Bundle?) { /* 省略 */ left.setOnClickListener { if (viewPager.currentItem > 0) fakeDrag(false) } right.setOnClickListener { if (viewPager.currentItem + 1 < viewPager.adapter?.count ?: 0) fakeDrag(true) } } private fun fakeDrag(forward: Boolean) { if (prevDragPosition == 0 && viewPager.beginFakeDrag()) { ValueAnimator.ofInt(0, viewPager.width).apply { duration = 1000L interpolator = FastOutSlowInInterpolator() addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator?) = Unit override fun onAnimationEnd(animation: Animator?) { viewPager.endFakeDrag() prevDragPosition = 0 } override fun onAnimationCancel(animation: Animator?) { viewPager.endFakeDrag() prevDragPosition = 0 } override fun onAnimationRepeat(animation: Animator?) = Unit }) addUpdateListener { val dragPosition: Int = it.animatedValue as Int val dragOffset: Float = ((dragPosition - prevDragPosition) * if (forward) -1 else 1).toFloat() prevDragPosition = dragPosition viewPager.fakeDragBy(dragOffset) } }.start() } }
このようなことをしています。
beginFakeDrag
でfake dragを始める- いい感じの
duration
とinterpolator
のValueAnimator
を用意 fakeDragBy
で少しずつスクロールさせるViewPager
のwidth
分のスクロールが終わったらendFakeDrag
でfake dragを終了
このようにCustomViewPager
と同様の挙動で隣のページへ切り替えることができました。
おわりに
今回と前回使ったサンプルアプリのソースはこちらです。
先日の記事「Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動」と合わせてみなさまのアプリ上のViewPagerの動き方をよりよいものにしていくための一助になれば幸いです。
明日は検索エンジニアsakuraの「社内SQL勉強会を開催しました」です。お楽しみに!
*1:リフレクションでmScrollerを変更するやり方はぐぐるといっぱい出てくるし、やたら使われてそうなので、そうそう変えられないかなーとは思いますけど