How to know when the RecyclerView has finished laying down the items?

狂风中的少年 提交于 2019-11-26 22:36:14

问题


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


回答1:


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.
                            //Remove listener after changed RecyclerView's height to prevent infinite loop
                            recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        }
                    });



回答2:


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
                 //
             }
};



回答3:


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)
    // ...
}    



回答4:


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.




回答5:


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)




回答6:


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.




回答7:


// 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");
}



回答8:


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.




回答9:


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.



来源:https://stackoverflow.com/questions/30397460/how-to-know-when-the-recyclerview-has-finished-laying-down-the-items

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!