tl;dr How to wait until the true scrolling is done after calling scrollToPosition()? What I mean by true scrolling is that (1) the RecyclerView has finished the scrolling and (2) it has bound all the ViewHolders for the set position of scroll.
Description:
I am trying to fix some transition animation for recycler view and I'm having a hard time achieving it.
The app is consistent of a RecyclerView in one of the activities and a ViewPager that acts as a "detail view" when users click on an item in the RecyclerView.
The forward navigation from RecyclerView to ViewPager works perfectly fine but the problem is, the backward navigation doesn't work. Please note that the user may navigate using the ViewPager in the detail view and when they re-enter the activity with RecyclerView, the position of the item may be different from the original case, meaning the RecyclerView needs to scoll first to the item and then resume the transition animation.
The reason I can't make it to work is because I resume the animation before the RecyclerView has properly scolled and bound the underlying ViewHolders but I don't know how to wait for the scrolling to truly finish and then resume the shared element transitions.
What I do in my code (on the Reenter code) is this:
if (isReentering) {
// postpone the animation here
ActivityCompat.postponeEnterTransition(getActivity());
scollIndex = ... // determine the correct item position
layoutManager.scrollToPosition(scrollIndex);
final RecyclerView recyclerView = getRecyclerView();
new Handler().post(new Runnable() {
#Override
public void run() {
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
recyclerView.requestLayout();
ActivityCompat.startPostponedEnterTransition(getActivity());
return true;
}
});
}
});
}
I also set the transition name in my Adapter as such:
#Override
public void onBindViewHolder(TViewHolder holder, int position) {
....
holder.setTransitionName(_data.get(position).getContentId().toString());
}
The transition name is the UUID of the item, set in the underlying View. Both the ViewPager and the RecyclerView set this correctly.
The scrollIndex is correct and the RecyclerView does scroll to it but here's the catch: The scrolling and binding only takes effect after I've already started the postponed enter transition in my code (by calling ActivityCompat.startPostponedEnterTransition(getActivity());) which causes weird transition animations.
What I would like to happen is to somehow wait until the scrolling and the ViewHolder binding is finished before calling startPostponedEnterTransition so my question is, how can I find out when the scrolling has finished and all the ViewHolders are bound so that I can call this?
Many thanks in advance,
You can check, if the ViewHolders have been bound, by using the code in this answer.
Related
I'm having an issue when deleting or adding items to a RecyclerView element, the problem is not when adding or deleting itself, so I explain the situation.
I have a recycler and a button to add elements to it, if the user press a view, it shows a dialog that contains a delete button and some more stuff. I can add and/or delete items on recycler view, and the view updates ok, using notifyDataSetChanged(), but when I press back button(only by doing this, not any other way) the view reappears again (in case of deletion) or disappear( if I did an adding) setting the recycler the way it was before changes, and I don't want this behavior, because of the delete and add method I created don't just add/delete the view, but associated data in a database too, so If I press the view after deletion the app crash because there are not such data anymore, but the view exists still.
All this content is inside a fragment accessible by a Navigation Drawer, if I navigate other fragment and return to this one again the back press do not affect the recycler anymore, and all those "ghosts" views desapear becouse the data is reloaded from database.
I need a som help for avoiding this, I know undo deletion can be done, but I don want to, I just want to delete data.
UPDATE: This is exactly what happens, the vanishing/reappearing of the items in the recycler View is caused by a back button pressed
This one shows the vanishing after adding
This one shows the reappearing after deleting
UPDATE 2: I've solved the problem by overriding the onBackPressed method using this documentation from google, when back pressed what I do is to navigate to first fragment in the navigation, this is not what I wanted but solved the problem temporarily (as a patch), I'm still looking for answer.
UPDATE 3: Posted my own answer with the fix of all this problem, not using onBackPressed, it worked fine, and I realize some curious behavior
your lists are out of sync. you have 2 lists, one that holds a reference for you collection of objects that are passed to the recycler, the other one is within the recycler to iterate over and draw your view holders.
I suggest once you delete|add sth, beside DB, do it to the outer list (one outside recycler) and pass the list as a whole to the recycler. this way you will ensure that the recycler's list is always up-to-date.
this is not the best solution out there, other possible solutions might include an RX Supported Db with no intermediate calls to invoke the recycler.
As you may notice my app has a navigation drawer, when trying to fix the problem by overridig the onBackPressed() in fragment, I tried to get back to the first fragment in my navigation drawer, when doing so I realize an error that I had not realized before:
NavigationUI.onNavDestinationSelected(menuItem,navController);
This line was duplicated in the NavigationItemSelectedListener:
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull final MenuItem menuItem) {
int id=menuItem.getItemId();
//this line indicates the action of changing fragment using navController reference
NavigationUI.onNavDestinationSelected(menuItem,navController);
if (id==R.id.nav_cuanto){
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {drawer.closeDrawer(GravityCompat.START);}
},300);
}
if(id==R.id.nav_reportes){
// NavigationUI.onNavDestinationSelected(menuItem,navController); -> this line caused the hole problem
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {drawer.closeDrawer(GravityCompat.START);}
},300);
}
if(id==R.id.nav_about){
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
drawer.closeDrawer(GravityCompat.START);
}
},300);
}
return true;
}
});
I deleted the line and everything worked just fine, as expected, this particular method creates a new instance of the fragment you are trying to navigate to, but the problem is that when you try to navigate to the fragment you already are in, it creates a new instance a navigate to it, and this new instance is on the over the old one, so when back pressed the app do not return to previous view, but to the same fragment with the old view (out of date, of couse), I couldn't imagine this behavior, this is highly not imaginable, and it's not in the docs so I almost die trying to understand. So WARNING: if someone is trying to use
NavigationUI.onNavDestinationSelected(menuItem,navController);
do not use it more than once in the same call, if so you will need to press back twice in best case scenario :-(
There is a ViewPager with Fragments generated dynamically.
Questions:
What is the way to catch the moment when user slides away from the fragment (so I can bring it into "clean", "init" state)?
or
How to catch moment when a Fragment is scrolled in?
Problems:
Have checked Fragment Lifecycle, but none of them is getting triggered when scrolled out/in (using ViewPager)
Lifecycle phases are triggered only if I scroll 2+ Fragments (those 3rd one is getting Paused/Resumed).
To get a callback when a fragment gets visible to the user you can override the setUserVisibleHint method, like this:
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser){
//Put your 'init' logic here
}
}
the variable isVisibleToUser will give the status of the visibility, so you can use the same method to handle when the fragment goes out.
addOnPageChangeListener
void addOnPageChangeListener (ViewPager.OnPageChangeListener listener)
Add a listener that will be invoked whenever the page changes or is incrementally scrolled. See ViewPager.OnPageChangeListener.
Components that add a listener should take care to remove it when finished. Other components that take ownership of a view may call clearOnPageChangeListeners() to remove all attached listeners.
You can implement this listener to track the movement of the fragments in the ViewPager.
There is also a method setOffscreenPageLimit() which Set the number of pages that should be retained to either side of the current page and it's default value is 1 and minimum value can be set to 0.
I have applied animation for my recyclerview as follows
SlideInRightAnimator animator = new SlideInRightAnimator();//using custom Animation to remove the recyclerview Item
animator.setRemoveDuration(200);//specify the duration to run the animation
mRecyclerView.setItemAnimator(animator);
I have applied this animation on recyclerview since I do not know the
However I would like to cancel stop animation in my Adapter once once I do notifyItemRemoved().
I understand the onBindViewHolder is called immediately after but not sure how can I pass my animator into my Recyclerview adapter or call the stop animation in general.
I have eventbus handler in the Fragment and I am calling the removeItem method on Adapter as below.
public void onEventMainThread(MyEvent event) {
switch (event.getEventType()) {
......
case DELETE:
mAdapter.removeItem(event.getItem());
break;
default: break;
}
}
Update: I also tried to do following after I call notify Item removed:
mRecyclerView.setItemAnimator(null);
But this eventually cancels the animation all together
Appreciate your kind response
Well, I came up with an option that works. I am sure there are better options, but here's what I did.
In my Adapter class, I copied the reference of the view holder within onBindViewHolder method. And then in theonRemoveItem method I used the viewHolder's itemview to animate using ObjectAnimator and Animation set. After the animation , I made the viewholder copy to be null.
This way I was able to achieve animation on individual view.
I have a RecyclerView with images and when I press an image the app opens another activity that contains a ViewPager with the same images but in the position of the one I selected.
I've done the transition in Lollipop to share this image between activities using supportPostponeEnterTransition and supportStartPostponedEnterTransition in the called activity to wait until the viewPager is loaded with images to start the transition.
When I enter in the called activity and when I press back the transitions are ok.
The problem I'm facing is if I move to another image in the ViewPager of the called activity, when I press back it animates the image that it was selected at the beginning, not the currently selected one.
I've been able to change the animated image to the one selected in the called activity with this:
#Override
public void onBackPressed() {
View view = ((ImageDetailFragment) adapter.getFragment(viewPager,
viewPager.getCurrentItem())).getTransitionView();
ViewCompat.setTransitionName(view, Constants.TRANSITION_IMAGE);
super.onBackPressed();
}
But it is returning to the same position of the original image in the list of the calling activity.
How can I do it to make the image return to its position in the list of the calling activity?
The first thing to do is to make sure that the views work properly without any Activity transition. That is, when your return from Activity with the ViewPager, the RecyclerView Activity should be showing the View that the ViewPage was showing. When you call the ViewPager activity, use startActivityForResult and use the result to scroll the RecyclerView to the correct position.
Once that is working, the Activity Transition can be made to work. I know that you've given each View in your RecyclerView a different transitionName, right? When you bind the View, call setTransitionName and give it a repeatable name. Typically this is the image URL or cursor row ID or at worst some munged index like "image_" + index.
The next thing you need to do is set the SharedElementCallback for both the calling Activity (exit) and called activity (enter). In each, you're going to need to override the onMapSharedElements callback to remap the shared element.
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// assuming just one shared element, excuse the ugly code
sharedElements.put(names.get(0), mSharedElement);
}
Here, mSharedElement has been set in the onActivityReenter in the calling (RecyclerView) activity and in onCreate and prior to finishAfterTransition (onBackPressed?) in the called (ViewPager) activity.
The onActivityReenter gives new functionality specifically for this case. You can look at the results there before the called Activity completes.
I am trying to synchronize ListViews scroll positions in a ViewPager.
I have a ViewPager with Fragments as children. Each fragment contains a ListView. I created a ListViewScrollListener interface, with which I listen to each ListView scroll activity. When one ListView is scrolled by the user, the active fragments in the ViewPager listen to the scroll event and call the following method on their ListView:
listView.setSelectionFromTop(firstVisibleItem, pixelOffset);
Sadly, the scroll positions of the ListView elements in the Fragments do not update. When I put the same setSelectionFromTop method in the onResume() method, it works like a charm, hence the position is set correctly when I swype the ViewPager and the Fragments are being created.
I also checked out StackOverflow for similar problems and tried the following solution, without positive results:
mListView.post(new Runnable() {
#Override
public void run() {
listView.setSelectionFromTop(firstVisibleItem, pixelOffset);
}
});
I checked if the setSelectionFromTop is called correctly, and apparently it is. Any ideas why it's not working?
Update 1
As hinted by Ne0, I also checked to run the setSelectionFromTop method inside the Fragment on the main ui thread, without any positive results.
getActivity().runOnUiThread( new Runnable() {
#Override
public void run() {
listView.setSelectionFromTop(firstVisibleItem, pixelOffset);
}
});
Any other ideas what this could be?