如何在运行时在 ViewPager2 中 add/remove 选项卡?只使用一个片段

How to add/remove tabs in ViewPager2 at runtime? Using only one fragment

在我正在测试这个概念的应用程序中,应用程序以一个选项卡开始,用户可以单击片段内的按钮并添加一个新选项卡,其中包含 pre-defined 列表中的项目名称的电影名称。假设该列表有 100 个标题,我如何才能 re-use 为每个创建的选项卡创建相同的片段,这样我就不必制作 100 个片段(每个标题一个)?

This link 展示了如何使用多个片段和一个示例应用 add/remove 选项卡。

class DynamicViewPagerAdapter(
    fragmentActivity: FragmentActivity,
    private val titleId: Int
) : FragmentStateAdapter(fragmentActivity) {

override fun createFragment(position: Int): Fragment {
    return DynamicFragment.getInstance(titleId)
}

override fun getItemCount(): Int {
    return titles.size
}

override fun getItemId(position: Int): Long {
    
    return position.toLong()
}

override fun containsItem(itemId: Long): Boolean {
    return titles.contains(titles[itemId.toInt()])
}

fun addTab(title: String) {
    titles.add(title)
    notifyDataSetChanged()
}

fun addTab(index: Int, title: String) {
    titles.add(index, title)
    notifyDataSetChanged()
}

fun removeTab(name: String) {
    titles.remove(name)
    notifyDataSetChanged()
}

fun removeTab(index: Int) {
    titles.removeAt(index)
    notifyDataSetChanged()
}

}

您可以通过使用将项目 ID 传递到的一个 Fragment 来实现此目的。然后片段需要从列表中检索项目并显示该项目的内容。这是一个示例寻呼机适配器。

private inner class SamplePagerAdapter(activity: AppCompatActivity, private val itemId: Int): FragmentStateAdapter(activity) {
            override fun getItemCount(): Int {
                return 100
            }
    
            override fun createFragment(position: Int): Fragment {
                return ItemFragment.newInstance(itemId)
            }
        }

我想通了,一个非常重要的部分是保持序数。在移除选项卡的过程中,适配器多次调用 getItemId() 和 containsItem()。在此过程中,适配器使用选项卡的位置和选项卡中项目的序号。在 example I found which uses different fragments and a predefined number of them, they use enum to get the ordinals, while I used a MutableMap (titles and keys; ordinals as values). Here 中 link 到完成的测试应用。

我使用了这些全局变量(仅作为测试):

// pre-defined list of titles
val testMovieTitles = listOf(
    "Hulk", "Chucky", "Cruella", "Nobody", "Scar Face",
    "Avatar", "Joker", "Profile", "Saw", "Ladies")

val titles = mutableListOf("All Movies")
val titlesOrdinals: MutableMap<String, Int> = mutableMapOf("All Movies" to 0)

const val MY_LOG = "MY_LOG"

activity:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.example.testmyviewpager2.*
import com.example.testmyviewpager2.databinding.ActivityDynamicViewPagerBinding
import com.google.android.material.tabs.TabLayoutMediator

class DynamicViewPagerActivity : AppCompatActivity() {

private var binding: ActivityDynamicViewPagerBinding? = null
var titleIncrementer = 0 // to use the next tile until it doesn't match one of the tabs

val activityViewPagerAdapter: DynamicViewPagerAdapter by lazy {
    DynamicViewPagerAdapter(this)}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityDynamicViewPagerBinding.inflate(layoutInflater)
    setContentView(binding!!.root)

    setUpTabs()
    addTabFabOnClick()
}

private fun setUpTabs() {
    binding!!.dynamicViewPager.offscreenPageLimit = 4
    binding!!.dynamicViewPager.adapter = activityViewPagerAdapter

    // Set the title of the tabs
    TabLayoutMediator(binding!!.dynamicTabLayout, binding!!.dynamicViewPager) { tab, position ->
        tab.text = titles[position]
    }.attach()
}

