This is supposedly a simple thing to achieve but for some reason I can't get it to work. I'm using SDK 30 emulator to test. Basically the new fragment appears but there is zero animation whatsoever. If I add a Fade() transition it works only for the fade transition. I've tried setting sharedElement callback but the methods are called only intermittently. Basically it's a simple image. You click on it and the new fragment appears. Click it again and it's gone.
Layout snippet of activity containing image
<FrameLayout
android:id="#+id/container_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
>
<ImageView
android:id="#+id/image_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_baseline_error_24"
android:transitionName="image"
android:layout_gravity="center"
/>
</FrameLayout>
Logic to open image fragment
override fun openImageFragment(imageView: ImageView) {
val fragment = ImageFragment()
fragment.image = imageView.drawable
fragment.arguments = Bundle().also {
it.putString(ImageFragment.KEY_TRANSITION, imageView.transitionName)
}
// fragment.enterTransition = Fade()
// fragment.exitTransition = Fade()
supportFragmentManager
.beginTransaction()
.setReorderingAllowed(true)
.addSharedElement(imageView, imageView.transitionName)
.replace(R.id.container_test, fragment)
.commitNow()
}
Image fragment layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<ImageView
android:id="#+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
/>
</FrameLayout>
ImageFragment
class ImageFragment : Fragment() {
private lateinit var binding: FragmentImageBinding
var image: Drawable?= null
companion object {
const val KEY_TRANSITION = "TRANSITION"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val transition = TransitionInflater.from(requireContext()).inflateTransition(android.R.transition.move)
sharedElementEnterTransition = transition
sharedElementReturnTransition = transition
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentImageBinding.inflate(inflater, container, false)
binding.imageView.transitionName = arguments?.getString(KEY_TRANSITION)
binding.imageView.setImageDrawable(image)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnClickListener {
requireActivity().supportFragmentManager
.beginTransaction()
.remove(this)
.commit()
}
}
}
After much trial and error, I figured out the reason.
My initial fragment was placed statically on the XML, which prevented the fragment manager from replacing it. By using the layout inspector I figured out that the new fragment was placed right below the first one.
Basically the solution is to add the first fragment programmatically so the fragment manager can manipulate it.
Related
I use navigation component and I faced a pretty interesting problem with fragments: whenever I open fragment B from fragment A and then go back to fragment A, fragment A's view state is lost. I mean the view is being created again. The same thing happens if I use FragmentTransaction instead of navigation component. When I detach the fragment and then attach again, the view is being created again. Is there any possible way I can prevent the view from being destroyed or is there any way I can save the state of the view?
UPDATE
I wrote a simple code to demonstrate this behavior:
activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/navHostView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.navHostView) as NavHostFragment?
val navController = navHostFragment!!.navController
navController.setGraph(R.navigation.navigation_main)
}
}
The navigation itself:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation_main"
app:startDestination="#id/fragmentA">
<fragment
android:id="#+id/fragmentA"
android:name="com.sever.fragmentviewtest.FragmentA"
android:label="FragmentA"
tools:layout="#layout/fragment_a">
<action
android:id="#+id/action_fragmentA_to_fragmentB"
app:destination="#id/fragmentB" />
</fragment>
<fragment
android:id="#+id/fragmentB"
android:name="com.sever.fragmentviewtest.FragmentB"
android:label="FragmentB"
tools:layout="#layout/fragment_b" />
</navigation>
Fragment A
class FragmentA : Fragment() {
private var shouldSetText = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
retainInstance = true
val view = inflater.inflate(R.layout.fragment_a, container, false)
if (shouldSetText) { // Will be called only once
view.findViewById<TextView>(R.id.tvTest).text = "Some text"
shouldSetText = false
}
view.rootView.setOnClickListener {
findNavController().navigate(R.id.action_fragmentA_to_fragmentB)
}
return view
}
}
Fragment B
class FragmentB : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
view.rootView.setOnClickListener {
findNavController().navigateUp()
}
return view
}
}
Important
The FragmentA's onCreateView is being called whenever I navigate back from FragmentB. And if I made some changes to FragmentA's views (like set text to a TextView) - they are lost after popping back stack from FragmentB. I tried to use navigateUp - the behavior is the same. And also retainInstance have not helped me to solve the issue.
I'm trying to make a simple button that changes the fragment, it doesn't give any error, it just doesn't work.
XML code:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.follow.FollowFragment">
<ImageView
android:id="#+id/imageView4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="background"
android:scaleType="fitXY"
android:src="#drawable/background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="#+id/loginButton"
android:layout_width="250dp"
android:layout_height="60dp"
android:layout_marginTop="50dp"
android:backgroundTint="#color/btn_col"
android:text="Log In" />
</LinearLayout>
</FrameLayout>
Fragment code:
class FollowFragment : Fragment() {
companion object {
fun newInstance() = FollowFragment()
}
private lateinit var viewModel: FollowViewModel
private lateinit var loginButton: Button
private lateinit var viewFollow: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_follow, container, false)
viewFollow = root
loginButton = viewFollow.findViewById(R.id.loginButton)
var loginFragment = LoginFragment()
println("Hello :(")
loginButton.setOnClickListener {
println("Hello :)")
//parentFragmentManager.beginTransaction().replace(R.id.nav_host_fragment, loginFragment).commit()
}
return inflater.inflate(R.layout.fragment_follow, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
// TODO: Use the ViewModel
}
}
Only the sad hello is printed, it seems to recognize the button, but the listener don't work. The app has a bottom navigation bar, and the screens are fragments.
By returning inflater.inflate(R.layout.fragment_follow, ...) you're inflating a second layout to be used for your fragment instead of the one that you're using to set the listener.
You should be returning root.
In my MainActivity I have a viewpager2 with Tablayout with 5 tabs. The swipe there is working fine. But in one of the 5 tabs I again have a viewpager2 with a tablayout of 3 tabs. In the inner viewpager2 (which is inside a fragment) the swipe isnt working at all and I don't understand why.
The outer viewpager2 is inside a activity while the inner viewpager2 is in the fragment
Viewpager2 inside fragment , swipe not working :
class FreelancerMyProjectsFragment : Fragment() {
lateinit var binding : FragmentFreelancerMyProjectsBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_freelancer_my_projects, container, false)
setUpViewPager()
addTabs()
return binding.root
}
private fun addTabs() {
TabLayoutMediator(binding.projectsTabLayout, binding.projectsViewPager) { tab, position ->
}.attach()
binding.projectsTabLayout.getTabAt(0)?.text =resources.getText(R.string.active)
binding.projectsTabLayout.getTabAt(1)?.text =resources.getText(R.string.bids)
binding.projectsTabLayout.getTabAt(2)?.text =resources.getText(R.string.past)
}
private fun setUpViewPager() {
val adapter = ScreenSlidePagerAdapterNewForFragment(this)
adapter.addFragment(ActiveProjectsFragment())
adapter.addFragment(BidsFragment())
adapter.addFragment(PastProjectFragment())
binding.projectsViewPager.offscreenPageLimit = 3
binding.projectsViewPager.adapter = adapter
}
}
class ScreenSlidePagerAdapterNewForFragment(fragment: Fragment) : FragmentStateAdapter(fragment) {
var fragmentList: ArrayList<Fragment> = ArrayList()
public fun addFragment(fragment: Fragment) {
fragmentList.add(fragment)
}
override fun getItemCount(): Int{ return fragmentList.size}
override fun createFragment(position: Int): Fragment {return fragmentList[position]}
}
And here is the layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/projectsViewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#+id/projectsTabLayout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</androidx.viewpager2.widget.ViewPager2>
<com.google.android.material.tabs.TabLayout
android:id="#+id/projectsTabLayout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginHorizontal="40dp"
app:tabTextAppearance="#style/ProjectTabLayoutFreelancer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</com.google.android.material.tabs.TabLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Please can someone tell me what I'm doing wrong?
The outer Viewpager is preventing the inner viewpager from receiving touch events. You need to intercept the event and stop the outer viewpager from swiping. See the link for implemented solution code
Do fragment need a very precise way of handling the texts that are set in the xml of the fragment ?
I had two times very similar problems : text written in xml cannot be updated properly.
Here is my first fragment. I previously had a foolish text written in the xml for the textview 'msg2ndfragment'. The issue I had was that during the onViewCreated, the text sent by the new Activity to the text would overlap the foolish text written in the Xml.
So I removed the foolish text, and now it is fine. (I could update thee text three times in a row it would work as long as the text was not initially defined in the xml),
class SecondFragment : Fragment() {
//Passer par new instance pour créé le fragment en lui donnant le nom à afficher
companion object {
fun newInstance(title: String?): SecondFragment {
val fragmentSecond = SecondFragment()
val args = Bundle()
args.putString(MainActivity.MESSAGE_SECOND_ACTIVITE, title)
fragmentSecond.arguments = args
return fragmentSecond
}
}
private lateinit var viewModel: SecondViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.second_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(SecondViewModel::class.java)
val message = arguments!!.getString(MainActivity.MESSAGE_SECOND_ACTIVITE, "")
val aries = view?.findViewById<TextView>(R.id.msg2ndfragment);
aries?.text = message
}
}
But now, I have a very similar issue: in a different fragment in another activity I have an editText with a hint. I want to make the hint disappear. Same issue : if the hint is written in the Xml : any text written by the user will only overlap the old text. If I initially define the hint dynamically, the hint disappears when the user starts writing.
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
val aries = view?.findViewById<Button>(R.id.btnGo2ndActivity);
aries?.setOnClickListener { onGoSecondFragmentClick(it) }
Log.d("p1", "p1");
//Faire disparaitre le int au touch car cela ne se fait pas automatiquement
val edTxtTo2nd = view?.findViewById<EditText>(R.id.edt2ndActEditText)
edTxtTo2nd?.hint = "a"
Log.d("pouf", "pouf");
/*val edTxtTo2nd = view?.findViewById<EditText>(R.id.edt2ndActEditText)
edTxtTo2nd?.setOnClickListener(View.OnClickListener { v ->
edTxtTo2nd?.setHint("")
}) */
}
private var listener: onMvmtClickListener? = null
public interface onMvmtClickListener {
fun onNextActivityClick(name1: String)
}
// Store the listener (activity) that will have events fired once the fragment is attached
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is onMvmtClickListener) {
listener = context as onMvmtClickListener
} else {
throw ClassCastException(
"$context must implement nMvmtClickListener"
)
}
}
fun onGoSecondFragmentClick(v: View?) {
val nom = view?.findViewById<EditText>(R.id.edt2ndActEditText);
listener?.onNextActivityClick(nom?.text.toString())
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<TextView
android:id="#+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/hello_world"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/btnGo2ndActivity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="21dp"
android:text="#string/go_to_2nd_act"
app:layout_constraintTop_toBottomOf="#+id/edt2ndActEditText"
tools:layout_editor_absoluteX="111dp"
tools:ignore="MissingConstraints" />
<EditText
android:id="#+id/edt2ndActEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:layout_marginBottom="21dp"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintBottom_toTopOf="#+id/btnGo2ndActivity"
app:layout_constraintTop_toTopOf="#+id/message"
tools:layout_editor_absoluteX="95dp"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
Edit : here are the main_activity kotelin and xml
class MainActivity : AppCompatActivity(), MainFragment.onMvmtClickListener {
companion object{
const val MESSAGE_SECOND_ACTIVITE = "Message.s
econd.activite"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment.newInstance())
.commitNow()
}
}
override fun onNextActivityClick(name1: String) {
var intent = Intent(this, SecondActivity::class.java)
intent.putExtra(MESSAGE_SECOND_ACTIVITE, name1)
startActivity(intent)
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id
/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<fragment
android:id="#+id/fragment"
android:name="com.example.myapplication.ui.main.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Using this workaround to define any text or hint feels wrong to me. How can I define them in the Xml of the fragment without having this overlap issue ?
Thank you in advance for any help.
In my application I'm trying to create 2 fragments with ViewPagers. The fragments have 3 tabs/pages each with RecyclerViews and they are supposed to be backed by a database, so I'm keeping them in a List.
class MainActivity : AppCompatActivity() {
val fragments = listOf(SwipeFragment(), SwipeFragment())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
button1.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
}
button2.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[1]).commit()
}
}
}
The problem is that after navigating away from the SwipeFragment and going back, the view seems empty. Then, if you navigate to e.g. the leftmost page, the rightmost one "appears" (when you go to it).
This results in the middle page staying empty. (the reason for it being that FragmentStatePagerAdapter keeps only the current page and 2 adjacent ones by default. The middle one doesn't get refreshed in a layout with 3 tabs - I tried it also with 5 tabs and I'm able to bring all of them back by going back and forth).
After some debugging I saw that the fragments that represent pages don't get removed from the FragmentManager, but the main SwipeFragment is. I can't get my head around how FragmentManager really works, even after reading almost all of the source code.
If I were able to somehow safely remove the fragments from the FragmentManager, it may work.
I've tried some techniques for saving state, but the framework does that automatically (which might actually be the cause of this problem).
I'm just a beginner in Android, so there's probably a better way to do this anyway. I'll leave the rest of the files here for reference.
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.test.MainActivity">
<FrameLayout
android:id="#+id/placeholder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Button
android:id="#+id/button1"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button1" />
<Button
android:id="#+id/button2"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button2" />
</LinearLayout>
</LinearLayout>
SwipeFragment.kt
class SwipeFragment : Fragment() {
// TODO: set up and keep the database here
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_swipe, container, false)
view.tab_layout.setupWithViewPager(view.pager)
view.pager.adapter = DummyPagerAdapter(activity.supportFragmentManager, index)
return view
}
}
fragment_swipe.xml
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.SwipeFragment">
<android.support.design.widget.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.view.ViewPager>
ItemListFragment.kt
class ItemListFragment() : Fragment() {
// Or keep the database here?
// Probably not the best idea though
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_item_list, container, false)
if (view is RecyclerView) {
view.layoutManager = LinearLayoutManager(view.context)
view.adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
}
return view
}
}
DummyPagerAdapter.kt
class DummyPagerAdapter(manager: FragmentManager, val parentIndex: Int) :
FragmentStatePagerAdapter(manager) {
override fun getItem(position: Int): Fragment = ItemListFragment()
override fun getPageTitle(position: Int): CharSequence = "${ position + 1 }"
override fun getCount(): Int = 3
}
And a basic implementation of RecyclerView.Adapter generated by Android Studio
MyItemRecyclerViewAdapter.kt
class MyItemRecyclerViewAdapter(val values: List<DummyItem>) :
RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
...
}
fragment_item_list.xml
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/list"
android:name="com.example.test.ItemListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:context="com.example.test.ItemListFragment"
tools:listitem="#layout/fragment_item" />
You should use childFragmentManager instead of activity.supportFragmentManager inside SwipeFragment.
view.pager.adapter = DummyPagerAdapter(childFragmentManager, index)
The difference between getSupportFragmentManager() and getChildFragmentManager() is discussed here.
Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.
In my case childFragmentManager didn't help. I think we can use a pattern "Observer" to update existing fragments of ViewPager with new data when return from another fragment.