52
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-28

はじめに

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

52
40
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
52
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?