LoginSignup
3
1

More than 5 years have passed since last update.

RecyclerView + Epoxy で目次機能付き画面を作ってみる

Last updated at Posted at 2018-09-22

完成イメージ

目次パーツ、見出しパーツ、テキストパーツからなる。
画面上部に目次があり、タップすると対応する見出し部分にスクロールする。
table_of_contents.gif

開発環境の設定

開発言語にKotlinを使用し、 databindingを活用していく。その他、ライブラリとしてEpoxyを導入する。

build.gradle
...
apply plugin: 'kotlin-kapt'

android {
    ...
    dataBinding {
        enabled = true
    }
}

kapt {
    generateStubs = true
    correctErrorTypes = true
}

dependencies {
    ...
    implementation 'com.airbnb.android:epoxy:2.10.0'
    kapt 'com.airbnb.android:epoxy-processor:2.10.0'
    implementation 'com.airbnb.android:epoxy-databinding:2.10.0'
    ...
}

メインのレイアウト

RecyclerViewのみを配置する。

activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" >

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            app:layoutManager="android.support.v7.widget.LinearLayoutManager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </FrameLayout>

</layout>

目次パーツのレイアウト

リンクのテキストを入れるためのLinearLayoutのみを配置しておく。

table_of_contents.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android" >

    <LinearLayout
        android:id="@+id/container"
        android:background="#cccccc"
        android:layout_margin="20dp"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</layout>

中身のリンク部分のレイアウトは以下となる。上記のLinearLayoutに下記のレイアウトのviewを動的にaddViewしていく。
リンクがタップされたらコールバックを返す。

link.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android" >

    <data>

        <variable name="link" type="com.example.tomo.tableofcontents.Link" />
        <variable name="callback" type="com.example.tomo.tableofcontents.TableOfContentsCallback" />

    </data>

    <FrameLayout
        android:onClick="@{() -> callback.onClickLink(link)}"
        android:background="?attr/selectableItemBackground"
        android:orientation="vertical"
        android:padding="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/title"
            android:text="@{link.title}"
            android:textColor="#000000"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </FrameLayout>

</layout>

見出しパーツのレイアウト

caption_parts.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable name="caption" type="com.example.tomo.tableofcontents.Content.CaptionParts" />

    </data>

    <LinearLayout
        android:padding="20dp"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:text="@{caption.title}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="text"
            />

        <View
            android:background="#000000"
            android:layout_marginTop="5dp"
            android:layout_width="match_parent"
            android:layout_height="2dp" />

    </LinearLayout>

</layout>

テキストパーツのレイアウト

text_parts.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android" >

    <data>

        <variable name="text" type="String" />

    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200dp">

        <TextView
            android:text="@{text}"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </FrameLayout>

</layout>

package-info.javaの作成

EpoxyにレイアウトファイルからBindingModel_クラスを自動生成してもらうために、package-info.javaを作成する。参考

package-info.java
@EpoxyDataBindingLayouts({
        R.layout.table_of_contents_parts,
        R.layout.text_parts,
        R.layout.caption_parts,
})
package com.example.tomo.tableofcontents;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;

データクラスの作成

目次パーツ、見出しパーツ、テキストパーツのデータ定義を作成する。目次と見出しを紐付けるために、内部的にtagを持たせている。

Data.kt
package com.example.tomo.tableofcontents

data class Data(val contents: List<Content>)

sealed class Content {
    data class TableOfContentsParts(val links: List<Link>) : Content()
    data class CaptionParts(val tag: String, val title: String) : Content()
    data class TextParts(val text: String) : Content()
}

data class Link(val tag: String, val title: String)

EpoxyContollerの作成

データが設定されたら、それぞれviewを構築していく。
目次パーツの中のリンク部分については、動的にviewをinflateし、addViewしていく。

EpoxyController.kt
package com.example.tomo.tableofcontents

import android.content.Context
import android.databinding.DataBindingUtil
import android.graphics.Paint
import android.view.LayoutInflater
import com.airbnb.epoxy.TypedEpoxyController
import com.example.tomo.tableofcontents.databinding.LinkBinding
import com.example.tomo.tableofcontents.databinding.TableOfContentsPartsBinding

class EpoxyController(private val context: Context, private val callback: TableOfContentsCallback) : TypedEpoxyController<Data>() {

