Share element between recyclerview and toolbar through fragments - android

I have a weird behavior with share elements and I don't know if it comes from a mistunderstanding of share element or a bad implementation.
I looked up on Google and no one seems to suffer from the problem I have.
Let me explain. I have two Fragments, Fragment A is containing a RecyclerView, Fragment B is a detailed view of recyclerview items. Both have a custom toolbar containing a TextView.
I wanted to share the textview of recyclerview items in A to the toolbar textview of B. Currently, the enter share element transition is working (A-->B) but it does not work in the other way (A<--B).
During the return transition, the toolbar textview of B stays in the toolbar and disappears with the B return transition.
But the share element transition works and another textview appears from the top of the recyclerview of A and does its job.
There it is the problem. After onBackPressed, the toolbar textview is not shared anymore and a copy of this toolbar textview is made and animated (only in the recyclerview, it doesn't come from the toolbar) instead of share the B toolbar textview to A recyclerview item
I don't get where is the problem. Transition names are good otherwise animations couldn't work. Any idea? (I am coding under Kotlin)
FragmentActivity
override fun onBackPressed() {
if(supportFragmentManager.backStackEntryCount > 1){
supportFragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
}
Fragment A Adapter ViewHolder
class AnimationRecyclerViewHolder(val view: View, val listener: AnimationRecyclerCallBack) : RecyclerView.ViewHolder(view) {
val text: TextView = view.animation_recycler_text
fun bind(data: AnimationRecyclerData) {
text.text = data.description
text.transitionName = "textTransitionName$layoutPosition"
view.setOnClickListener {
listener.callback(data, view)
}
}
}
Fragment A
override fun callback(data: AnimationRecyclerData, view: View) {
val frag = AnimationPageFragment.newInstance(data.type, this)
val transac = activity!!.supportFragmentManager.beginTransaction()
transac.shareText(view.animation_recycler_text, context!!, this, frag)
transac.replace(activity!!.fragContainerCatalogue.id, frag).addToBackStack(null).commit()
}
shareText method
fun FragmentTransaction.shareText(textview: TextView, context: Context, currentFrag: AbstractAnimationFragment, nextFrag: AbstractAnimationFragment) : FragmentTransaction {
val sharedTransitionName = textview.transitionName
val bundle = nextFrag.arguments ?: Bundle()
bundle.putString("sharedTransitionKey", sharedTransitionName)
bundle.putString("nextTitle", textview.text.toString())
nextFrag.arguments = bundle
nextFrag.enterTransition = Fade()
nextFrag.sharedElementEnterTransition = nextFrag.createShareTransition(sharedTransitionName, currentFrag.context!!)
currentFrag.activity!!.window.sharedElementsUseOverlay = false
return this.addSharedElement(textview, sharedTransitionName)
}
Fragment B
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var rootView = inflater.inflate(R.layout.animation_page_fragment_content, container, false)
toolbar = inflatedView.findViewById(R.id.toolbarShared)
titleToolbar = inflatedView.findViewById(R.id.toolbarTitleShared)
toolbar.setBackgroundColor(Color.RED)
titleToolbar.textSize = 25f
titleToolbar.setTextColor(Color.BLACK)
val bundle = this.arguments ?: Bundle()
val transitionName = bundle.getString("sharedTransitionKey")
val titleName = bundle.getString("nextTitle")
titleToolbar.text = titleName
titleToolbar.transitionName = transitionName
activity!!.window.sharedElementsUseOverlay = true
sharedElementEnterTransition = createShareTransition(titleToolbar.transitionName, context!!)
return rootView
}

I found out the solution.
The issue was indeed related to my RecyclerView.
I never said that my recyclerview was also animated when it is appearing.
I had an enter animation when the recyclerview appears and an animation when user is scrolling. I disabled both and it worked properly.
Now, I just have to find a way to disable RecyclerView animations on return and everything will be okay.
Thanks for those who read me and tried to help me.
The code I used to solve my issue :
frag.setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onSharedElementEnd(sharedElementNames: MutableList<String>?, sharedElements: MutableList<View>?, sharedElementSnapshots: MutableList<View>?) {
super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots)
viewAdapter.mApplyAnim = true
}
})

