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.
Related
I'm populating a GridView wth an AsyncTask:
#Override
public void onLoad() {
super.onLoad();
//... set Loading view and actually get the data
adapter = new DepartmentGridAdapter(getActivity(), R.layout.gird_department_item_layout,
mSalesPresenter.getDepartments());
}
#Override
public void onDoneLoading() {
super.onDoneLoading();
gridView.setAdapter(adapter); // Populate the GridView
onShowTutorial(); // <--- This is where I need to get the firstChild.
}
After the AsyncTask is done, I just need to access the firstChild of the GridView:
#Override
public void onShowTutorial() {
if (gridView.getChildAt( gridView.getFirstVisiblePosition())!= null )
// ... doStuff
}
But the statement is Always null. I even call getCount for the GridView and it's not 0. The problem seems that the GridView childs are not accesible right away. If i Use a button to force the execution of the method onShowTutorial() after the UI is ready then I can access the first child.
I'm running out of ideas to trigger the method after the execution of the thread.
Any solution?
Try to post a runnable to be run when the gridView is done doing what it is currently doing (more info regarding View.post() here) :
#Override
public void onDoneLoading() {
super.onDoneLoading();
gridView.setAdapter(adapter); // Populate the GridView
// Tells the gridView to "queue" the following runnable
gridView.post(new Runnable() {
#Override
public void run() {
onShowTutorial(); // <--- This is where I need to get the firstChild.
}
});
}
Found it.
I used a ViewTreeObserver and it works:
gridView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
onShowTutorial();
}
});
In order to detect finish of app bar collapsing I called addOnOffsetChangedListener. In listener's onOffsetChanged I catch and handle the moment of collapsing done. Then I need to stop listen for offset changes.
In most examples here is call of removeOnOffsetChangedListener(this) from inside of onOffsetChanged. But looking in AppBarLayout.java I see:
private void dispatchOffsetUpdates(int offset) {
// Iterate backwards through the list so that most recently added listeners
// get the first chance to decide
if (mListeners != null) {
for (int i = 0, z = mListeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = mListeners.get(i);
if (listener != null) {
listener.onOffsetChanged(this, offset);
}
}
}
}
So if there is more than one listener installed, calling removeOnOffsetChangedListener(this) naturally results in IndexOutOfBoundsException.
Have I missed something? Is there safe way to 'unsubscribe' from listening for offset updates?
Interestingly, this wouldn't be a problem if their code actually did what the comment says. Anyway, we can defer the removal by putting the call to removeOnOffsetChangedListener() in a Runnable, and posting it to the AppBarLayout in onOffsetChanged(), instead of calling it directly there.
For example:
AppBarLayout.OnOffsetChangedListener listener = new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(final AppBarLayout abl, int offset) {
abl.post(new Runnable() {
#Override
public void run() {
abl.removeOnOffsetChangedListener(listener);
}
}
);
}
};
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()
}
So a few weeks ago i asked this question: recyclerview periodic ui child updates.
And today i want to refactor that funcionality using Rxjava. It's actually pretty simple, i accomplish the following way:
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (friend.getGameStatus().equals(GameStatus.INGAME)) {
holderOnline.startRepeatingTask();
} else {
holderOnline.stopRepeatingTask();
}
}
class MyViewHolderOnline extends RecyclerView.ViewHolder {
private Subscription subscribe;
public MyViewHolderOnline(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
public void startRepeatingTask() {
subscribe = Observable.interval(updateInterval, TimeUnit.MILLISECONDS)
.map(aLong -> current.getGameStatusToPrint())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<String>() {
#Override public void onCompleted() { }
#Override public void onError(Throwable e) { }
#Override
public void onNext(String gameStatusToPrint) {
gameStatus.setText(gameStatusToPrint);
}
});
}
void stopRepeatingTask() {
if(subscribe != null && !subscribe.isUnsubscribed())
subscribe.unsubscribe();
}
}
The problem however is different. I have a navigationDrawer implemented with Activities that are Paused and not Destroyed when switched. So, after i switch to the activity that don't contains this adapter, the observable keeps on sending stuff because its a periodic interval Observable. So my question is, what should i do? I need to unsubscribe when the activity is paused, but i have no idea how since to, and also how to subscribe back. Any help or ideas?
So, if I understand you correctly, one way to solve your problem is to answer the question: In an Activity that contains a RecyclerView, how do I get references to all the ViewHolders that are currently displayed/bound?
For example, to stop all the updates you could then do the following in the onPause() of your Activity:
// assuming you are using a LinearLayoutManager:
final int firstVisibleIndex = mLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleIndex = mLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleIndex; i <= lastVisibleIndex; i++) {
YourViewHolder viewHolder = (YourViewHolder) findViewHolderForAdapterPosition (i);
viewHolder.stopRepeatingTask();
}
And likewise, you could restart the tasks in the onResume of your Activity.
BUT: Now that I wrote this I am not sure whether there may be ViewHolders beyond the visible area of the RecyclerView (i. e. before the first or after the last visible item) that are already bound but that would not be reached with this method. If that turns out to be the case you can still iterate over all indices of the items in your adapter and just discard any null return values.
I would like a ScrollView to start all the way at the bottom. Any methods?
you should run the code inside the scroll.post like this:
scroll.post(new Runnable() {
#Override
public void run() {
scroll.fullScroll(View.FOCUS_DOWN);
}
});
scroll.fullScroll(View.FOCUS_DOWN) also should work.
Put this in a scroll.Post(Runnable run)
Kotlin Code
scrollView.post {
scrollView.fullScroll(View.FOCUS_DOWN)
}
scroll.fullScroll(View.FOCUS_DOWN) will lead to the change of focus. That will bring some strange behavior when there are more than one focusable views, e.g two EditText. There is another way for this question.
View lastChild = scrollLayout.getChildAt(scrollLayout.getChildCount() - 1);
int bottom = lastChild.getBottom() + scrollLayout.getPaddingBottom();
int sy = scrollLayout.getScrollY();
int sh = scrollLayout.getHeight();
int delta = bottom - (sy + sh);
scrollLayout.smoothScrollBy(0, delta);
This works well.
Kotlin Extension
fun ScrollView.scrollToBottom() {
val lastChild = getChildAt(childCount - 1)
val bottom = lastChild.bottom + paddingBottom
val delta = bottom - (scrollY+ height)
smoothScrollBy(0, delta)
}
Sometimes scrollView.post doesn't work
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
BUT if you use scrollView.postDelayed, it will definitely work
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
},1000);
What worked best for me is
scroll_view.post(new Runnable() {
#Override
public void run() {
// This method works but animates the scrolling
// which looks weird on first load
// scroll_view.fullScroll(View.FOCUS_DOWN);
// This method works even better because there are no animations.
scroll_view.scrollTo(0, scroll_view.getBottom());
}
});
I increment to work perfectly.
private void sendScroll(){
final Handler handler = new Handler();
new Thread(new Runnable() {
#Override
public void run() {
try {Thread.sleep(100);} catch (InterruptedException e) {}
handler.post(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(View.FOCUS_DOWN);
}
});
}
}).start();
}
Note
This answer is a workaround for really old versions of android. Today the postDelayed has no more that bug and you should use it.
i tried that successful.
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, scrollView.getHeight());
}
}, 1000);
Here is some other ways to scroll to bottom
fun ScrollView.scrollToBottom() {
// use this for scroll immediately
scrollTo(0, this.getChildAt(0).height)
// or this for smooth scroll
//smoothScrollBy(0, this.getChildAt(0).height)
// or this for **very** smooth scroll
//ObjectAnimator.ofInt(this, "scrollY", this.getChildAt(0).height).setDuration(2000).start()
}
Using
If you scrollview already laid out
my_scroll_view.scrollToBottom()
If your scrollview is not finish laid out (eg: you scroll to bottom in Activity onCreate method ...)
my_scroll_view.post {
my_scroll_view.scrollToBottom()
}
When the view is not loaded yet, you cannot scroll. You can do it 'later' with a post or sleep call as above, but this is not very elegant.
It is better to plan the scroll and do it on the next onLayout(). Example code here:
https://stackoverflow.com/a/10209457/1310343
One thing to consider is what NOT to set. Make certain your child controls, especially EditText controls, do not have the RequestFocus property set. This may be one of the last interpreted properties on the layout and it will override gravity settings on its parents (the layout or ScrollView).
Not exactly the answer to the question, but I needed to scroll down as soon as an EditText got the focus. However the accepted answer would make the ET also lose focus right away (to the ScrollView I assume).
My workaround was the following:
emailEt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus){
Toast.makeText(getActivity(), "got the focus", Toast.LENGTH_LONG).show();
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
}, 200);
}else {
Toast.makeText(getActivity(), "lost the focus", Toast.LENGTH_LONG).show();
}
}
});
I actually found that calling fullScroll twice does the trick:
myScrollView.fullScroll(View.FOCUS_DOWN);
myScrollView.post(new Runnable() {
#Override
public void run() {
myScrollView.fullScroll(View.FOCUS_DOWN);
}
});
It may have something to do with the activation of the post() method right after performing the first (unsuccessful) scroll. I think this behavior occurs after any previous method call on myScrollView, so you can try replacing the first fullScroll() method by anything else that may be relevant to you.
Using there is another cool way to do this with Kotlin coroutines. The advantage of using a coroutine opposed to a Handler with a runnable (post/postDelayed) is that it does not fire up an expensive thread to execute a delayed action.
launch(UI){
delay(300)
scrollView.fullScroll(View.FOCUS_DOWN)
}
It is important to specify the coroutine's HandlerContext as UI otherwise the delayed action might not be called from the UI thread.
In those case were using just scroll.scrollTo(0, sc.getBottom()) don't work, use it using scroll.post
Example:
scroll.post(new Runnable() {
#Override
public void run() {
scroll.fullScroll(View.FOCUS_DOWN);
}
});
One possible reason of why scroll.fullScroll(View.FOCUS_DOWN) might not work even wrapped in .post() is that the view is not laid out. In this case View.doOnLayout() could be a better option:
scroll.doOnLayout(){
scroll.fullScroll(View.FOCUS_DOWN)
}
Or, something more elaborated for the brave souls: https://chris.banes.dev/2019/12/03/suspending-views/
A combination of all answers did the trick for me:
Extension Function PostDelayed
private fun ScrollView.postDelayed(
time: Long = 325, // ms
block: ScrollView.() -> Unit
) {
postDelayed({block()}, time)
}
Extension Function measureScrollHeight
fun ScrollView.measureScrollHeight(): Int {
val lastChild = getChildAt(childCount - 1)
val bottom = lastChild.bottom + paddingBottom
val delta = bottom - (scrollY+ height)
return delta
}
Extension Function ScrolltoBottom
fun ScrollView.scrollToBottom() {
postDelayed {
smoothScrollBy(0, measureScrollHeight())
}
}
Be aware that the minimum delay should be at least 325ms or the scrolling will be buggy (not scrolling to the entire bottom). The larger your delta between the current height and the bottom is, the larger should be the delayed time.
Some people here said that scrollView.post didn't work.
If you don't want to use scrollView.postDelayed, another option is to use a listener. Here is what I did in another use case :
ViewTreeObserver.OnPreDrawListener viewVisibilityChanged = new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if (my_view.getVisibility() == View.VISIBLE) {
scroll_view.smoothScrollTo(0, scroll_view.getHeight());
}
return true;
}
};
You can add it to your view this way :
my_view.getViewTreeObserver().addOnPreDrawListener(viewVisibilityChanged);
If your minimum SDK is 29 or upper you could use this:
View childView = findViewById(R.id.your_view_id_in_the_scroll_view)
if(childView != null){
scrollview.post(() -> scrollview.scrollToDescendant(childView));
}
This works instantly. Without delay.
// wait for the scroll view to be laid out
scrollView.post(new Runnable() {
public void run() {
// then wait for the child of the scroll view (normally a LinearLayout) to be laid out
scrollView.getChildAt(0).post(new Runnable() {
public void run() {
// finally scroll without animation
scrollView.scrollTo(0, scrollView.getBottom());
}
}
}
}