private fun addTabFabOnClick() {
    binding!!.addTabFab.setOnClickListener {
        val nextTitlePosition = titles.size - 1
        val nextOrdinalId = titlesOrdinals.size - 1
        var nextTitle = testMovieTitles[nextTitlePosition]

        // if a title has been added before, don't add it
        // new tabs cannot have the same name as old tabs
        while(titles.contains(nextTitle)) {
            titleIncrementer++
            nextTitle = testMovieTitles[nextTitlePosition + titleIncrementer]
        }
        if (titleIncrementer > 0) { Log.d("${MY_LOG}Activity", "incrementer: $titleIncrementer") }

        if(!titles.contains(nextTitle)) {
            activityViewPagerAdapter.addTab(nextOrdinalId+1, nextTitle)
        } else {
            Log.d("${MY_LOG}Activity", "\t\t titles contains next title \t\t $titles $nextTitle")}
    }
}
}

可重复使用的片段:

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.testmyviewpager2.*
import kotlinx.android.synthetic.main.fragment_dynamic.*

class DynamicFragment : Fragment() {

private var fragmentViewPagerAdapter: DynamicViewPagerAdapter? = null
private var titleToDisplay = "All Movies"

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_dynamic, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    // get the adapter instance from the main activity
    fragmentViewPagerAdapter = (activity as? DynamicViewPagerActivity)!!.activityViewPagerAdapter
    removeButtonOnClick()
    dynamic_fragment_text.text = titleToDisplay
    Log.d("${MY_LOG}fragCreated", "name: ${titleToDisplay}")
    super.onViewCreated(view, savedInstanceState)
}

override fun onDestroy() {
    Log.d("${MY_LOG}destroyed", "\t\t\t $titles")
    Log.d("${MY_LOG}destroyed", "\t\t\t $titlesOrdinals")
    super.onDestroy()
}

private fun removeButtonOnClick() {
    removeButton.setOnClickListener {

        val numOfTabs = titles.size
        if (numOfTabs > 1 && titleToDisplay != "All Movies") {
            fragmentViewPagerAdapter!!.removeTab(titleToDisplay)
        }
    }
}

fun setTitleText(title: String) {
    titleToDisplay = title
}

companion object{
    //The Fragment retrieves the Item from the List and display the content of that item.
    fun getInstance(titleId: Int): DynamicFragment {
        val thisDynamicFragment = DynamicFragment()
        val titleToDisplay = titles[titleId]
        thisDynamicFragment.setTitleText(titleToDisplay)
        return thisDynamicFragment
    }
}
}

ViewPager2 适配器:

import android.util.Log
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.testmyviewpager2.*

class DynamicViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {

private val theActivity = DynamicViewPagerActivity()

override fun createFragment(position: Int): Fragment {
    // Used this to change the text inside each fragment
    return DynamicFragment.getInstance(titles.size-1)
}

override fun getItemCount(): Int {
    return titles.size
}

override fun getItemId(position: Int): Long {
    return titlesOrdinals[titles[position]]!!.toLong()
}

// called when a tab is removed
override fun containsItem(itemId: Long): Boolean {
    var thisTitle = "No Title"
    titlesOrdinals.forEach{ (k, v) ->
        if(v == itemId.toInt()) {
            thisTitle = k
        }
    }
    return titles.contains(thisTitle)
}

fun addTab(ordinal: Int, title: String) {
    titles.add(title)

    // don't rewrite an ordinal
    if(!titlesOrdinals.containsKey(title)) {
        titlesOrdinals[title] = ordinal
    }
    notifyDataSetChanged()
    Log.d("${MY_LOG}created", "\t\t\t $titles")
    Log.d("${MY_LOG}created", "\t\t\t $titlesOrdinals")
}

fun removeTab(name: String) {
    titles.remove(name)
    notifyDataSetChanged()
    Log.d("${MY_LOG}removeTab", "----------------")
}

fun removeTab(index: Int) {
    titles.removeAt(index)
    notifyDataSetChanged()
}
}