Detect animation finish in Android's RecyclerView - android
The RecyclerView, unlike to ListView, doesn't have a simple way to set an empty view to it, so one has to manage it manually, making empty view visible in case of adapter's item count is 0.
Implementing this, at first I tried to call empty view logic right after modifying underlaying structure (ArrayList in my case), for example:
btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
devices.remove(0); // remove item from ArrayList
adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
updateEmptyView();
}
});
It does the thing, but has a drawback: when the last element is being removed, empty view appears before animation of removing is finished, immediately after removal. So I decided to wait until end of animation and then update UI.
To my surprise, I couldn't find a good way to listen for animation events in RecyclerView. First thing coming to mind is to use isRunning method like this:
btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
devices.remove(0); // remove item from ArrayList
adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
#Override
public void onAnimationsFinished() {
updateEmptyView();
}
});
}
});
Unfortunately, callback in this case runs immediately, because at that moment inner ItemAnimator still isn't in the "running" state. So, the questions are: how to properly use ItemAnimator.isRunning() method and is there a better way to achieve the desired result, i.e. show empty view after removal animation of the single element is finished?
Currently the only working way I've found to solve this problem is to extend ItemAnimator and pass it to RecyclerView like this:
recyclerView.setItemAnimator(new DefaultItemAnimator() {
#Override
public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
updateEmptyView();
}
});
But this technique is not universal, because I have to extend from concrete ItemAnimator implementation being used by RecyclerView. In case of private inner CoolItemAnimator inside CoolRecyclerView, my method will not work at all.
PS: My colleague suggested to wrap ItemAnimator inside the decorator in a following manner:
recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));
It would be nice, despite seems like overkill for a such trivial task, but creating the decorator in this case is not possible anyway, because ItemAnimator has a method setListener() which is package protected so I obviously can't wrap it, as well as several final methods.
I have a little bit more generic case where I want to detect when the recycler view have finished animating completely when one or many items are removed or added at the same time.
I've tried Roman Petrenko's answer, but it does not work in this case. The problem is that onAnimationFinished is called for each entry in the recycler view. Most entries have not changed so onAnimationFinished is called more or less instantaneous. But for additions and removals the animation takes a little while so there it's called later.
This leads to at least two problems. Assume you have a method called doStuff() that you want to run when the animation is done.
If you simply call doStuff() in onAnimationFinished you will call it once for every item in the recycler view which might not be what you want to do.
If you just call doStuff() the first time onAnimationFinished is called you may be calling this long before the last animation has been completed.
If you could know how many items there are to be animated you could make sure you call doStuff() when the last animation finishes. But I have not found any way of knowing how many remaining animations there are queued up.
My solution to this problem is to let the recycler view first start animating by using new Handler().post(), then set up a listener with isRunning() that is called when the animation is ready. After that it repeats the process until all views have been animated.
void changeAdapterData() {
// ...
// Changes are made to the data held by the adapter
recyclerView.getAdapter().notifyDataSetChanged();
// The recycler view have not started animating yet, so post a message to the
// message queue that will be run after the recycler view have started animating.
new Handler().post(waitForAnimationsToFinishRunnable);
}
private Runnable waitForAnimationsToFinishRunnable = new Runnable() {
#Override
public void run() {
waitForAnimationsToFinish();
}
};
// When the data in the recycler view is changed all views are animated. If the
// recycler view is animating, this method sets up a listener that is called when the
// current animation finishes. The listener will call this method again once the
// animation is done.
private void waitForAnimationsToFinish() {
if (recyclerView.isAnimating()) {
// The recycler view is still animating, try again when the animation has finished.
recyclerView.getItemAnimator().isRunning(animationFinishedListener);
return;
}
// The recycler view have animated all it's views
onRecyclerViewAnimationsFinished();
}
// Listener that is called whenever the recycler view have finished animating one view.
private RecyclerView.ItemAnimator.ItemAnimatorFinishedListener animationFinishedListener =
new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
#Override
public void onAnimationsFinished() {
// The current animation have finished and there is currently no animation running,
// but there might still be more items that will be animated after this method returns.
// Post a message to the message queue for checking if there are any more
// animations running.
new Handler().post(waitForAnimationsToFinishRunnable);
}
};
// The recycler view is done animating, it's now time to doStuff().
private void onRecyclerViewAnimationsFinished() {
doStuff();
}
What worked for me is the following:
detect that a view holder was removed
in this case, register a listener to be notified when dispatchAnimationsFinished() is called
when all animations are finished, call a listener to perform the task (updateEmptyView())
public class CompareItemAnimator extends DefaultItemAnimator implements RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
private OnItemAnimatorListener mOnItemAnimatorListener;
public interface OnItemAnimatorListener {
void onAnimationsFinishedOnItemRemoved();
}
#Override
public void onAnimationsFinished() {
if (mOnItemAnimatorListener != null) {
mOnItemAnimatorListener.onAnimationsFinishedOnItemRemoved();
}
}
public void setOnItemAnimatorListener(OnItemAnimatorListener onItemAnimatorListener) {
mOnItemAnimatorListener = onItemAnimatorListener;
}
#Override
public void onRemoveFinished(RecyclerView.ViewHolder viewHolder) {
isRunning(this);
}}
Here's a little Kotlin extension method that builds on the answer by nibarius.
fun RecyclerView.executeAfterAllAnimationsAreFinished(
callback: (RecyclerView) -> Unit
) = post(
object : Runnable {
override fun run() {
if (isAnimating) {
// itemAnimator is guaranteed to be non-null after isAnimating() returned true
itemAnimator!!.isRunning {
post(this)
}
} else {
callback(this#executeAfterAllAnimationsAreFinished)
}
}
}
)
Check from latest androidx.recyclerview:recyclerview:1.2.0 inside ItemAnimator method:
boolean isRunning(#Nullable ItemAnimatorFinishedListener listener)
Example (Kotlin):
recyclerView.itemAnimator?.isRunning {
// do whatever you need to
}
Extending Roman Petrenko's answer, if you are using androidx recycler view with kotlin, you can do something like that:
taskListRecycler.apply {
itemAnimator = object : DefaultItemAnimator() {
override fun onAddFinished(item: RecyclerView.ViewHolder?) {
super.onAddFinished(item)
//Extend
}
override fun onRemoveFinished(item: RecyclerView.ViewHolder?) {
super.onRemoveFinished(item)
//Extend
}
}
layoutManager = LinearLayoutManager(context)
adapter = taskListAdapter
}
There is a method in the ItemAnimator class that is called when all item animations are finished:
/**
* Method which returns whether there are any item animations currently running.
* This method can be used to determine whether to delay other actions until
* animations end.
*
* #return true if there are any item animations currently running, false otherwise.
*/
public abstract boolean isRunning();
You can override it to detect when all item animations have ended:
recyclerView.itemAnimator = object : DefaultItemAnimator() {
override fun isRunning(): Boolean {
val isAnimationRunning = super.isRunning()
if(!isAnimationRunning) {
// YOUR CODE
}
return isAnimationRunning
}
}
To expand on Roman Petrenko's answer, I don't have a truly universal answer either, but I did find the Factory pattern to be a helpful way to at least clean up some of the cruft that is this issue.
public class ItemAnimatorFactory {
public interface OnAnimationEndedCallback{
void onAnimationEnded();
}
public static RecyclerView.ItemAnimator getAnimationCallbackItemAnimator(OnAnimationEndedCallback callback){
return new FadeInAnimator() {
#Override
public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
callback.onAnimationEnded();
super.onAnimationEnded(viewHolder);
}
};
}
}
In my case, I'm using a library which provides a FadeInAnimator that I was already using. I use Roman's solution in the factory method to hook into the onAnimationEnded event, then pass the event back up the chain.
Then, when I'm configuring my recyclerview, I specify the callback to be my method for updating the view based on the recyclerview item count:
mRecyclerView.setItemAnimator(ItemAnimatorFactory.getAnimationCallbackItemAnimator(this::checkSize));
Again, it's not totally universal across all any and all ItemAnimators, but it at least "consolidates the cruft", so if you have multiple different item animators, you can just implement a factory method here following the same pattern, and then your recyclerview configuration is just specifying which ItemAnimator you want.
In my situation, I wanted to delete a bunch of items (and add new ones) after an animation ended. But the isAnimating Event is trigged for each holder, so #SqueezyMo's function wouldn't do the trick to execute my action simultaneously on all items. Thus, I implemented a Listener in my Animator with a method to check if the last animation was done.
Animator
class ClashAnimator(private val listener: Listener) : DefaultItemAnimator() {
internal var winAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
HashMap()
internal var exitAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
HashMap()
private var lastAddAnimatedItem = -2
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
return true
}
interface Listener {
fun dispatchRemoveAnimationEnded()
}
private fun dispatchChangeFinishedIfAllAnimationsEnded(holder: ClashAdapter.ViewHolder) {
if (winAnimationsMap.containsKey(holder) || exitAnimationsMap.containsKey(holder)) {
return
}
listener.dispatchRemoveAnimationEnded() //here I dispatch the Event to my Fragment
dispatchAnimationFinished(holder)
}
...
}
Fragment
class HomeFragment : androidx.fragment.app.Fragment(), Injectable, ClashAdapter.Listener, ClashAnimator.Listener {
...
override fun dispatchRemoveAnimationEnded() {
mAdapter.removeClash() //will execute animateRemove
mAdapter.addPhotos(photos.subList(0,2), picDimens[1]) //will execute animateAdd
}
}
Note the action won't be called if there are no animations
fun RecyclerView.onDefaultAnimationFinished(action: () -> Unit, scope: CoroutineScope) {
var startedWaiting = false
fun waitForAllAnimations() {
if (!isAnimating) {
action()
return
}
scope.launch(Dispatchers.IO) {
delay(25)
}
scope.launch(Dispatchers.Main) {
waitForAllAnimations()
}
}
itemAnimator = object : DefaultItemAnimator() {
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
super.onAnimationFinished(viewHolder)
if (!startedWaiting)
waitForAllAnimations()
startedWaiting = true
}
}
}
In scenarios like these where the API is designed so poorly for something as trivial as this I just smartly brute-force it.
You can always just run a background task or Thread that periodically polls if the animator is running and when it's not running, execute the code.
If you're a fan of RxJava, you can use this extension function I made:
/**
* Executes the code provided by [onNext] once as soon as the provided [predicate] is true.
* All this is done on a background thread and notified on the main thread just like
* [androidObservable].
*/
inline fun <reified T> T.doInBackgroundOnceWhen(
crossinline predicate: (T) -> Boolean,
period: Number = 100,
timeUnit: java.util.concurrent.TimeUnit =
java.util.concurrent.TimeUnit.MILLISECONDS,
crossinline onNext: T.() -> Unit): Disposable {
var done = false
return Observable.interval(period.toLong(), timeUnit, Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.computation())
.takeWhile { !done }
.subscribe {
if (predicate(this)) {
onNext(this)
done = true
}
}
}
In your case you can just do:
recyclerView.doInBackgroundOnceWhen(
predicate = { adapter.isEmpty && !recyclerView.itemAnimator.isRunning },
period = 17, timeUnit = TimeUnit.MILLISECONDS) {
updateEmptyView()
}
What this does is it checks if the predicate is satisfied every 17 milliseconds, and if so will execute the onNext block. (17 millis for 60fps)
This is computationally expensive and inefficient... but it gets the job done.
My current preferred way of doing these things is by making use of Android's native Choreographer which allows you to execute callbacks on the next frame, whenever that may be.
Using Android Choreographer:
/**
* Uses [Choreographer] to evaluate the [predicate] every frame, if true will execute [onNextFrame]
* once and discard the callback.
*
* This runs on the main thread!
*/
inline fun doOnceChoreographed(crossinline predicate: (frameTimeNanos: Long) -> Boolean,
crossinline onNextFrame: (frameTimeNanos: Long) -> Unit) {
var callback: (Long) -> Unit = {}
callback = {
if (predicate(it)) {
onNextFrame(it)
Choreographer.getInstance().removeFrameCallback(callback)
callback = {}
} else Choreographer.getInstance().postFrameCallback(callback)
}
Choreographer.getInstance().postFrameCallback(callback)
}
A word of warning, this is executed on the main thread unlike with the RxJava implementation.
You can then easily call it like so:
doOnceChoreographed(predicate = { adapter.isEmpty && !recyclerView.itemAnimator.isRunning }) {
updateEmptyView()
}
Related
Android SwipeRefreshLayout Spinner hides before completion
I have managed to implement the SwipeRefreshLayout in my project, and it works as expected, executing the callback code, displaying the spinner, and reloading the relevant data. And it works time and again without issue. However, the problem is, whenever I perform the pull-down gesture, the spinner is pulled down correctly, turning clockwise in sync with the pull gesture, but it immediately jumps back to the top once the touch is released, even before the callback process has completed. This is how it's been implemented: tableRefresh = SwipeRefreshLayout(this) tableRefresh?.setOnRefreshListener { tableRefresh?.isRefreshing = true swipeRefreshHandler() } // global Intent var cloudService: Intent? = null private fun swipeRefreshHandler() { // starts a server-update service startService(cloudService?.setAction(serverManualSync)) } cloudServiceBroadcastReceiver = object: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { // server-update service notifies of completion serverManualSyncComplete -> tableRefresh?.isRefreshing = false // other cases } } } Again, there is no problem with the functionality per se, just that the spinner does not remain visible for the duration of the callback. And it works on every consecutive pull gesture. What could I be doing wrong?
This is because you are setting the SwipeRefreshLayout to stop refreshing immediately. The swipeRefreshHandler() method is called directly. Instead, you should listen to when the callback process is completed then set the isRefreshing to false. Could you show the relevant callback code? Perhaps it is running asynchronously?
Your code could look something like this: tableRefresh = SwipeRefreshLayout(this) tableRefresh?.setOnRefreshListener { swipeRefreshHandler() } private fun swipeRefreshHandler(loadingState: Boolean = true) { if(loadingState){ // // code to reload the data... // } sideMenuRefresh?.isRefreshing = loadingState } then in you async call (using coroutines or retrofit for example), you can call swipeRefreshHandler() // loading state for ongoing operation OR swipeRefreshHandler(false) // when data or process completed (success/fail)
How to wait for list of Maybe items to complete in RxJava2?
Apologies in advance if I lack a basic understanding of how to use RxJava2, because this seems to me something that should be quite fundamental. I've wracked my brains with unsuccessful Google searches, so welcome any resource recommendations. I've opted to use a 'sanitized' representation of my workaround code for the sake of clarity. Problem description I have an RxJava2 function asyncCallForList() that returns a Maybe<Arraylist<CustomClass>>. Each CustomClass object in this list only has a few basic fields populated (e.g. the source database only contains a unique identifier and a title string for each item). The full data required for each item is in another database location, which is retrieved using another function asyncCallForItem(uid), which returns a Maybe<CustomClass> based on the unique identifier, where the encapsulated CustomClass has all the required data. This function is to be called for each item in the list returned by asyncCallForList(). The desired functionality is to update my UI once all the objects in the list have been populated. Workaround #1 It is easy enough to loop through the resulting array list in the doOnSuccess() attached to the initial Maybe<Arraylist<CustomClass>>, then update my UI in the doOnSuccess() on the Maybe<CustomClass> returned by the subsequent asynchronous calls. This is not an acceptable workaround as there will be an unknown number of UI updates being made (the initial list returned could have any amount of items) and will hurt performance. Workaround #2 This gets the desired outcome but feels like the wrong way to go about it - I suspect there is a more elegant RxJava2 solution. Basically, I create a custom Observable in which loop through the items in the list and get the full data for each. However, rather than update the UI each time I populate a CustomClass item, I increase a counter, then check if the counter exceeds or equals the initial list size. When this condition is met I call the onComplete() method for the observable's emitter and update the UI there. private void fetchRemoteDataAndUpdateUi() { //Counter reset to zero before any asynchronous calls are made. int count = 0; Maybe<ArrayList<CustomClass>> itemList = asyncCallForList(); Consumer<ArrayList<CustomClass>> onListReturnedSuccess; onListReturnedSuccess = new Consumer<ArrayList<CustomClass >>() { #Override public void accept(ArrayList<CustomClass> list) throws Exception { //Custom observable created here, in which the resulting array list is processed. listObservable = Observable.create(new ObservableOnSubscribe<CustomClass>() { #Override public void subscribe(final ObservableEmitter<CustomClass> e) throws Exception { for (CustomClass customClass : list) { final CustomClass thisCustomClass = customClass; //Call to get full data on list item called here. asyncCallForItem(customClass.getUid()) .doOnSuccess(new Consumer<CustomClass>() { #Override public void accept(CustomClass customClass) throws Exception { thisCustomClass.update(customClass); e.onNext(thisCustomClass); count++; if (count >= list.size()) { e.onComplete(); } } }).subscribe(); } } }); listObservable .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Observer<CustomClass>() { #Override public void onSubscribe(Disposable d) { } #Override public void onNext(CustomClass customClass) { //Here I add the populated CustomClass object to an ArrayList field that is utilised by the UI. listForUi.add(customClass); } #Override public void onError(Throwable e) { } #Override public void onComplete() { //Here the UI is finally updated once all CustomClass objects have been populated. updateUi(); } }); } }; //Kick everything off. itemList.doOnSuccess(onListReturnedSuccess).subscribe(); }
flatMap it! asyncCallForList() .subscribeOn(Schedulers.io()) .flatMapSingle(list -> Flowable.fromIterable(list) .flatMapMaybe(item -> asyncCallForItem(item.id) .subscribeOn(Schedulers.io()) .doOnSuccess(response -> { // copy state from the original item response.text = item.text; }) , 1) // number of concurrent item calls .toList() ) .observeOn(AndroidSchedulers.mainThread()) .subscribe(successList -> { /* update UI */ }, error -> { /* report error */ });
Notify when all onCreateViewHolder inside a RecylerView have finished
RecyclerView calls onCreateViewHolder a bunch of times and then just keeps binding the data to these views. My view creation is slightly expensive and hence I need to defer rest of the UI tasks until my RecyclerView is done creating all the views. I tried adding a ViewTreeObserver.OnGlobalLayoutListener but this callback gets called before even the first onCreateViewHolder() call. Any idea how do I go about it?
After some research I've found out a solution with Handler. As you I'm looking for a beautiful code and this is a bit messy for me. But works perfectly anyway. Handler is a class that you can use in a way to post message and/or Runnable, which will be added in a queue, then executed when that queue is finished. My plan is, given that the adapter works on the UI, (inflate ect...) the creation and initialization (all onCreateViewHolder and onBindViewHolder) are added at a moment in the handler of the main thread. That means that if you post a message in the main thread queue (the same obligatory used by your adapter), then the message will be executed after any previous request (after your adapted has finished to initialize everything). Exemple : Main activity Initialization of the handler : private Handler mHandler; #Override protected void onCreate(Bundle iSavedInstanceState) { ... mHandler = new Handler(Looper.getMainLooper()); } Initialization of your CustomAdapter : private void initializeAdapter(...) { MyCustomAdapter lMyNewAdapter = new MyCustomAdapter(...) ... lNewAdapter.SetOnFirstViewHolderCreation(new MyCustomAdapter.OnFirstViewHolderCreation { #Override public void onCreation() { mHandler.post(new Runnable() { #Override public void run() { // Finally here, the code you want to execute // At the end of any Create and Bind VH of your // Adapter } }); } }); } MyCustomAdapter private boolean mIsViewHolderCreationStarted; private OnFirstViewHolderCreation mOnFirstViewHolderCreation; public CustomItemViewAdapter onCreateViewHolder( #NonNull ViewGroup iViewGroup, int iI) { ... if (!mIsViewHolderCreationStarted) { mIsViewHolderCreationStarted = true; if (mOnFirstViewHolderCreation != null) { // It's at this point that we want to add a new request // in the handler. When we're sure the request of the // adapter has begun. mOnFirstViewHolderCreation.onCreation(); } } } public void setOnFirstViewHolderCreation(OnFirstViewHolderCreation iAction) { mOnFirstViewHolderCreation = iAction; } public interface OnFirstViewHolderCreation { void onCreation(); } Note Be aware that this solution will execute a code at the end of the first initialization of the enteer page that it is possible to show in a case of a RecyclerView. A onCreateViewHolder might be called in case the screen is scrolled. Which means that this solution does not guarantee you this handler message is executed after all possible onCreateViewHolder. It only helps you to avoid an overload on the MainThread, during the greedy work of the adapter init. Something else, in case you're using animations with your adapter to make it appears smoothly or something else (one of the good reasons to use this way to do), don't forget to put your RecyclerView in VISIBLE and not GONE, otherwise, the initialization of the adapter never happens.
Android Espresso is waiting rxjava .delay from observable
I have a server call made with Retrofit + RxJava that i want to test its behaviour on screen. The goal is to have a loading image set before the call be performed and, after getting the results, hide the loading image and show the data. I tried to setup the mock using the "delay" method from the Observable class, so Espresso can find the image. That's the code i used: Observable<AccountDetails> observable = Observable.just(details) .delay(5, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()); doReturn(observable).when(mScope).requestAccounts(); performServerCall(); onView(withId(R.id.panel_loading)).check(matches(isDisplayed())); After running the test, i realized that the Espresso is actually waiting the delay set on Observable, before actually executing the check (isDisplayed). That way it will only check after the info is loaded and the loading image is gone. Is that the normal behaviour for RxJava/Espresso? Is there a better way achieve this?
There must be an animation in the R.id.panel_loading that is being executed. When there is an animation in the UI thread espresso waits until it is finished. I had the same problem and I did a ViewAction to disable the animations for my custom loadings, here is the code: public static ViewAction disableAnimations() { return new ViewAction() { #Override public Matcher<View> getConstraints() { return isAssignableFrom(CustomLoading.class); } #Override public String getDescription() { return "Disable animations"; } #Override public void perform(UiController uiController, View view) { CustomLoading loading = (CustomLoading) view; loading.setAnimations(false); } }; } and I call it this way before pressing the button that shows the loading and so the tests do not wait: onView(withId(R.id.panel_loading)).perform(disableAnimations()); If the panel_loading is not the one that makes the animation something else must be. Hope this helps.
How to know when the RecyclerView has finished laying down the items?
I have a RecyclerView that is inside a CardView. The CardView has a height of 500dp, but I want to shorten this height if the RecyclerView is smaller. So I wonder if there is any listener that is called when the RecyclerView has finished laying down its items for the first time, making it possible to set the RecyclerView's height to the CardView's height (if smaller than 500dp).
I also needed to execute code after my recycler view finished inflating all elements. I tried checking in onBindViewHolder in my Adapter, if the position was the last, and then notified the observer. But at that point, the recycler view still was not fully populated. As RecyclerView implements ViewGroup, this anwser was very helpful. You simply need to add an OnGlobalLayoutListener to the recyclerView: View recyclerView = findViewById(R.id.myView); recyclerView .getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { #Override public void onGlobalLayout() { // At this point the layout is complete and the // dimensions of recyclerView and any child views // are known. recyclerView .getViewTreeObserver() .removeOnGlobalLayoutListener(this); } });
Working modification of #andrino anwser. As #Juancho pointed in comment above. This method is called several times. In this case we want it to be triggered only once. Create custom listener with instance e.g private RecyclerViewReadyCallback recyclerViewReadyCallback; public interface RecyclerViewReadyCallback { void onLayoutReady(); } Then set OnGlobalLayoutListener on your RecyclerView recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { #Override public void onGlobalLayout() { if (recyclerViewReadyCallback != null) { recyclerViewReadyCallback.onLayoutReady(); } recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); after that you only need implement custom listener with your code recyclerViewReadyCallback = new RecyclerViewReadyCallback() { #Override public void onLayoutReady() { // //here comes your code that will be executed after all items are laid down // } };
If you use Kotlin, then there is a more compact solution. Sample from here. This layout listener is usually used to do something after a View is measured, so you typically would need to wait until width and height are greater than 0. ... it can be used by any object that extends View and also be able to access to all its specific functions and properties from the listener. // define 'afterMeasured' layout listener: inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) { viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { if (measuredWidth > 0 && measuredHeight > 0) { viewTreeObserver.removeOnGlobalLayoutListener(this) f() } } }) } // using 'afterMeasured' handler: myRecycler.afterMeasured { // do the scroll (you can use the RecyclerView functions and properties directly) // ... }
The best way that I found to know when has finished laying down the items was using the LinearLayoutManager. For example: private RecyclerView recyclerView; ... recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false){ #Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); // TODO } ); ...
I improved the answer of android developer to fix this problem. It's a Kotlin code but should be simple to understand even if you know only Java. I wrote a subclass of LinearLayoutManager which lets you listen to the onLayoutCompleted() event: /** * This class calls [mCallback] (instance of [OnLayoutCompleteCallback]) when all layout * calculations are complete, e.g. following a call to * [RecyclerView.Adapter.notifyDataSetChanged()] (or related methods). * * In a paginated listing, we will decide if load more needs to be called in the said callback. */ class NotifyingLinearLayoutManager(context: Context) : LinearLayoutManager(context, VERTICAL, false) { var mCallback: OnLayoutCompleteCallback? = null override fun onLayoutCompleted(state: RecyclerView.State?) { super.onLayoutCompleted(state) mCallback?.onLayoutComplete() } fun isLastItemCompletelyVisible() = findLastCompletelyVisibleItemPosition() == itemCount - 1 interface OnLayoutCompleteCallback { fun onLayoutComplete() } } Now I set the mCallback like below: mLayoutManager.mCallback = object : NotifyingLinearLayoutManager.OnLayoutCompleteCallback { override fun onLayoutComplete() { // here we know that the view has been updated. // now you can execute your code here } } Note: what is different from the linked answer is that I use onLayoutComplete() which is only invoked once, as the docs say: void onLayoutCompleted (RecyclerView.State state) Called after a full layout calculation is finished. The layout calculation may include multiple onLayoutChildren(Recycler, State) calls due to animations or layout measurement but it will include only one onLayoutCompleted(State) call. This method will be called at the end of layout(int, int, int, int) call. This is a good place for the LayoutManager to do some cleanup like pending scroll position, saved state etc.
I tried this and it worked for me. Here is the Kotlin extension fun RecyclerView.runWhenReady(action: () -> Unit) { val globalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { action() viewTreeObserver.removeOnGlobalLayoutListener(this) } } viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener) } then call it myRecyclerView.runWhenReady { // Your action }
Also in same cases you can use RecyclerView.post() method to run your code after list/grid items are popped up. In my cases it was pretty enough.
I have been struggling with trying to remove OnGlobalLayoutListener once it gets triggered but that throws an IllegalStateException. Since what I need is to scroll my recyclerView to the second element what I did was to check if it already have children and if it is the first time this is true, only then I do the scroll: public class MyActivity extends BaseActivity implements BalanceView { ... private boolean firstTime = true; ... #Override protected void onCreate(Bundle savedInstanceState) { ... ViewTreeObserver vto = myRecyclerView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { #Override public void onGlobalLayout() { if (myRecyclerView.getChildCount() > 0 && MyActivity.this.firstTime){ MyActivity.this.firstTime = false; scrollToSecondPosition(); } } }); } ... private void scrollToSecondPosition() { // do the scroll } } HTH someone! (Of course, this was inspired on #andrino and #Phatee answers)
Here is an alternative way: You can load your recycler view in a thread. Like this First, create a TimerTask void threadAliveChecker(final Thread thread){ Timer timer = new Timer(); timer.schedule(new TimerTask() { #Override public void run() { if(!thread.isAlive()){ runOnUiThread(new Runnable() { #Override public void run() { // stop your progressbar here } }); } } },500,500); } Second, create a runnable Runnable myRunnable = new Runnable() { #Override public void run() { runOnUiThread(new Runnable() { #Override public void run() { // load recycler view from here // you can also toast here } }); } }; Third, create a thread Thread myThread = new Thread(myRunnable); threadAliveChecker(); // start showing progress bar according to your need (call a method) myThread.start(); Understanding the above code now: TimerTask - It will run and will check the thread (every 500 milliseconds) is running or completed. Runnable - runnable is just like a method, here you have written the code that is needed to be done in that thread. So our recycler view will be called from this runnable. Thread - Runnable will be called using this thread. So we have started this thread and when the recyclerView load (runnable code load) then this thread will be completed (will not live in programming words). So our timer is checking the thread is alive or not and when the thread.isAlive is false then we will remove the progress Bar.
If you are using the android-ktx library and if you need to perform an action after positioning all elements of the Activity, you can use this method: // define 'afterMeasured' Activity listener: fun Activity.afterMeasured(f: () -> Unit) { window.decorView.findViewById<View>(android.R.id.content).doOnNextLayout { f() } } // in Activity: override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(...) afterMeasured { // do something here } }
This is how I did it recyclerView.setLayoutManager(new LinearLayoutManager(this){ #Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); //code to run after loading recyclerview new GuideView.Builder(MainActivity.this) .setTargetView(targetView) .setGravity(Gravity.auto) .setDismissType(DismissType.outside) .setContentTextSize(18) .build() .show(); } }); I wish this will help you.
You can use with this approach if ((adapterPosition + 1) == mHistoryResponse.size) { Log.d("debug", "process done") } get the adapterPosition with plus 1 and check it with your data classes size, if it has same size, the process is practically complete.
For those that are not using Kotlin and are still struggling, I took a fast look at the doOnNextLayout(crossinline action: (view: T) -> Unit) solution they implemented, and it is pretty simple. IF you are NOT working with a custom RecyclerView (CustomRecyclerView extends RecyclerView), you may want to rethink it as this will bring a lot of benefits you may want to add in the future (smooth scroll to position, vertical dividers, etc..) Inside the CustomRecyclerView.class public void doOnNextLayout(Consumer<List<View>> onChange) { addOnLayoutChangeListener( new OnLayoutChangeListener() { #Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { onChange.accept(getChildren()); removeOnLayoutChangeListener(this); } } ); } The getChildren() method is building a List of size getChildCount(); and a add(getChild(i)) on each iteration. Now... One important aspect about the code is this: removeOnLayoutChangeListener(this); This means that the devs are asking for you to execute this before each list submission to the adapter. In theory we could only place the listener ONCE upon RecyclerView creation (which IMO would be cheaper/better) + because we are retrieving the views, we could retrieve their respective binds with DataBindingUtils. and get whatever data the adapter gave the view onBind via their DataBind. To do this tho it requires more code. First the adapter needs to be aware of the Fragment they inhabit, OR the RecyclerView::setAdapter needs to provide a ViewLifeCyclerOwner, a third easier option is to provide the adapter with a onViewDestroy() method, and execute it on Fragment's onDestroyView() method. #Override public void onDestroyView() { super.onDestroyView(); adater.onViewDestroyed(); } by overriding the onAttachedToRecyclerView, we are able to attach them as observers. private final List<Runnable> submitter = new ArrayList<>(); #Override public void onAttachedToRecyclerView(#NonNull RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); if (recyclerView instanceof CustomRecyclerView) { submitter.add(((CustomRecyclerView) recyclerView)::onSubmit); } } Where the onSubmit method on the CustomRecyclerView side will provide a boolean that will tell the recyclerView whether a list is being submitted. private boolean submitting; public void doOnNextLayout(Consumer<List<View>> onChange) { addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (submitting) { onChange.accept(getChildren()); submitting = false; } } ); } public void onSubmit() { submitting = true; } Each Runnable will be executed at the moment of list submission: In the case of the ListAdapter there are 2 possible entry points: private void notifyRVs() { for (Runnable r:submitter ) { r.run(); } } #Override public void submitList(#Nullable List<X> list, #Nullable Runnable commitCallback) { notifyRVs(); super.submitList(list, commitCallback); } #Override public void submitList(#Nullable List<X> list) { notifyRVs(); super.submitList(list); } Now to prevent memory leaks we must clear the List of Runnables on ViewDestroyed() inside the Adapter... public void onViewDestroyed() { submitter.clear(); } Now because the functionality of the method changed we should rename it, and decouple the Consumer<List> from the LayoutChangeListener() private Consumer<List<View>> onChange = views -> {}; public void setOnListSubmitted(Consumer<List<View>> onChange) { this.onChange = onChange; } public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) { super(context, attrs); //Read attributes setOnListSubmissionListener(); } private void setOnListSubmissionListener() { addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (submitting) { onChange.accept(getChildren()); submitting = false; } } ); }
What worked for me was to add the listener after setting the RecyclerView adapter. ServerRequest serverRequest = new ServerRequest(this); serverRequest.fetchAnswersInBackground(question_id, new AnswersDataCallBack() { #Override public void done(ArrayList<AnswerObject> returnedListOfAnswers) { mAdapter = new ForumAnswerRecyclerViewAdapter(returnedListOfAnswers, ForumAnswerActivity.this); recyclerView.setAdapter(mAdapter); recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { #Override public void onGlobalLayout() { progressDialog.dismiss(); } }); } }); This dismisses the "progressDialog" after the global layout state or the visibility of views within the view tree changes.
// Another way // Get the values Maybe<List<itemClass>> getItemClass(){ return /* */ } // Create a listener void getAll(DisposableMaybeObserver<List<itemClass>> dmo) { getItemClass().subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(dmo); } // In the code where you want to track the end of loading in recyclerView: DisposableMaybeObserver<List<itemClass>> mSubscriber = new DisposableMaybeObserver<List<itemClass>>() { #Override public void onSuccess(List<itemClass> item_list) { adapter.setWords(item_list); adapter.notifyDataSetChanged(); Log.d("RECYCLER", "DONE"); } #Override public void onError(Throwable e) { Log.d("RECYCLER", "ERROR " + e.getMessage()); } #Override public void onComplete() { Log.d("RECYCLER", "COMPLETE"); } }; void getAll(mSubscriber); //and #Override public void onDestroy() { super.onDestroy(); mSubscriber.dispose(); Log.d("RECYCLER","onDestroy"); }
recyclerView.getChildAt(recyclerView.getChildCount() - 1).postDelayed(new Runnable() { #Override public void run() { //do something } }, 300); RecyclerView only lays down specific number of items at a time, we can get the number by calling getChildCount(). Next, we need to get the last item by calling getChildAt (int index). The index is getChildCount() - 1. I'm inspired by this person answer and I can't find his post again. He said it's important to use postDelayed() instead of regular post() if you want to do something to the last item. I think it's to avoid NullPointerException. 300 is delayed time in ms. You can change it to 50 like that person did.