Related

Should I inflate the layout in onCreateView or onViewCreated?

I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)

why my SharedViewModel is Executing first?

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
noDataTextView = view.findViewById(R.id.no_data_textView)
noDataImageView = view.findViewById(R.id.no_data_imageView)
mToDoViewModel.getAllData.observe(viewLifecycleOwner, Observer { data ->
adapter.setData(data)
mSharedViewModel.checkIfDatabaseEmpty(data)
})
floatingActionButton = view.findViewById<FloatingActionButton>(R.id.floatingActionButton)
listLayout = view.findViewById(R.id.listLayout)
floatingActionButton.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
//set menu
setHasOptionsMenu(true)
mSharedViewModel.emptyDatabase.observe(viewLifecycleOwner, Observer { data ->
showEmptyDatabaseViews(data)
})
return view
}
I have a visibility system going on where if the database is empty then the image is shown.
but when I run the code first image shows up then the data shows up then I debugged it and seen that mSharedViewModel.emptyDatabase.observe() function is running first? what is the main issue here,
ps, I am using suspended fun to load the data
Edit 1:
my default visibility is invisible
<ImageView>
.
.
android:visibility="invisible"
this is my ShareViewModel Class Which will check the database empty or not
class SharedViewModel(application: Application) : AndroidViewModel(application) {
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
fun checkIfDatabaseEmpty(toDoData: List<ToDoData>){
emptyDatabase.value=toDoData.isEmpty()
}
and this my ViewModel
class ToDoViewModel(application: Application):AndroidViewModel(application) {
private val toDoDao= ToDoDatabase.getDatabase(application).ToDoDao()
private val repository:ToDoRepository
val getAllData: LiveData<List<ToDoData>>
init {
repository=ToDoRepository(toDoDao)
getAllData=repository.getAllData
}
Your expectation:
I have a visibility system going on where if the database is empty then the image is shown.
According to your code:
android:visibility="invisible"
The default visibility is invisible okay but check the view model code
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
You set the value to true. So when any observer start observing the changes, the default value will be passed to the observer, so logically your code is OK, database is empty and image view is visible.
So, you should set false as the default value.

make toolbar disappear when image view is clicked

I want to implement a feature that is sorta like a focus mode, essentially when someone clicks on a image view when looking at an image the tool bar would disappear unless it is reclicked. I believe this allows users to focus more on the image instead of toolbar unless they want information on the image. I have tinkered with this idea and tried to set an onclick feature on the image view, so that once clicked it would turn the visibility on the toolbar to invisible and when its clicked again it would make it visible. The problem is that I can only access the image view in the adapter I set for it and even when I got the alogrithm right (as in I put print statements to see what if statement I enter, and that works successfully) but what happens is that the toolbar doesn't react to it as if I cannot communicate with it.
from adapter
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val page_layout: View = inflater.inflate(R.layout.activity_viewer, container, false)
val presenter: Toolbar = page_layout.findViewById<View>(R.id.presenter) as Toolbar
val image_layout: View = inflater.inflate(R.layout.view_pager_item, container, false)
val page_image: PhotoView = image_layout.findViewById<View>(R.id.page_image) as PhotoView
Picasso.get().load(PageList[position].link).into(page_image)
page_image.setOnClickListener(View.OnClickListener {
println("clicked")
println(presenter.visibility)
if (presenter.visibility == View.INVISIBLE) {
println("outside")
presenter.visibility = View.VISIBLE
} else {
println("inside")
presenter.visibility = View.INVISIBLE
}
})
container.addView(image_layout)
return image_layout
}
from onCreate method from activity
class Page_Activity : AppCompatActivity() {
lateinit var lstPages: MutableList<Page>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewer)
lstPages = ArrayList()
mangaPages()
val myrv = findViewById<View>(R.id.view_page) as ViewPager
myViewPager = PageViewAdapter(this, lstPages)
myrv.adapter = myViewPager
the foucus on the activity is to make the call so that there is an array full of image link which gets throws into the adapter so that I can display it back in the activity. Nevertheless, I tested with this concept with a framelayout where I attack a onClickListener, but for someone reason when I do it to an image view in an adapter it act different. Anyways any help with this would be much appreciated!!! Thank you!
Your toolbar doesn't behave as you want because you are inflating a new toolbar in your adapter.
In this code
val page_layout: View = inflater.inflate(R.layout.activity_viewer, container, false)
val presenter: Toolbar = page_layout.findViewById<View>(R.id.presenter) as Toolbar
So, in order to access the toolbar that you have inflated in your Page_Activity, you can implement a callback from your adapter to your Page_Activity, like this
First, create a callback
interface PageImageCallback {
fun onClick()
}
Then, create a variable, setter function and call onClick function in your adapter class
class Adapter() {
private lateinit var pageImageCallback: PageImageCallback
fun setPageImageCallback(pageImageCallback: PageImageCallback) {
this.pageImageCallback = pageImageCallback
}
...
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val image_layout: View = inflater.inflate(R.layout.view_pager_item, container, false)
val page_image: PhotoView = image_layout.findViewById<View>(R.id.page_image) as PhotoView
Picasso.get().load(PageList[position].link).into(page_image)
page_image.setOnClickListener(View.OnClickListener {
println("clicked")
pageImageCallback.onClick()
})
container.addView(image_layout)
return image_layout
}
}
Lastly, implement the callback in Page_Activity
class Page_Activity : AppCompatActivity(), PageImageCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewer)
...
myViewPager.setPageImageCallback(this)
}
...
override fun onClick() {
if (presenter.visibility == View.INVISIBLE) {
println("outside")
presenter.visibility = View.VISIBLE
} else {
println("inside")
presenter.visibility = View.INVISIBLE
}
}
}

