Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
32
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

@unpi

Android NestedScrollView内でRecyclerViewを使用してみた

はじめに

ScrollViewの中にRecyclerViewを置くケースです。
通常のScrollViewは使えないので、ネスト可能なNestedScrollViewを使用します。

画面全体がスクロール可能で、下の方にリスト表示がある構成です。

レイアウト構成

ざっくりですが、こんな感じです。
NestedScrollViewの中にコンテンツAコンテンツBのレイアウトを包含しており、
コンテンツBのレイアウトにRecyclerViewが定義されております。

Activityのレイアウト

activity_main.xml
<androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/contentRoot"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
            android:id="@+id/nestedScrollView"
            android:overScrollMode="never"
            android:layout_marginTop="?attr/actionBarSize"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <LinearLayout
                android:focusableInTouchMode="true"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
            <!-- コンテンツA -->
            <include layout="@layout/view_content_box"/>
            <!-- コンテンツB -->
            <include layout="@layout/view_content_list"/>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

    <FrameLayout android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="?attr/actionBarSize">

        <include layout="@layout/view_section" android:id="@+id/sectionHeader"/>
    </FrameLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

コンテンツAのレイアウト

view_content_box.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:orientation="vertical"
              android:id="@+id/contentBox"
              android:layout_height="wrap_content">

    <View
            android:layout_marginTop="60dp"
            android:layout_marginStart="60dp"
            android:layout_marginEnd="60dp"
            android:background="@color/contentA"
            android:layout_width="match_parent"
            android:layout_height="100dp"/>

            ... 省略

    <include android:visibility="invisible" layout="@layout/view_section" android:id="@+id/sectionBox"/>

</LinearLayout>

コンテンツBのレイアウト

view_content_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:orientation="vertical"
              android:id="@+id/contentList"
              android:layout_height="wrap_content">

    <include layout="@layout/view_section" android:id="@+id/sectionList"/>

    <androidx.recyclerview.widget.RecyclerView
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:id="@+id/recyclerView"
            android:nestedScrollingEnabled="false"
            android:overScrollMode="never"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

</LinearLayout>

実装のポイント

RecyclerViewの高さは動的に設定する必要がある

layout_height="match_parent"で指定すると、
全レコードを表示する高さのRecyclerViewが出来てしまい、
結果セルの使い回しができなくなる。

onCreateなどでRecyclerViewのリサイズを行う。
(以下の青枠領域の高さをコードで設定する)

recyclerView.setOnGlobalLayout {
    val contentHeight = findViewById<View>(android.R.id.content).height
    val height = contentHeight - nestedScrollView.y - sectionHeader.height
    recyclerView.layoutParams = LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.MATCH_PARENT,
        height.toInt()
    )
    // ... 省略

以下は、addOnGlobalLayoutListenerの拡張関数

fun View.setOnGlobalLayout(block: () -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            block()
            viewTreeObserver.removeOnGlobalLayoutListener(this)
        }
    })
}

isNestedScrollingEnabledを設定する

RecyclerViewにisNestedScrollingEnabledのプロパティがある。
本フラグを適切なタイミングで制御しないと、正しいスクロールが得られなくなる。

試しに、isNestedScrollingEnabledをtrue(デフォルト)にした結果が以下である。

このように外側のNestedScrollViewと、
内側のRecyclerViewが独立してスクロールしてしまう。
(要求仕様を満たすのであれば別に問題はない)

NestedScrollViewのスクロールイベントでフラグの制御を行う

nestedScrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int ->
    // コンテンツBの位置までスクロールしてきたら、
    // RecyclerViewをスクロール可能にする
    recyclerView.isNestedScrollingEnabled = scrollY >= contentList.y
}

また、XML側も初期値を設定しておく

<androidx.recyclerview.widget.RecyclerView
    android:nestedScrollingEnabled="false"

全体のソースコード(XMLは除く)

class MainActivity : AppCompatActivity() {

    private val contentListAdapter = ContentListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        initView()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.list_items_1 -> {
                contentListAdapter.listItems = 1
                true
            }
            R.id.list_items_4 -> {
                contentListAdapter.listItems = 4
                true
            }
            R.id.list_items_100 -> {
                contentListAdapter.listItems = 100
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    private fun initView() {
        recyclerView.adapter = contentListAdapter

        val sectionHeaderText = sectionHeader.findViewById<TextView>(R.id.sectionText)
        val sectionBoxText = sectionBox.findViewById<TextView>(R.id.sectionText)
        val sectionListText = sectionList.findViewById<TextView>(R.id.sectionText)

        sectionBoxText.text = "Content Box"
        sectionListText.text = "Content List"

        fun updateView(scrollY: Int) {
            recyclerView.isNestedScrollingEnabled = scrollY >= contentList.y
            when {
                scrollY >= contentList.y - sectionHeader.height && scrollY < contentList.y -> {
                    sectionHeader.isVisible = false
                    sectionBox.isVisible = true
                }
                scrollY >= contentList.y -> {
                    sectionHeader.isVisible = true
                    sectionHeaderText.text = sectionListText.text
                }
                else -> {
                    sectionHeader.isVisible = true
                    sectionBox.isInvisible = true
                    sectionHeaderText.text = sectionBoxText.text
                }
            }
        }
        nestedScrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int ->
            updateView(scrollY)
        }
        recyclerView.setOnGlobalLayout {
            val contentHeight = findViewById<View>(android.R.id.content).height
            val height = contentHeight - nestedScrollView.y - sectionHeader.height
            recyclerView.layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                height.toInt()
            )
            updateView(0)
        }
    }

}

fun View.setOnGlobalLayout(block: () -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            block()
            viewTreeObserver.removeOnGlobalLayoutListener(this)
        }
    })
}

fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
    return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}

class ContentListAdapter : RecyclerView.Adapter<ContentListAdapter.ViewHolder>() {

    var listItems: Int = 50
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ContentListAdapter.ViewHolder {
        return ContentListAdapter.ViewHolder(viewGroup.inflate(R.layout.view_list_cell))
    }

    override fun getItemCount(): Int {
        return listItems
    }

    override fun onBindViewHolder(holder: ContentListAdapter.ViewHolder, position: Int) {
        holder.setViewData(position + 1)
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val iconImage = itemView.iconImage

        fun setViewData(number: Int) {
            iconImage.text = number.toString()
        }
    }
}

おわり

プロジェクトファイルはGitHubに上げております。
https://github.com/unpii/android-scroll-example

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
32
Help us understand the problem. What are the problem?