2018/12/13に開催された Shibuya.apk #30 にて発表した資料です
それっぽいカルーセルを作るCyber Agent Inc.Yoshihiro Wada a.k.a. @e10dokup2018/12/13 Shibuya.apk
View Slide
自己紹介• Yoshihiro Wada a.k.a. @e10dokup• 大体右のアイコンで息をしています• CyberAgant Inc. / Ameba• Amebaブログやってます• We’re Hiring• カメラ沼• 財布の透過率が高いのが最近の悩み
今日のコンテンツ• カルーセルを作る• ループするしないとかするとかいろいろあるが…• 今回は画面横方向に面が並んだものと定義します• 正直EpoxyにCarouselItemってあるやん…?21 3 4
特に…• 画面横幅いっぱいにコンテンツアイテムとかを出すスナップで行き来できるカルーセル• マージンがついたり• 触っていないときは隣の面が見えるように…とかだったり• 今回はRecyclerViewで実装していきます(ViewPager)• 本音:Groupieで雑にAdapterを作って管理したかった21 3
やっていきましょう
レベル1単純に画面いっぱいのカルーセル
どうということはない• RecyclerViewに対してlayout_widthがmatch_parentなItemのLayoutを用意する• Adapterにはそのレイアウトを渡してInflateさせる• SnapHelperにRecyclerViewを渡してスナップによるスクロールをさせるようにする
SnapHelper• RecyclerView.onFlingListenerを継承したAbstract class• フリングしたときに子Viewにスナップさせる• 継承クラスは2つ• LinearSnapHelper• snap対象のViewがRecyclerViewの中心に来るようにスナップ• PagerSnapHelper• ViewPagerっぽい挙動のスナップ• SnapHelper#attachToRecyclerView(recyclerView)でアタッチ• nullを入れるとdetach。多重attachしないように気をつける
レイアウトを用意してandroid:layout_width="match_parent"android:layout_height="150dp"android:background="#EEEEEE">android:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"ɾɾɾ/>
RecyclerViewにAdapterをセットする// container͕RecyclerViewͰ͢adapter = RecyclerAdapter(list)// onDestroyͳΓͰdetach͢ΔsnapHelper.attachToRecyclerView(container)container.setHasFixedSize(true)container.layoutManager = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL, // ਫฏํͳͷͰ…false)container.adapter = adapter
レベル2アイテム間にスペースを つけるカルーセル
Spaceを入れるItemDecorationをつけよう• getItemOffsetsをoverrideしたItemDecorationを用意する• 引数のoutRectのleft/rightを必要なspace分ずらす• 実装したItemDecorationをRecyclerViewにaddするだけcontainer.addItemDecoration(SpaceItemDecoration(SPACE_SIZE))
ItemDecorationの中はこんな感じoverride fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State) {outRect.set(0, 0, space, 0) // ඞཁͳϚʔδϯΛηοτ// outRect.right = space ͱ͔ͰOKɻͨͩ͠pxͳͷͰdp -> px͢Δ}
なんか左寄りなので真ん中にしたいoverride fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State) {// ࠨӈۉʹϚʔδϯ͕ͨΔΑ͏ʹ͢ΔoutRect.set(space / 2, 0, space / 2, 0)}
良さそうちゃんと真ん中に寄ってる
レベル3隣のアイテムが見えるカルーセル
やりたいことは…• 触っていないときは隣のアイテムが見えている状態にしたい• 画面幅に対して左右60dpずつマージンをとる• itemが並ぶ間隔はマージンの半分である30dpにする2 3160dp30dp
変えないといけないこと• 元々がwidthがmatch_parentなので、このままではいくらItemDecorationでOffsetをつけても隣のViewは同じようには見えない• OffsetでItemのサイズが変わるので無理やり一つ見えてもほかのサイズが変わったり…• アイテム自体を固定値に…できたら良かった• 世の中の端末すべてが横幅360dpではない• 隣のアイテムが見える + アイテムが中央に来るは達成できるが…• 端末によっては中央にアイテムが来るように調整した結果 余白が大きくスカスカに見える• Eg. Nexus 5X -> 411dp、Essential PH-1 -> 431dp…
つまり…• match_parent時にしたときのitemのサイズを制限できればいい• ということは親が小さくなればいい• 親になるRecyclerView自体にPaddingを設定する
少し算数する• アイテム間のスペースが30dpの時、RecyclerViewのPaddingを60dpにして、かつ中央にアイテムが来るような値のとり方を考える• 前述の通り、アイテムの中央寄せの為に左右に同じスペースをとる• つまりアイテムの左右には15dpを与える• 最初のアイテムも左側には15dp持つことになるので、Paddingを安直に60dpにすると75dpになる。ここは60dpで抑えたい• よってRecyclerView取るべきPaddingは60-15=45dp2 3160dp30dp
少し算数する(一般化)• アイテム間のスペースがx[dp]の時、RecyclerViewのPaddingを2x[dp]にして、かつ中央にアイテムが来るような値のとり方を考える• 前述の通り、アイテムの中央寄せの為に左右に同じスペースをとる• つまりアイテムの左右には0.5x[dp]を与える• 最初のアイテムも左側には0.5x[dp]持つことになるので、Paddingを安直に2x[dp]にすると2.5x[dp]になる。ここは2x[dp]で抑えたい• よってRecyclerViewが取るべきPaddingは2x-0.5x=1.5x[dp]• XML上に書いてもItemDecorationには反映されないのでこれをコードで定義する• SpaceItemDecorationを作る際に一緒にRecyclerViewを引数に渡してみる
いい感じにマージンを設定するfun from(recyclerView: RecyclerView,itemSpaceDp: Int): SpaceItemDecoration {val paddingPx = itemSpaceDp.toPx() // ͍͍ײ͡ʹdp -> pxม// RecyclerViewʹ༩͑Δpaddingʢ1.5x[dp]Λ༻ҙ͢Δʣval edgePaddingPx = (paddingPx * 3 / 2).toInt()recyclerView.setPadding(edgePaddingPx, 0, edgePaddingPx, 0)return SpaceItemDecoration(paddingPx.toInt())}
やったか…?隣が見えていない(´・ω・`)
つまり…• match_parent時にしたときのitemのサイズを制限できればいい• ということは親が小さくなればいい• 親になるRecyclerView自体にPaddingを設定する• RecyclerView自体にPaddingをつけても隣は結局見えない
つまり…• match_parent時にしたときのitemのサイズを制限できればいい• ということは親が小さくなればいい• 親になるRecyclerView自体にPaddingを設定する• RecyclerView自体にPaddingをつけても隣は結局見えない• clipToPaddingをfalseにすることで見えるようになる• デフォルトではPaddingによる余白の描画は切り取るようになっているがそれを表示する• paddingされたコンテンツの外側にあるものが見えるようになる
いい感じにマージンを設定するfun from(recyclerView: RecyclerView,itemSpaceDp: Int): SpaceItemDecoration {val paddingPx = itemSpaceDp.toPx() // ͍͍ײ͡ʹdp -> pxม// RecyclerViewʹ༩͑Δpaddingʢ1.5x[dp]Λ༻ҙ͢Δʣval edgePaddingPx = (paddingPx * 3 / 2).toInt()recyclerView.setPadding(edgePaddingPx, 0, edgePaddingPx, 0)recyclerView.clipToPadding = false // ͜ΕͰྡ͕ݟ͑Δreturn SpaceItemDecoration(paddingPx.toInt())}
良さそうちゃんと隣も見えていて中央に寄ってる
まとめ• RecyclerViewでいろんなカルーセルを作る話でした• 結局作りたかったのは「隣のitemがちらっと見えるカルーセル」• 今回は「画面幅に対するPaddingの半分の間隔でitemが並んでいる時」の話なので デザインで異なる場合はそれにあった算数が必要• EpoxyのCarouselItemを使うと setItemSpacingPx でアイテム間のスペースを 設定できるのでItemDecorationを書かなくてもなんとかなるのかな• 未検証です
ありがとうございました