How to refresh data in fragment in ViewPager2 with an interface

I'm trying to refresh the data in my fragment every time the user clicks on it in the Bottom Menu Navigation. I already wrote an interface which gets called each time the fragment gets selected by the user
The problem is that the method inside my fragment has no access to the view of the fragment (I guess):
MainMenu
viewPager = findViewById(R.id.frame_container)
viewPager!!.offscreenPageLimit = 5
viewPager!!.orientation = ViewPager2.ORIENTATION_HORIZONTAL
viewPager!!.adapter = pageAdapter
viewPager!!.currentItem = 0
viewPager!!.isUserInputEnabled = false
viewPager!!.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if(position == 2) {
val mFragment = ListFragment()
mFragment.ready(this#MainMenu)
}
}
})
The Interface "Ready":
interface ReadyInterface {
fun ready(activity: FragmentActivity?)
}
and the ListFragment:
class ListFragment: Fragment(), ReadyInterface {
var mView : View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_list, container, false)
this.mView = view
val user = FirebaseAuth.getInstance().currentUser
return if (user != null) {
view
} else {
val unregisteredView = inflater.inflate(R.layout.fragment_unregistred, container, false)
val registerNow = unregisteredView.findViewById<TextView>(R.id.textview_registernow)
unregisteredView
}
override fun ready(activity: FragmentActivity?) {
testText = mView!!.findViewById(R.id.test_text)
testText.text = "Test Text here"
Toast.makeText(activity!!.applicationContext,"Test",Toast.LENGTH_LONG).show()
}
This code here crashes with a "kotlin.KotlinNullPointerException" on line "testText.text = ...."
So I guess the fun ready hasn't got access to the view of my fragment because of the fragments lifecycle, am I right? How could I fix this?
There is no need to implement interface here. You can directly make ready() a member function of ListFragment and call it from onPageSelected(position: Int) in MainMenuActivity. Currently, the interface method is being called before onCreateView() causing it to throw null pointer exception as the view is not initialised yet.
You are creating a new instance of ListFragment() while calling ready on it. This new fragment is not attached to the ViewPager. This new instance also has not gone through any lifecycle methods, therefore, it's view is not yet initialized. Due to this, you get a NullPointerException while calling methods on a view.
To solve this you can instead fetch the current fragment from the ViewPager and call your method on it.

