Cannot use AnimatedVectorDrawable inside Fragment - android

I am trying to run an animated vector inside a fragment. First, I tried passing a drawable resource to fragment and then setting it as a background and starting the AnimatedVectorDrawable object created from the background. It only showed a static background with no animation. Alternatively, I set the background resource in the fragment's layout resource and then creating an AnimatedVectorDrawable from the background in the fragment code and start the AVD. Again, the result was the same: No animation.
I also tried passing the animation to the UI thread to run (just in case fragments use a different thread). Again, no success.
According to Android Developers, as of API 25, vector animations run on RenderThread. I am not sure if it has something with my failure to start the animation inside the fragment.
My code for the fragment is as follows:
class AvdFragment : Fragment() {
/**
* AVD resource file passed to this fragment to be animated
*/
private var mAvdResource: Int = 0;
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_avd, container, false)
}
override fun setArguments(args: Bundle?) {
super.setArguments(args)
mAvdResource = args!!.getInt(AVD_RES)
}
private val LOG_TAG = javaClass.name
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val root = activity!!.findViewById<ViewGroup>(R.id.fragment_root)
root.setBackgroundResource(mAvdResource)
val avd = root.background as AnimatedVectorDrawable
avd.start()
}
companion object {
val AVD_RES = "avd_resource"
}
}

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!)

Main Activity go to background as a result of morphing animation

I have multiple Activities includes multiple fragments. I try to display morphing animation from a RecyclerView item in 1st fragment to an ImageView in 2nd fragment. 1st fragment go to background (it becomes a bit dark) and when I return back from 2nd fragment, when View lays out in its place in RecyclerView, 1st fragment comes to foreground (it becomes lighter). It looks like a blinking in whole screen.
In 1st Activity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setExitSharedElementCallback(
MaterialContainerTransformSharedElementCallback()
)
}
In 1st fragment :
override fun onClick(t: T, poster: View) {
val intent = Intent(activity, DetailActivity::class.java).apply {
putExtras(Bundle().apply {
putParcelable(EXTRA_TMDB_ITEM, t)
putParcelable(EXTRA_NAV_TYPE, navType)
})
}
val options = ActivityOptions.makeSceneTransitionAnimation(
activity,
poster, t.name
)
startActivity(intent, options.toBundle())
}
in 2nd Activity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
// set up shared element transition
setEnterSharedElementCallback(
MaterialContainerTransformSharedElementCallback()
)
window.sharedElementEnterTransition = getContentTransform()
window.sharedElementReturnTransition = getContentTransform()
}
/** get a material container arc transform. */
private fun getContentTransform(): MaterialContainerTransform {
return MaterialContainerTransform().apply {
addTarget(R.id.details_poster)
duration = 450
pathMotion = MaterialArcMotion()
}
}
in 2nd fragment :
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
ViewCompat.setTransitionName(binding.detailsPoster, tmdbItem.name)
}
Source code can be found here : https://github.com/alirezaeiii/TMDb-Paging
Do you know how can I avoid 1st fragment in 1st Activity go to background?

BottomSheetDialog dims background being used in ViewPager2

