はじめに
ScrollViewの中にRecyclerViewを置くケースです。
通常のScrollViewは使えないので、ネスト可能なNestedScrollViewを使用します。
画面全体がスクロール可能で、下の方にリスト表示がある構成です。
レイアウト構成
ざっくりですが、こんな感じです。
NestedScrollView
の中にコンテンツA
とコンテンツB
のレイアウトを包含しており、
コンテンツBのレイアウトにRecyclerView
が定義されております。
Activityのレイアウト
<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のレイアウト
<?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のレイアウト
<?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