I have following transition in my app, but the animation to the new destination lags because of extensive loading into recyclerview and other graphic elements.
I have tried to use postDelayed when loading elements on the recyclerview, but on some devices you feel you are waiting after transition and others it take longer time to do the transition (device dependent). So I landed on 700ms, but it is not good for all...
Is there a way (i.e. callbacks) where I can detect when the transition has actually confirmed ended.
My code is farly simple, when clicking a button in one fragment I call this:
private fun onDetailClick(stationId: String, itemView: View)
{
(itemView.context as MainActivity).bottom_navigation.visibility = View.GONE
StationDetailRepository.readStationCurrentPrices2(stationId)
val direction = StationListFragmentDirections.actionStationListFragmentToStationDetailFragment(stationId)
try {
Log.i(TAG,"trns: before navigate")
itemView.findNavController()?.navigate(direction)
Log.i(TAG,"trns: after navigate")
}catch(iae: IllegalArgumentException)
{
iae.printStackTrace()
}
catch (ise: IllegalStateException)
{
ise.printStackTrace()
}
}
Simply, how can I detect that this navigation actually has ended, and there set a viewModel share which I can observe in the other Fragment, so can trigger the loading of elements in a sane manner ?
RG
Related
I have used Animatable.animateTo for animating like below,
val percentageAnimate = remember { Animatable(0.001f) }
LaunchedEffect(Unit) {
percentageAnimate.animateTo(percentage)
}
with percentageAnimate.value I will be drawing my PieChart in the Canvas Composable.
I need the animation only during the first composition.
When I used the above said item in the LazyVerticalGrid, the animation gets triggered everytime when the list item got recycled and added.
This is because you have set percentageAnimate as key, the key is changed (and its internal state) when you call .animateTo() and that leads to relaunching the coroutine in LaunchEffect. If you want to start the animation only once you need to have constant key.
val percentageAnimate = remember { mutableStateOf(Animatable(0.001f)) }
LaunchedEffect(Unit) {
percentageAnimate.value.animateTo(percentage)
}
Then use it in the draw phase percentageAnimate.value
I am trying to create a progress bar that will display while an image is downloading from a server. This image is loaded into a custom view. (I need it to be custom because I draw on the image.)
My solution was to add the custom view into the XML under the layout of the fragment, and mark its visibility as Visibility.GONE. This worked in the XML editor, as the progress bar took up the full space. Invisible did not work as it's position was still displayed.
The issue comes when the image path is given to my custom view. It would seem that setting Visibility.GONE on a view means that the view is not measured. But I need the dimensions of the view to measure how large the bitmap should be.
// Create the observer which updates the UI.
val photoObserver = Observer<String?> { photoPath ->
spinner.visibility = View.GONE
thumbnailFrame.visibility = View.VISIBLE
thumbnailFrame.invalidate()
thumbnailFrame.setImage(photoPath)
Looking at the Logs from the custom view, it is calling onMeasured() but it is doing it too late. I need onMeasure() to be called before setImage(). Is there a better way of handling this and if not is there a way to force the code to wait until I know the view has finished its measuring process?
Solved using a basic listener pattern with an anonymous class inline. I'm not sure if there is a better way but this way works just fine. Delay is not much of an issue since the view draws quite fast anyways.
* Set a listener to notify parent fragment/activity when view has been measured
*/
fun setViewReadyListener(thumbnailHolder: ViewReadyListener) {
holder = thumbnailHolder
}
interface ViewReadyListener {
fun onViewSet()
}
private fun notifyViewReadyListener() {
holder?.onViewSet()
}
spinner.visibility = View.INVISIBLE
thumbnailFrame.visibility = View.VISIBLE
//We have to make sure that the view is finished measuring before we attempt to put in a picture
thumbnailFrame.setViewReadyListener(object : ThumbnailFrame.ViewReadyListener {
override fun onViewSet() {
thumbnailFrame.setImage(photoPath)
//If we have a previous saved state, load it here
val radius = viewModel.thumbnailRadius
val xPosit = viewModel.thumbnailXPosit
val yPosit = viewModel.thumbnailYPosit
if (radius != null) {
thumbnailFrame.setRadius(radius)
}
if (xPosit != null) {
thumbnailFrame.setRadius(xPosit)
}
if (yPosit != null) {
thumbnailFrame.setRadius(yPosit)
}
}
})
}
So, I have simply code
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
DownloadImageTask(holder.avatar).execute(mDataSet[position].avatar);
holder.header.setText(mDataSet[position].header)
holder.body.setText(mDataSet[position].body)
for (i in 0 until mDataSet[position].images.size){
val imgUrl= mDataSet[position].images.get(i)
var image= ImageView(holder.itemView.context)
image.layoutParams = ViewGroup.LayoutParams(200, 200)
image.maxHeight = 200
image.maxWidth = 200
val layout= holder.itemView.findViewById<View>(R.id.linear_layout)
DownloadImageTask(image).execute(imgUrl)
layout.linear_layout.addView(image)
}
}
But after scrolling down and up in view images are shuffling between any items in recyclerView. So, how fix it?
Also don't forgot to remove previous image views added to your linear_layout.
try add linear_layout.removeAllViews() after canceling download process & right before start new images download.
EDIT: IF you update to use Glide..your code must be smaller to this:
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
Glide.with(holder.avatar.context)
.load(mDataSet[position].avatar)
.into(holder.avatar);
holder.header.setText(mDataSet[position].header)
holder.body.setText(mDataSet[position].body)
val layout= holder.itemView.findViewById<View>(R.id.linear_layout)
//cancel previous image download
layout.linear_layout.children.toList().filter { it is ImageView }
.forEach { Glide.with(holder.itemView.context).clear(it) }
// remove image views
layout.linear_layout.removeAllViews()
//add row images
for (i in 0 until mDataSet[position].images.size){
val imgUrl= mDataSet[position].images.get(i)
var image= ImageView(holder.itemView.context)
image.layoutParams = ViewGroup.LayoutParams(200, 200)
image.maxHeight = 200
image.maxWidth = 200
Glide.with(holder.itemView.context).load(imgUrl).into(image)
layout.linear_layout.addView(image)
}
}
note: I try keep code sample, but is better to reuse exist dynamic image views & remove the rest.
This is happening because same view is getting reused and you have multiple async task on a single view when view is scrolled fast. As mentioned in the previous answers library such as Fresco, Glide, Volley etc handles it automatically.
Simplest way to do solve it in current scheme of things is set the async task as a tag in the view and when reusing the same view cancel previous async task which has been set on it. (Pardon java syntax, I'm not friendly with Kotlin yet)
Something like this :
AsyncTask prevTask = (AsyncTask) holder.avatar.getTag();
if(prevTask != null) {
prevTask.cancel();
}
AsyncTask task = DownloadImageTask(holder.avatar);
task.execute(mDataSet[position].avatar)
holder.avatar.setTag(task);
You will have to write similar code for images in the for loop.
It looks like you are using AsyncTask which seems to be the culprit. RecyclerView is rebinding previously created ViewHolders as you scroll them on/off the screen and, since you don't seem to be canceling uncompleted async tasks, you have a race case.
It is probably the case that the async task completes after the ViewHolder is rebound and so it updates the ViewHolder with images for the old item in your mDataSet. To fix this, you need to track and cancel the async tasks as the ViewHolders are bound/unbound.
Better yet, I would advise using an image loading library like Glide. When a ViewHolder is bound, you can cancel any previous loading requests for the ImageViews with Glide.clear() and use Glide.load(...).into(...) to load the new images.
I want to implement shared element transition in my app, when one activity's recycler view item transforms into another activity like here: https://storage.googleapis.com/spec-host-backup/mio-design%2Fassets%2F15N3n1xwTt0briEbfIvFUG01pMv2d_xaT%2F02-focus-focalelement-do.mp4. (source: https://material.io/design/motion/choreography.html#using-a-focal-element)
Namely, the item is fading out and changes bounds then the new activity fade in. As far as I understand it is simple AutoTransition, but it doesn't work. Simple fading doesn't work as well.
Thus, for now I achieve only that the item gets background of new activity an then changes its bounds.
So, I ended up by adding recycler view item's layout in the resulting activity layout. The data (e.g. the title, etc.) of the clicked item is transferred to the next activity with intent.putExtra(). Shared elements in such case will be of course the item's root view and resulting activity's root view. When activity starts I set the data of the item to matching views in activity via SharedElementCallback, e.g.:
setEnterSharedElementCallback(
object : SharedElementCallback() {
override fun onSharedElementStart(...) {
val title = intent.getStringExtra(TITLE)
activity_item_title.text = title
........
}
override fun onSharedElementEnd(...) {
}
})
This allows to show exactly the same item view at the beginning of the transition. Then it should start change its bounds, fading out this item's view at the same time. And at some moment (e.g. in the middle of the transition) when the initial view completely fades out, the laouyt of the activity shows up, fading in gradually. To do this we need to hide item's view in the middle of the transition (View.visibility = View.GONE) and make activity views visible. Probably this is not the best way, but I solve this by adding a listener to shared element enter transition and used Handler().postDelayed:
window.sharedElementEnterTransition.addListener(
object : android.transition.Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {}
override fun onTransitionResume(transition: Transition) {}
override fun onTransitionPause(transition: Transition) {}
override fun onTransitionCancel(transition: Transition) {}
override fun onTransitionStart(transition: Transition) {
Handler().postDelayed({
activity_item.visibility = View.GONE
activity_view_1.visibility = View.VISIBLE
activity_view_2.visibility = View.VISIBLE
.............
.............
// Also you could e.g. set the background to your activity here, ets.
activity_view_root.background = backgroundDrawable
}, 150) //suppuse that the whole transition length is 300ms
}
}
})
The transition animations themselves could look as follows:
<transitionSet>
<targets>
<target android:targetId="#id/activity_root_view"/>
</targets>
<transition
class="com.organization.app.utils.FadeOutTransition"
android:duration="150"/>
<transition
class="com.organization.app.utils.FadeInTransition"
android:startDelay="150"/>
<changeBounds android:duration="300"/>
</transitionSet>
Here, custom FadeOutTransition and FadeInTransition were used since simple android <fade/> animation doesn't work with shared elements. These classes are similar to that given in the answer here: Why Fade transition doesn't work on Shared Element.
The steps for creating return transition are similar.
I have a RecyclerView holding a list of Artists. An artist can be marked as a favorite or not a favorite.
There's a heart to represent this state, and I have a transition drawable, where I'm going from a outlined heart, to a filled in heart based on if the artist in question has been marked as a favorite. The animation itself is working correctly and smoothly transitions between the two when the user clicks on the heart, and looks beautiful.
TransitionDrawable td = new TransitionDrawable(new Drawable[]{
ContextCompat.getDrawable(getActivity(), R.drawable.favorite_border),
ContextCompat.getDrawable(getActivity(), R.drawable.favorite)});
artistViewHolder.image.setImageDrawable(td);
if (artist.isFavorite()) {
td.startTransition(0);
}
In the onClick for the item, I call either .reverseTransition(300) or .startTransition(300) depending on the next state.
I'm running into problems anytime the view is created (1st screen load) and I need to start in the 2nd position (favorite). The initial load you can see it flicker from the empty heart to the filled heart, even though my animation time is set to 0 when it's created. This also happens anytime the list gets invalidated such as if it gets filtered into a smaller set of artists.
I guess since it needs to do things and isn't actually just setting the drawable, 0 isn't actually "instant". I can't find any other way to start in the "end" position though, other than to reverse the starting position of the images in the transition drawable itself.
If I do that though, start/reverse are going to behave differently on a heart by heart basis. At best, I would need to extend the transition drawable, and override the start/reverse/reset methods to take into account which initial state it's in, and that seems kind of hacky?
Am I missing something obvious to start in the end position but not cause that flicker?
Thanks!
You don't migtht remove flicker on td.startTransition(0);
So, you need change order in your Drawables array.
For example
Drawable favorite_border = ContextCompat.getDrawable(getActivity(), R.drawable.favorite_border);
Drawable favorite = ContextCompat.getDrawable(getActivity(), R.drawable.favorite);
TransitionDrawable td = new TransitionDrawable(isFavorite
? new Drawable[]{favorite, favorite_border}
: new Drawable[]{favorite_border, favorite});
artistViewHolder.image.setImageDrawable(td);
This will work provided that you do this in onBindViewHolder
Unless there's a better way to do this, I've gone ahead and extended it, and flipped the start/reverse call based off the initial state.
public class MyTransitionDrawable extends TransitionDrawable{
private boolean initialFavorite = false;
public MyTransitionDrawable(Drawable[] layers) {
super(layers);
}
public MyTransitionDrawable(Drawable[] layers, boolean initialFavorite) {
super(layers);
this.initialFavorite = initialFavorite;
}
public boolean isInitialFavorite() {
return initialFavorite;
}
#Override
public void startTransition(int durationMillis) {
if(!initialFavorite){
super.startTransition(durationMillis);
}else{
super.reverseTransition(durationMillis);
}
}
#Override
public void reverseTransition(int durationMillis) {
if(!initialFavorite){
super.reverseTransition(durationMillis);
}else{
super.startTransition(durationMillis);
}
}
}
`