如何在运行时在 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()
}
}
在我正在测试这个概念的应用程序中,应用程序以一个选项卡开始,用户可以单击片段内的按钮并添加一个新选项卡,其中包含 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()
}
}