I have a fragment that I want to display as an embedded fragment in a ViewPager and as a Bottom Sheet. I followed this https://developer.android.com/reference/android/app/DialogFragment#DialogOrEmbed and created a DialogFragment
private val mViewModel: CardPricesViewModel by viewModels()
private val binding by viewBinding(FragmentCardDetailPricesBinding::inflate)
companion object {
// This is the same value as the navArg name so that the SavedStateHandle can acess from either
const val ARG_SKU_IDS = "skuIds"
fun newInstance(skus: List<Long>?) =
CardDetailPricesFragment().apply {
arguments = Bundle().apply {
putLongArray(ARG_SKU_IDS, skus?.toLongArray())
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
However, when it gets inflated in a ViewPager the background dims as though it is a BottomSheetDialogFragment
However, when I manually do it with
supportFragmentManager
.beginTransaction()
.replace(binding.cardPricesFragmentContainer.id, cardDetailPricesFragment)
.commit()
It works fine. I see that the FragmentStateAdapter uses FragmentViewHolders instead of the using transactions directly (?), so I am not sure how to resolve this issue. I see that onCreateDialog() is being called, so if I call dismiss() after onViewCreated(), it works properly, but I am not sure if this a workaround
After some digging, I found the DialogFragment.setShowsDialog(boolean) method that you can use to disable the dialog being created.

Android return transition doesn't work after 5 seconds

Using the AndroidX navigation component, I've created a shared element transition between two fragments. The first fragment has a RecyclerView, and the second fragment shows details of the clicked element. The enter transition always works as expected. The return transition works if I hit the back button right away. However, if I wait more than about 5 seconds before hitting the back button, the animation doesn't work properly.
In that case, there is no animation at all. No move, no fade, everything in the fragment just shows up immediately in place.
Here's the navigation code in the main fragment:
override fun onClick(deck: SlideDeck, sourceView: View) {
viewModel.activeDeck = deck
val extras = FragmentNavigatorExtras(sourceView to "deck_details")
findNavController().navigate(
R.id.action_deckListFragment2_to_deckDetailsFragment,
null,
null,
extras
)
}
Here's the code to set the shared element transition in the second fragment:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val animation = TransitionInflater.from(requireContext()).inflateTransition(
android.R.transition.move
)
sharedElementEnterTransition = animation
sharedElementReturnTransition = animation
binding = FragmentDeckDetailsBinding.inflate(inflater, container, false)
return binding.root
}
Finally, back in the main fragment, I had to postpone the enter transition:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentDeckListBinding.inflate(inflater, container, false)
postponeEnterTransition()
binding.root.doOnPreDraw {
startPostponedEnterTransition()
}
return binding.root
}
I also had to make sure each element in the RecyclerView had a unique and consistent transitionName as follows:
<androidx.cardview.widget.CardView
android:id="#+id/deckDetailsCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="0dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
app:cardCornerRadius="4dp"
android:transitionName="#{String.format(#string/transition_name_string, deck.id)}"
android:onClick="#{() -> itemClickListener.onClick(deck, deckDetailsCard)}">
...
</androidx.cardview.widget.CardView>
Again, this all works as hoped unless I wait longer than ~5 seconds before returning to the main fragment. I've tested this on 2 emulaters (SDK 21 and 28) and a physical device (Galaxy S10E). All exhibit the same behavior.
I figured it out. The problem was here in the main fragment's onViewCreated:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val deckObserver = Observer<List<SlideDeck>> { deckList ->
val listener: DeckListOnItemClickListener = this
val deckListAdapter = DeckListAdapter(requireContext(), deckList,
listener)
deckListAdapter.dataSet = deckList
binding.deckRecyclerView.adapter = deckListAdapter
deckListAdapter.notifyDataSetChanged()
}
viewModel.deckList.observe(viewLifecycleOwner, deckObserver)
}
Do you see it?
I wasn't creating an adapter for the RecyclerView until I received a change notification from the ViewModel. I think the problem was that the RecyclerView didn't have an adpater set until after the animation ran.
So I made the adapter a member of the Fragment, set the RecyclerView's adapter member directly in onViewCreated, then just updated the adapter's list when changes were observed. That seems to have fixed it.

How to initialize context in fragment on Android kotlin

In my application i want show message when fragment has show.
I used viewPager and BottomNavBar for show 4 fragments!
I want when click on BottomNavBar items show fragment and i want when visibility fragment show message.
I write below codes :
class HomeRegisteredFragment : Fragment() {
lateinit var toolbarTile: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_registered, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
//Set title
toolbarTile.text = resources.getString(R.string.registered)
context?.let { ContextCompat.getColor(it, R.color.blue_active) }?.let {
toolbarTile.setTextColor(it)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
Log.e("showFragLog", "Show")
context?.let { Toast.makeText(it, "Show", Toast.LENGTH_SHORT).show() }
}
}
}
In my above codes, when click on my BottomNavBar for show fragment, show me Log message but not show Toast message.
When click on another BottomNavBar items and again click on previous BottomNavBar item, then show Toast message.
I think in first time not initialize context in setUserVisibleHint method.
How can i initialize context for show Toast in every time?
I changed your codes with below codes :
class HomeRegisteredFragment : Fragment() {
lateinit var toolbarTile: TextView
lateinit var handler: Handler
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_registered, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
//Initialize
handler = Handler()
//Set delay
handler.postDelayed({
Toast.makeText(requireContext(),"Show",Toast.LENGTH_SHORT).show()
}, 10)
}
}
}
First you should use requireContext() instead of context() for avoid from memory leak.
For show Toast for every time, you can initialize handler in setUserVisibleHint , then after some delay run your code!
I hope help you
Storing context in a variable is a horrible practive and most of the times leads to memory leaks, use requireContext() this method was introduced in Support Library 27.1.0. Nowdays most likely you will have a newer version or even using androidx so there is no excuse for storing a context
If you are looking for application context to show the toast message, try the below way and see if it works. Also, initialize it onCreate method so you have the activity context at that point.
val appContext = context!!.applicationContext
O have a similar trouble here. I have one Activity with multiple Fragments, and I need a ListView to show some employes.
But when I call the Adapter class, I don't know how to pass the context variable:
binding.listviewCoordenacoes.isClickable = true
binding.listviewCoordenacoes.adapter = CoordenadorAdapter(requireContext().applicationContext as Activity, arrayListCoordenador)
binding.listviewCoordenacoes.setOnClickListener{}
In the examples in general, it works in Activities. If not possible, I will create an Activity and put it in that.

Categories

Resources