How do I switch from one fragment to another when a list item is clicked?

I have a list in "All Users" Fragment. When a name on list is clicked, It should switch to "User Details" Fragment. I can't seem to find a way to somehow destroy this fragment and bring up the new one, or just display the new one on top of old.
The language used is Kotlin. I'm new to this. I tried some ways with Fragment Manager but can't seem to figure out the right syntax.
Main Activity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Default view when app starts
title = resources.getString(R.string.allusers)
loadFragment(AllUsersFragment())
//Bottom Navigation Bar
navigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_allusers -> {
title = resources.getString(R.string.allusers)
loadFragment(AllUsersFragment())
return#setOnNavigationItemSelectedListener true
}
R.id.navigation_transfer -> {
title = resources.getString(R.string.transfer)
loadFragment(TransferFragment())
return#setOnNavigationItemSelectedListener true
}
R.id.navigation_logs -> {
title = resources.getString(R.string.logs)
loadFragment(LogsFragment())
return#setOnNavigationItemSelectedListener true
}
}
false
}
}
//function to load fragment when bottom navigation is clicked
fun loadFragment(fragment: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.container, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
}
AllUsersFragment.kt
class AllUsersFragment : Fragment(){
private lateinit var listView:ListView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.fragment_allusers, container, false)
val details = UserDetailsFragment()
val bundle = Bundle() //To transfer information
listView = rootView.findViewById(R.id.usersList)
val userNames = arrayOf(
"Andrew Jackson",
"Barry Alan",
"Caitlyn Snow",
"Drake Ramoray"
)
val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, userNames)
listView.adapter = adapter
//List view item clicked listener
listView.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
val userName = userNames[i]
bundle.putString("str", userName)
bundle.putInt("int", i)
details.arguments = bundle
}
return rootView
}
}
UserDetails.kt
class UserDetailsFragment : Fragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.fragment_user_details,container,false)
val nameField = rootView.findViewById<TextView>(R.id.nameField)
val imageField = rootView.findViewById<ImageView>(R.id.imageField)
//Importing images
val andrewjackson = BitmapFactory.decodeResource(context!!.resources,R.drawable.andrewjackson)
val barryalan = BitmapFactory.decodeResource(context!!.resources,R.drawable.barryalan)
val caitlynsnow = BitmapFactory.decodeResource(context!!.resources,R.drawable.caitlynsnow)
val drakeramoray = BitmapFactory.decodeResource(context!!.resources,R.drawable.drakeramoray)
val userImages = ArrayList<Bitmap>()
userImages.add(andrewjackson)
userImages.add(barryalan)
userImages.add(caitlynsnow)
userImages.add(drakeramoray)
val name = arguments!!.getString("str") //Displays name clicked on Details Page
nameField.setText(name)
val i =arguments!!.getInt("int") //Displays image of user from array
imageField.setImageBitmap(userImages[i])
return rootView
}
}
I think using the loadFragment function in MainActivity.kt it might be possible, but I can't figure out a way to use that inside the fragment.
Expected Output:
When a name on the list is clicked, it should change to another fragment in the same container, that displays user details.
You can use this code in your fragment to replace the fragment in your container
val someFragment = YourUserDetailsFragment()
val transaction = fragmentManager!!.beginTransaction()
transaction.replace(R.id.container, someFragment)
transaction.addToBackStack(null)
transaction.commit()
Yes, There are two ways using Interface and by the public method. As your fragment is bound with the activity that means it's the parent is activity.
So you can access the method just to make public inactivity and access by using
(activity as MainActivity).loadFragment(//Your fragment intance)
Here is the example to communicate between activity and fragments.
You already have everything set up, in your, on click listener you should just call activity?.loadFragment(UserDetailsFragment())
listView.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
val userName = userNames[i]
bundle.putString("str", userName)
bundle.putInt("int", i)
details.arguments = bundle
(activity as MainActivity)?.loadFragment(details)
}

Categories

Resources