本記事では、現在選択されているタブを再度タップしたときに、タブ内のリストを先頭までスクロール(以下、Scroll To Topと呼びます)させる方法について記載しています。
なお、本記事の最後にサンプルコードのリンクを記載しているので、実行環境の詳細についてはそちらをご参照いただけますと幸いです。
実装方法
1. Scroll To Topのための処理を外部から呼び出すためのインタフェースを定義する
まず、Scroll To Topのための処理を外部から呼び出すために、ListScrollable
インタフェースを定義します。
interface ListScrollable { fun scrollToTop() }
そして、タブ内に表示するFragmentのうち、Scroll To Topを行いたいFragmentに対してListScrollable
を実装します。
overrideするscrollToTop()
の中で、Scroll To Topのための処理を呼び出します。
// RecyclerViewだけをコンポーネントに持つFragment。 class MainFragment : Fragment(), ListScrollable { private lateinit var binding: FragmentMainBinding ... override fun scrollToTop() { binding.recyclerView.smoothScrollToPosition(0) } ... }
2. タブ内の各Fragmentを参照するためのプロパティをFragmentStateAdapter
の具象クラスに追加する
次に、タブ内の各Fragmentを参照するためのプロパティをFragmentStateAdapter
の具象クラスに追加します。
FragmentStateAdapter
は、ViewPager2
を使ってFragmentをページングするために必要な抽象クラスです。
class SectionsPagerAdapter( private val tabsCount: Int, fragmentActivity: FragmentActivity ) : FragmentStateAdapter(fragmentActivity) { // タブ内の各Fragmentの参照を保持しています。 private val _fragments = mutableListOf<Fragment>() // 外部に対してはimmutableなリストとして公開します。 val fragments: List<Fragment> = _fragments override fun createFragment(position: Int): Fragment { return MainFragment.newInstance().also { _fragments.add(it) } } override fun getItemCount(): Int { return tabsCount } }
懸念点として、FragmentStateAdapter
はタブ内の各Fragmentを外部に対して本来は公開していないので、この実装が適切かどうか正直よく分かっていません。
ただ、今回は他にいい方法が思いつかなかったので、この形式で話を進めます。
よりよい方法がありましたら、ぜひアドバイスいただけますと幸いです🙇♂️
3. TabLayout
のイベントリスナー経由でScroll To Topのための処理を呼び出す
最後に、TabLayout
のイベントリスナー経由でScroll To Topのための処理を呼び出します。
現在選択されているタブを再度タップしたときはonTabReselected()
が呼ばれるので、その中でscrollToTop()
を呼び出します。
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { ... binding.tabs.addOnTabSelectedListener(object : SimpleOnTabSelectedListener() { override fun onTabReselected(tab: TabLayout.Tab?) { super.onTabReselected(tab) val tabPosition = tab?.position if (tabPosition != null) { val fragment = sectionsPagerAdapter.fragments.getOrNull(tabPosition) // ListScrollableを実装したFragmentのときだけ、scrollToTop()を呼び出します。 (fragment as? ListScrollable)?.scrollToTop() } } }) ... } ... }
もしくは、FragmentStateAdapter
内で各Fragmentは「"f" + holder.getItemId()
」のタグ名でActivityに追加されているので、2.の対応を行わずに、supportFragmentManager.findFragmentByTag()
を使って取得してもよさそうです。
holder.getItemId()
はAdapterのposition
を返却しています。
android.googlesource.com
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { ... binding.tabs.addOnTabSelectedListener(object : SimpleOnTabSelectedListener() { override fun onTabReselected(tab: TabLayout.Tab?) { super.onTabReselected(tab) val tabPosition = tab?.position if (tabPosition != null) { // ※先ほどのコードとの差分。タグ名に対応するFragmentがないときはnullが返却されます。 val fragment = supportFragmentManager.findFragmentByTag("f$tabPosition") // ListScrollableを実装したFragmentのときだけ、scrollToTop()を呼び出します。 (fragment as? ListScrollable)?.scrollToTop() } } }) ... } ... }
これで、TabLayout
+ ViewPager2
でScroll To Topが実現できます。
なお、SimpleOnTabSelectedListener
は、TabLayout.OnTabSelectedListener
を実装したクラスとなっています。
open class SimpleOnTabSelectedListener : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { // This space for rent } override fun onTabUnselected(tab: TabLayout.Tab?) { // This space for rent } override fun onTabReselected(tab: TabLayout.Tab?) { // This space for rent } }
サンプルコード
サンプルコードは以下になります。