    override fun buildModels(data: Data) {

        data.contents.forEach {
            when (it) {
                is Content.TableOfContentsParts -> {
                    val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
                    TableOfContentsPartsBindingModel_()
                            .onBind { model, view, position ->
                                val container = (view.dataBinding as TableOfContentsPartsBinding).container
                                if (container.childCount == 0) {
                                    it.links.forEach {
                                        val binding = DataBindingUtil.inflate<LinkBinding>(inflater, R.layout.link, null, false)
                                        binding.link = it
                                        binding.callback = callback
                                        binding.title.paintFlags = binding.title.paintFlags or Paint.UNDERLINE_TEXT_FLAG
                                        container.addView(binding.root)
                                    }
                                }
                            }
                            .id(modelCountBuiltSoFar)
                            .addTo(this)
                }

                is Content.CaptionParts -> {
                    CaptionPartsBindingModel_()
                            .id(modelCountBuiltSoFar)
                            .caption(it)
                            .addTo(this)
                }

                is Content.TextParts -> {
                    TextPartsBindingModel_()
                            .id(modelCountBuiltSoFar)
                            .text(it.text)
                            .addTo(this)
                }
            }
        }
    }
}

MainActivity.ktの作成

テスト用のデータを作成し、EpoxyControllerに設定している。
目次のリンクがタップされたらコールバックメソッドが呼び出され、一致するタグの見出し位置が画面のトップとなるようsmooth scrollさせる。

MainActivity.kt
package com.example.tomo.tableofcontents

import android.databinding.DataBindingUtil
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import com.example.tomo.tableofcontents.databinding.ActivityMainBinding
import android.support.v7.widget.LinearSmoothScroller

class MainActivity : AppCompatActivity(), TableOfContentsCallback {

    private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) }
    private val controller: EpoxyController by lazy { EpoxyController(this, this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val contents = listOf(
                Content.TableOfContentsParts(listOf(
                        Link("tag1", "caption1"),
                        Link("tag2", "caption2"),
                        Link("tag3", "caption3"),
                        Link("tag4", "caption4"),
                        Link("tag5", "caption5"),
                        Link("tag6", "caption6"),
                        Link("tag7", "caption7"),
                        Link("tag8", "caption8"),
                        Link("tag9", "caption9"),
                        Link("tag10", "caption10")
                )),
                Content.CaptionParts("tag1", "caption1"), Content.TextParts("textParts1"),
                Content.CaptionParts("tag2", "caption2"), Content.TextParts("textParts2"),
                Content.CaptionParts("tag3", "caption3"), Content.TextParts("textParts3"),
                Content.CaptionParts("tag4", "caption4"), Content.TextParts("textParts4"),
                Content.CaptionParts("tag5", "caption5"), Content.TextParts("textParts5"),
                Content.CaptionParts("tag6", "caption6"), Content.TextParts("textParts6"),
                Content.CaptionParts("tag7", "caption7"), Content.TextParts("textParts7"),
                Content.CaptionParts("tag8", "caption8"), Content.TextParts("textParts8"),
                Content.CaptionParts("tag9", "caption9"), Content.TextParts("textParts9"),
                Content.CaptionParts("tag10", "caption10"), Content.TextParts("textParts10")
        )
        val data = Data(contents)

        controller.setData(data)
        binding.recyclerView.adapter = controller.adapter
    }

    override fun onClickLink(link: Link) {
        val smoothScroller = object : LinearSmoothScroller(this) {
            override fun getVerticalSnapPreference(): Int {
                return LinearSmoothScroller.SNAP_TO_START
            }
        }
        for (itemIdx in 0 until controller.adapter.itemCount) {
            val model = controller.adapter.getModelAtPosition(itemIdx)
            if (model is CaptionPartsBindingModel_ && link.tag == model.caption().tag) {
                val layoutManager = binding.recyclerView.layoutManager as LinearLayoutManager
                smoothScroller.targetPosition = itemIdx
                layoutManager.startSmoothScroll(smoothScroller)
                break
            }
        }
    }
}
TableOfContentsCallback.kt
package com.example.tomo.tableofcontents

interface TableOfContentsCallback {
    fun onClickLink(link: Link)
}

まとめ

RecyclerViewとEpoxyを使用して目次機能付き画面ができた。:ok_woman:
table_of_contents.gif

参考

RecyclerViewの実装が楽になるEpoxyライブラリを使ってみる
プロジェクトファイル一式

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1