My navigation in app is based on ViewPager and custom Page content in it.
I can initialize screens and then navigate through them with switchScreen function.
I have upper main navigation with tabs where you can navigate by clicking tabs and each tab has its designated screen to show.
Each of those main screens can have inner ViewPager with separate navigation and screens.
Issue is that sometimes under some unknown circumstances (I'm not sure why it is happening or what is causing this issue) inner ViewPager will inject its screen into main navigation flow. And really weird thing is, that if I debug it in code, it seems like ViewPager is indeed switching and showing right screen, but I see wrong screen on my display.
It seems like that those main navigation tabs are taking control of inner navigation view pager inside Screen C and displaying its screens inside outer ViewPager (but in code it looks fine - If I switch to Tab B, it will indeed show in logs that Screen B1 is set and displayed, but I see Screen C2 instead)
MainNavigation is initialized inside MainActivity and it has indeed visible tabs which user can click.
Inner navigation inside each Tab does not have any tabLayout visible, it is switching only in code by calling function switchScreen
Example:
MainNavigationActivity (same module handling screen selection is initialized inside each main Pages handling inner navigation):
protected fun createCustomTabScreenFlow(){
mainPagerAdapter = FlowViewPagerAdapter(mainFlowList)
mainViewPager.adapter = mainPagerAdapter
mainViewPager.offscreenPageLimit = mainFlowList.size
mainViewPager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener{
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
backButtonStatus(isEnabled = true)
App.log("MainTabActivity: -------")
App.log("MainTabActivity: onPageSelected: $position")
App.log("MainTabActivity: hasVisibleTabs: $hasVisibleTabs")
val page = mainFlowList[position]
App.log("MainTabActivity: onPageSelected: new: ${page::class.java} current: ${mainCurrentPage?.let { it::class.java }?:"null"}")
setTabsEnabled(false)
if (hasVisibleTabs){
if (page != mainCurrentPage){
//switch automatically
App.log("MainTabActivity: onPageSelected: switch to page: ${page::class.java}")
mainCurrentPage?.onScreenRemoved()
mainCurrentPage?.setScreenVisibleState(false)
switchScreen(page::class.java)
} else {
App.log("MainTabActivity: onPageSelected: switch to page: same page")
setTabsEnabled(true)
}
}
}
})
mainTabLayout.setupWithViewPager(mainViewPager)
(0..mainTabLayout.tabCount).forEach { tab->
mainFlowList.getOrNull(tab)?.icon?.let {
mainTabLayout.getTabAt(tab)?.let { tabItem->
val tabItemView = CustomResources.inflateLayout(layoutInflater, R.layout.custom_tab_view, null, false) as ViewGroup
tabItemView.findViewById<ImageView>(R.id.icon).setImageResource(it)
tabItem.customView = tabItemView
tabItem.customView?.requestLayout()
}
}
}
onScreenFlowIntialized()
}
fun switchScreen(
screen: Class<out FlowScreen>,
payload: List<ScreenPayload>? = null,
action: (() -> Unit)? = null,
useAnim: Boolean = true,
){
App.log("MainTabActivity: switchScreen: $screen")
try {
val index = mainFlowList.indexOfFirst { item -> item::class.java == screen }
App.log("MainTabActivity: switchScreen: index: $index")
if (index >= 0){
delayedScreenSelection {
mainTabLayout.getTabAt(index)?.select()
mainViewPager.setCurrentItem(index, useAnim)
mainPagerAdapter.getPageAtPos(index)?.apply {
App.log("MainTabActivity: switchScreen: page: ${this::class.java}")
mainCurrentPage?.apply {
setScreenVisibleState(false)
resetToDefault()
removeBottomSheet(false)
this#MainTabActivity.removeBottomSheet(false)
}
mainCurrentPage = this
mainCurrentPage?.apply {
setScreenVisibleState(true)
clearPayload()
}
payload?.let { mainCurrentPage?.sendPayload(payload) }
onScreenSwitched()
onPageChanged()
}?:kotlin.run {
setTabsEnabled(true)
}
}
} else {
setTabsEnabled(true)
}
}catch (e: IndexOutOfBoundsException){
App.log("MainTabActivity: switchScreen: $e")
setTabsEnabled(true)
}
}
FlowViewPagerAdapter:
class FlowViewPagerAdapter(private var pageList: List<FlowScreen>): PagerAdapter() {
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun getCount(): Int {
return pageList.size
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
override fun instantiateItem(container: ViewGroup, position: Int): View {
val layout = pageList[position].getScreen(container)
container.addView(layout)
return layout
}
var mCurrentView: View? = null
override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {
mCurrentView = `object` as View
val pager = container as? CustomizableViewPager
mCurrentView?.let { v->
pager?.measureCurrentView(v)
}
}
fun getPageAtPos(pos: Int): FlowScreen?{
return pageList.getOrNull(pos)
}
override fun getPageTitle(position: Int): CharSequence? {
return getPageAtPos(position)?.name
}
}
Related
I'm trying to add more fragments dynamically inside my ViewPager2 however when I call the adapter.add(MyNewFragment, position) 2 things happens, if I use notifyDataSetChange() is not showing up the fragment but if I put notifyItemInserted(position) is showing up but the app crashes with an java.lang.IllegalStateException: Fragment already added.
This is my adapter class:
class ViewPagerAdapter(
list: MutableList<Fragment>,
fm: FragmentManager,
lifecycle: Lifecycle,
//fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fm, lifecycle) {
var fragmentList = list
override fun getItemCount(): Int {
return fragmentList.size
}
fun addScreen(fragment: Fragment, position: Int) {
if(!fragmentList.contains(fragment)){
fragmentList.add(position, fragment)
//notifyItemInserted(position)
notifyDataSetChanged()
}
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
// I tried to use these methods that I saw here but then the viewpager is totally blank and does not load anything or do the same Fragment already added error.
/* override fun getItemId(position: Int): Long {
return fragmentList[position].id.toLong()
}
override fun containsItem(itemId: Long): Boolean = fragmentList.any { it.id.toLong() == itemId }*/
}
And this is my the screen where the viewPager is:
#AndroidEntryPoint
class ViewPagerScreen : Fragment() {
//...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (!::savedViewInstance.isInitialized) {
_binding = FragmentViewPagerScreenBinding.inflate(inflater, container, false)
//....
savedViewInstance = binding.root
}
binding.viewPager.isSaveEnabled = false
return savedViewInstance
}
fun addNewScreen(){
adapter.addScreen(NewFragment(), binding.viewPager.currentItem + 1)
}
private fun initViewPager() {
binding.viewPager.offscreenPageLimit = 2
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
}
})
adapter = ViewPagerAdapterByFragments(
mutableListOf(FirstScreen(), SecondScreen(), ThirdScreen()),
this#ViewPagerScreen.childFragmentManager,
lifecycle
)
binding.viewPager.adapter = adapter
binding.viewPager.isUserInputEnabled = false
}
}
I think I solved the problem, thanks #snachmsm for pointing me out where to look for.
What I did was quite simple, I only had to change the param list: MutableList<Fragment> to list:MutableList<Pair<String,Fragment>> where my String is my fragmentId. Then on the method createFragment return a new fragment base on my fragmentId.
Here's the code itself:
override fun createFragment(position: Int): Fragment {
return when (fragmentList!![position]!!.first) {
"firstScreen" -> return FirstScreen()
"secondScreen" ->return SecondScreen()
"thirdScreen" ->return ThirdScreen()
"newScreen" -> return NewScreen()
else -> AnotherScreen()
}
}
Also my addScreenMethod() now looks like this:
fun addScreen(newfragment: Pair<String, Fragment>, position: Int) {
val result = fragmentList.find {
it.first == newfragment.first
}
if (result == null) {
fragmentList.add(position, newfragment)
notifyItemInserted(position)
}
}
I have the following FragmentStateAdapter:
class TabPagerAdapter(activity: AppCompatActivity, private val itemsCount: Int) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return itemsCount
}
override fun createFragment(position: Int): Fragment {
val default = CreateFragment()
return when (position) {
0 -> default
1 -> {
ProjectFragment()
}
else -> default
}
}
}
How could I refresh a specific fragment (I only need this for ProjectFragment) each time a user swipes to this tab?
You can consider overriding onResume() method for ProjectFragment. Setting fields to null in onResume() might help.
I'm trying to do a proof of concept where a Webview loads local HTML files and users are able to swipe left/right to go to the next file, all while in fullscreen. I'm utilizing ViewPager2 and normal Webviews. I have that part working, but what I want to do is, upon the user doing a single tap, show or hide the toolbar, status bar and navigation controls. Right now I have the setOnTouchListener code on the viewPager, but it looks like the touch events are being consumed by the webview.
How can I accomplish where a single tap would do a toggle between fullscreen and non-fullscreen mode, without disrupting ViewPager paging and long press in the webview?
Here is most of my code. I'm leaving parts out for brevity that should not be related.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewPager.setOnTouchListener { _, event ->
when(event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = event.rawX
initialY = event.rawY
moved = false
}
MotionEvent.ACTION_MOVE -> {
if (event.rawX != initialX || event.rawY != initialY) {
moved = true
}
}
MotionEvent.ACTION_UP -> {
if (!moved) {
toggle()
}
}
}
true
}
viewPager.adapter = MyAdapter(items, this)
}
}
class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.view_pager_item, parent, false) as WebView
return MyViewHolder(view, items)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return items.size
}
}
class MyViewHolder (private val webView: WebView, private val items: List<String>) :
RecyclerView.ViewHolder(webView) {
internal fun bind(position: Int) {
webView.loadUrl("file://" + items[position])
}
}
I have a tab layout in which I have included five fragments, but when i click on tabs, the fragments layout is not showed. In my adapter I've extended FragmentPagerAdapter which has FragmentManager and a variable behaviour.
class ViewPagerAdapter(#NonNull fm:FragmentManager, behaviour:Int):
FragmentPagerAdapter(fm, behaviour) {
private val tabs:Array<Fragment> = arrayOf(
Category1Fragment(),
Category2Fragment(),
Category3Fragment(),
Category4Fragment(),
Category5Fragment()
)
#NonNull
override fun getItem(position: Int): Fragment {
return tabs[position]
}
override fun getCount(): Int {
return tabs.size
}
#Nullable
override fun getPageTitle(position: Int): CharSequence? {
return when (position) {
0 -> "Bags"
1 -> "Watches"
2 -> "Shoes"
3 -> "Glasses"
4 -> "Audio"
else -> null
}
}
}
and in my activity I've called adapter through viewpager, but when I debug is telling me that adapter is null, here's my activity code:
val adapter = ViewPagerAdapter(supportFragmentManager, tab_layout.selectedTabPosition)
val viewPager = findViewById<ViewPager>(R.id.view_pager)
viewPager.adapter = adapter
val tab = findViewById<TabLayout>(R.id.tab_layout)
tab.setupWithViewPager(view_pager)
// -------------------------------------------------------------------------------- //
tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(p0: TabLayout.Tab?) {
}
override fun onTabUnselected(p0: TabLayout.Tab?) {
}
override fun onTabSelected(p0: TabLayout.Tab?) {
viewPager.currentItem = tab.selectedTabPosition
}
})
In the previous versions of android I did the same, but in androidX things are something different. I tried also to use ViewPager2, but it was confusing.
Any help is appreciated!
In the picture below you can see the desired layout:
So, ViewPager has three pages, left and right page are statically filled with two fragments (Fragment 1 and Fragment 2) and the page in the center should have a layout that has toolbar on top, bottom toolbar on bottom and fragment in between which can be changed dynamically.
As far as I can see, there are two ways that this can be done.
Do some magic with the center page that will have both toolbars and set dynamically Fragment0
Each fragment that should go to page in the center should have those toolbars and have the same way of handling events from them, i.e. subclass on AbstractCenterFragment, which contains everything needed to implement this layout and behaviour.
Even if it would be easier to implement solution 2, I don't like it at all, for many reasons. I won't address them here, but if you insist, I can provide them.
As for the solution 1, is there anything that you can suggest to solve this from activity level, since activity with all its components (viewmodel, etc..) should handle this layout?
If I use FragmentPagerAdapter for ViewPager and create parent fragment in the center page which contains toolbar, bottom toolbar and Fragment0, how will that affect performance?
Do you have any other suggestion as using some other type of PagerAdapter that will be suffice to handle behavior of given layout?
Thanks!
PS if you give -1 to this question, please, be polite enough to explain why you gave it.
Ok, I made hybrid solution between FragmentViewPagerAdapter and usual implementation of ViewPagerAdapter with given layout.
Here it is:
abstract class NoMiddleFragmentPagerAdapter(private val mFragmentManager: FragmentManager) : PagerAdapter() {
private var mCurTransaction: FragmentTransaction? = null
private var mCurrentPrimaryItem: Fragment? = null
abstract fun getLeftItem(): Fragment
abstract fun getRightItem(): Fragment
#Throws(IllegalStateException::class)
override fun startUpdate(container: ViewGroup) {
if (container.id == View.NO_ID) {
throw IllegalStateException( "ViewPager with adapter $this requires a view id.")
}
}
override fun instantiateItem(container: ViewGroup, position: Int): Any = if (position == MIDDLE) instantiateCentralArea(container, position) else instantiateFragment(container, position)
private fun instantiateFragment(container: ViewGroup, position: Int) : Fragment {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction()
}
val itemId = getItemId(position)
// Do we already have this fragment?
val name = makeFragmentName(container.id, itemId)
var fragment = mFragmentManager.findFragmentByTag(name)
if (fragment != null) {
mCurTransaction!!.attach(fragment)
} else {
fragment = if (position == LEFT) getLeftItem() else getRightItem()
mCurTransaction!!.add(
container.id, fragment,
makeFragmentName(container.id, itemId)
)
}
if (fragment !== mCurrentPrimaryItem) {
fragment.setMenuVisibility(false)
fragment.userVisibleHint = false
}
return fragment
}
abstract fun instantiateCentralArea(container: ViewGroup, position: Int): Any
override fun getCount(): Int = 3
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) = if (position == MIDDLE) destroyMiddlePart(container, `object`) else destroyFragment(container, position, `object`)
private fun destroyFragment(container: ViewGroup, position: Int, any: Any) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction()
}
mCurTransaction!!.detach(any as Fragment)
}
private fun destroyMiddlePart(container: ViewGroup, `object`: Any) = container.removeView(`object` as View)
override fun setPrimaryItem(container: ViewGroup, position: Int, o: Any) {
if (o !is Fragment) {
super.setPrimaryItem(container, position, o)
return
}
if (o !== mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem!!.setMenuVisibility(false)
mCurrentPrimaryItem!!.userVisibleHint = false
}
o.setMenuVisibility(true)
o.userVisibleHint = true
mCurrentPrimaryItem = o
}
}
override fun finishUpdate(container: ViewGroup) {
if (mCurTransaction != null) {
mCurTransaction!!.commitNowAllowingStateLoss()
mCurTransaction = null
}
}
override fun isViewFromObject(view: View, any: Any): Boolean {
return when (any) {
is Fragment -> any.view == view
is View -> view === any
else -> false
}
}
override fun saveState(): Parcelable? {
return null
}
override fun restoreState(state: Parcelable?, loader: ClassLoader?) {}
fun getItemId(position: Int): Long {
return position.toLong()
}
companion object {
private const val LEFT = 0
private const val MIDDLE = 1
private const val RIGHT = 2
private fun makeFragmentName(viewId: Int, id: Long): String {
return "android:switcher:$viewId:$id"
}
}
}
When implementing this adapter in Activity I created following class:
private class HomePageAdapter(private val layoutInflater: LayoutInflater, fragmentManager: FragmentManager) : NoMiddleFragmentPagerAdapter(fragmentManager) {
override fun getLeftItem(): Fragment = LeftFragment()
override fun getRightItem(): Fragment = RightFragment()
override fun instantiateCentralArea(container: ViewGroup, position: Int): Any {
val view = layoutInflater.inflate(R.layout.main_content_area, container, false)
container.addView(view)
return view
}
}