i am porting my adapter into RecyclerView.Adapter
what i want to achieve:
when the user scrolls down near the end i want to start fetch data, i also want to add i ProgressBar view at the end to let the user know more data is coming.
the way i implemented this in my BaseAdapter: on getView in the view requested in near the end, i would start fetching more data, call notifyDataSetChanged (to get the ProgressBar view to show) and only then return the view needed forgetView.
what i tried doing in RecyclerView.Adapter: i tried to do the same thing basically, this time in the method onBindViewHolder,
but if i try and call notifyItemInserted inside this method i get the following exception:
IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
what i tried: i noticed that onBindViewHolder gets called from onLayoutChildren from LayoutManager, i tried overriding it and calling notifyItemInserted after its super but i got the same exception
how can i achieve my goal?
Handler handler = new Handler();
final Runnable r = new Runnable() {
public void run() {
adapter.notifyDataSetChanged();
}
};
handler.post(r);
My sample code where I call adapter.notifyDataSetChanged(); from Activity
It's also worth noting the JavaDoc on the RecyclerView.isComputingLayout() method that triggers this exception :
/**
* Returns whether RecyclerView is currently computing a layout.
* <p>
* If this method returns true, it means that RecyclerView is in a lockdown state and any
* attempt to update adapter contents will result in an exception because adapter contents
* cannot be changed while RecyclerView is trying to compute the layout.
* <p>
* It is very unlikely that your code will be running during this state as it is
* called by the framework when a layout traversal happens or RecyclerView starts to scroll
* in response to system events (touch, accessibility etc).
* <p>
* This case may happen if you have some custom logic to change adapter contents in
* response to a View callback (e.g. focus change callback) which might be triggered during a
* layout calculation. In these cases, you should just postpone the change using a Handler or a
* similar mechanism.
*
* #return <code>true</code> if RecyclerView is currently computing a layout, <code>false</code>
* otherwise
*/
public boolean isComputingLayout() {
return mLayoutOrScrollCounter > 0;
}
The salient phrase being :
In these cases, you should just postpone the change using a Handler or a
similar mechanism.
In my case, I was modifying the adapter contents from another thread, which was (occasionally) causing a collision. Shifting the update to a Handler on the UI thread naturally solves this.
Using a Handler for adding items and calling notify...() from this Handler fixed the issue for me.
Neat and easy way:
recyclerView.post(new Runnable() {
#Override
public void run() {
adapter.notifyDataSetChanged();
}
});
Explanation: You use your RecyclerView instance and inside the post method a new Runnable added to the message queue. The runnable will be run on the user interface thread. This is a limit for Android to access the UI thread from background (e.g. inside a method which will be run in a background thread).
Try this
if (!recyclerView.isComputingLayout()) {
recyclerView.notifyDataSetChanged();
}
I use this:
Handler handler = new Handler();
private void notifyItemInsertedEx(final int position) {
try {
notifyItemInserted(position);
} catch (Exception ex) {
handler.post(new Runnable(){
public void run(){
notifyItemInserted(position);
}
});
}
}
Related
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.
My situation is the following: I have a RecyclerView in which I want to insert data.
I add data one by one until the RecyclerView is full. The data comes from a web service.
This is the code I use:
#Override
public void receive(Response response) {
_adapter.add(response.getData());
new Handler().post(new Runnable() {
#Override
public void run() {
fetchIfNotFull();
}
});
}
private void fetchIfNotFull() {
if (_layoutManager.findLastVisibleItemPosition() == _layoutManager.getItemCount() - 1)
fetchData(); // this will call receive(Response) when it's done
}
The problem is that, when I run the application, the RecyclerView is not filled, like I expect (but sometimes it does!).
I found out that _layoutManager.findLastVisibleItemPosition() does not always return the correct value (the one I expect at least), whereas _layoutManager.getItemCount() does, so no more data are fetched...
I thought that wrapping the call inside the Handler would help, so it would be called after the next layout update, but it didn't do the trick.
And here is the strange thing: If I call handler.postDelayed() with 1000 milliseconds, it works fine! (I didn't try other values), because the layout was updated after that time. But I don't like this solution (hack). Is there a way to make sure that the LayoutManager has been updated?
After the line
_adapter.add(response.getData());
add this one
_adapter.notifyItemChanged(_adapter.getItemCount() - 1);
long time watcher, first time writer :P
I got this problem:
I can't seem to change anything that has to do with the layout of android from my playSoundThread.
In this example, I use EventListeners. I already tried the simple way. I passed the ScrollView through, so that the thread can change it. But when it's happening, the thread stops immediately. And even when I use EventListeners, the same Problem occurs.
Changing a variable and posting log information works fine, but not layout Objects.
The first thing is, that I want to scroll a HorizontalScrollView from out the Thread's run() method.
the second case is, that, if the thread comes to it's end, I wanna fire an "i'm finished"-Event and change the image and function of an ImageButton
Here's the run()-method of the thread
public void run() {
if(this.playbackPosition < rhythm.tracks.get(0).sounds.size()) {
for (Track t : rhythm.tracks) {
if (t.sounds.get(this.playbackPosition).equals("1")) {
this.sp.play(t.SoundID, 1, 1, 1, 0, 1);
}
}
this.playbackPosition++;
if ( this.playbackPosition >= (this.scrollIndex*(192/this.zoom)) ){
this.scrollIndex++;
//Here I wanna fire the "Scroll" event
for(ScrollListener sl : scrollListeners){
sl.update(scrollPositions[scrollIndex]);
}
}
}
//This is the point where the playback is finished and the event to change a button is fired
else {
tmpListener.update();
}
}
}
The declaration of the OnPlaybackFinishedListener can be found in the class Player, which is the parent of the PlaySoundThread:
public void addOnPlaybackFinishedListener(){
tmpListener = new OnPlaybackFinishedListener() {
#Override
public void update() {
scheduledExecutorService.shutdown();
//this is a seconds Listener, which was implemented to test, if the problem still occurs with a little listener chain
shutdownListener.update();
}
};
}
public void addShutdownListener(OnExecutorShutdown sl){
this.shutdownListener = sl;
}
And here's the part of the MainActivity which is the parent class of Player and adds the shutdown listener and the ScrollListener:
awesomePlayer.addScrollListener(new ScrollListener(){
public void update(int position){
Log.i("ScrollListener update()","Running ScrollTo( "+position+", "+VIEW_rhythmscroll.getScrollY()+")");
VIEW_rhythmscroll.scrollTo(position, VIEW_rhythmscroll.getScrollY());
}
});
awesomePlayer.addOnPlaybackFinishedListener();
awesomePlayer.addShutdownListener(new OnExecutorShutdown() {
#Override
public void update() {
// TODO Auto-generated method stub
//This method changes the Pause Button to a Play Button with a new OnClickListener and a new Picture
BUTTON_STOP.performClick();
}
});
Can anyone help? Is there another way to avoid this problem? I'm developing on Android 2.2
Is it even possible to access UI elements from a thread?
Thanks in advance :)
You can't modify UI elements from a seperate thread, UI elements have to be modified from the main, UI Thread. There are a lot of topics on this, but you can update the UI by using an AsyncTask's onPostExecute(), onPreExecute(), or onProgressUpdate() methods, the Activity class's runOnUiThread(Runnable action), or by sending a Message to a Handler.
The problem I have is that listView.getLastVisiblePosition always returns -1 so I can't hide the searchView. I check this right after setting the adapter and anywhere I have tried to put this it still returns -1. I didn't see in the Docs why this would be but I imagine it would return -1 if the ListView is not showing any items. However, listView.getFirstVisiblePosition() returns 0 always, even when there is more than one item showing.
I have tried both methods Here but it doesn't make a difference when getting the wrong value.
#SuppressLint("NewApi") private void setFilters(String curType, Object curFilter)
{
// initialize several lists
itemsAdapter = new ArrayAdapter<Rowdata>(this, R.layout.list_item_text, foodItems);
listView.setAdapter(itemsAdapter);
int numItems = listView.getLastVisiblePosition() - listView.getFirstVisiblePosition();
if (numItems > foodItems.length)
{ searchField.setVisibility(View.GONE); }
else
{ searchField.setVisibility(View.VISIBLE); }
}
This method is called any time a Button is pressed or text is changed that can filter through the list. So the question is why would listView.getLastVisiblePosition() always return -1 and why would listView.getFirstVisiblePosition() always return 0? No errors/exceptions, everything runs fine except for not getting the expected results. Note: itemsAdapter.getCount() returns the correct value.
Also, I have to support API >=10
Edit
If anyone needs clarification, let me know. But basically, I have an EditText I use to search through the list. I want to hide this when there aren't more items in the list than what fit on the screen. listView.getLastVisiblePosition() always returns -1
I would really like to know the cause of the original problem but if anyone has any better way of hiding the search box when items all fit on the screen, I am open to suggestions.
Update
I put a breakpoint in onItemClick() and there I get the correct values for getFirstVisiblePosition(), getLastVisiblePosition(), and listView.getChildCount(). Before this, I get 0, -1, and null respectively.
What you need to do is roughly
listview.post(new Runnable() {
public void run() {
listview.getLastVisiblePosition();
}
});
Why this way and not directly?
Android apps run in a big event loop known as the UI / main thread. Everything that is executed in there is the result of some event. For example when your Activity needs to be created that's some sort of Activity creation event. The loop will execute code that handles this event and will for example once your are considered "created" call the onCreate method. It might call more than one method within the same iteration but that's not really important.
When you setup things like the UI in any of those onSomething methods nothing is actually drawn directly. All you do is set some state variables like a new Adapter. Once you return from those on methods the system gains back control and will check what it needs to do next.
The system will for example check if the window needs to be redrawn and if so will enqueue a redraw event in the event queue which is at a later point executed by the loop. If nothing needs to be drawn it's just idle and will wait for example for touch events that are enqueued for that loop as well.
Back to your problem: By calling .setAdapter() you essentially reset all states of the ListView. And since actual updates of the ListView will only happen after you hand control back to the system you will get nothing useful out of .getLastVisiblePosition().
What needs to happen before is that ListView is instructed to be redrawn or to measure it's new size, count the amount of items it has and so on. Once it has done that it will be able to give you the required information.
.post(Runnable r) simply enqueues a Runnable into the eventqueue which is then executed by the loop once it's first in the queue.
a Runnable does not require a Thread, it's just a regular Object with a method named run() and the contract of a Runnable is simply that something (which often happens to be a Thread) can call the run() method to execute whatever you want to run. Magical loop does that.
Result of you posting a runnable is looks inn pseudo code somewhat like this:
void loop() {
yourActivity.onSomething() { loop.enqueue(runnable) }
ListView.redraw() // |
runnable.run() // <--+
}
My suggestion to resolve this problem will not be professional or light weight.
I am suggesting that you should get count of all views in listView and check every one of them are they visible.
example:
private int getIndexOfLastVisibleView(ListView view){
int count = view.getChildCount()-1;
for(int i = count ; i>=0 ; i--){
View checkedView = view.getChildAt(i);
if(checkedView.isShown()){
return i;
}
}
return -1;
}
May not be perfect but I hope that it will work.
You can refer to my answer here Strange problem with broadcast receiver in Android not exactly the same but you can get the idea why your code not working.
To make it more clear, when you set the adapter to the ListView, nothing has been drawn yet and the method getLastVisiblePosition() can only return the correct value after the listview finish drawing all of it's visible children and know which one is the last visible one.
So, the most appropriate approach I can suggest here is trigger a callback after the listView finished drawing and we get the correct value then.
The ListView with listener after drawing:
static class MyListView extends ListView {
private OnDrawCompletedListener mOnDrawCompletedListener;
public MyListView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mOnDrawCompletedListener != null) {
mOnDrawCompletedListener.onDrawCompleted();
}
}
public void setOnDrawCompletedListener(OnDrawCompletedListener listener) {
mOnDrawCompletedListener = listener;
}
public static interface OnDrawCompletedListener {
public void onDrawCompleted();
}
}
The sample code for getting last visible position
mListView.setAdapter(new EfficientAdapter(this));
//Will get -1 here
Log.e("Question-17953268",
"getLastVisiblePosition = "
+ mListView.getLastVisiblePosition());
mListView.setOnDrawCompletedListener(new OnDrawCompletedListener() {
#Override
public void onDrawCompleted() {
//Will get correct value here
Log.e("Question-17953268",
"getLastVisiblePosition = "
+ mListView.getLastVisiblePosition());
}
});
Thanks to zapl's answer I was able to get what I needed. I thought I would post the full code in case it helps anyone
listView.post(new Runnable()
{
public void run()
{
int numItemsVisible = listView.getLastVisiblePosition() -
listView.getFirstVisiblePosition();
if (itemsAdapter.getCount() - 1 > numItemsVisible)
{ searchField.setVisibility(View.VISIBLE); }
else
{
searchField.setVisibility(View.GONE);
setFilters("searchtext", "");
}
}
});
I'm looking at the ListActivity source code, and I'm seeing that a private Handler is being defined, and that a Runnable is posted to this handler in the onContentChanged() method.
I don't quite get the point of this, as the handlers, as I understand it, are there for inter-thread communication. Here, the definition of the handler and the posting is happening on the same thread, and no delay is specified in the post() call. I can't see the handler being used for anything else, either.
I've probably misunderstood something about the use of handlers here. Why is it done the way it is here, and not by just running mList.focusableViewAvailable() (the call inside the runnable) directly? Wouldn't the result be the same?
Beneath is what I believe are the relevant portions of the ListActivity source code:
public class ListActivity extends Activity {
protected ListView mList;
private Handler mHandler = new Handler();
private Runnable mRequestFocus = new Runnable() {
public void run() {
mList.focusableViewAvailable(mList);
}
};
/**
* Updates the screen state (current list and other views) when the
* content changes.
*
* #see Activity#onContentChanged()
*/
#Override
public void onContentChanged() {
super.onContentChanged();
View emptyView = findViewById(com.android.internal.R.id.empty);
mList = (ListView)findViewById(com.android.internal.R.id.list);
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
if (emptyView != null) {
mList.setEmptyView(emptyView);
}
mList.setOnItemClickListener(mOnClickListener);
if (mFinishedStart) {
setListAdapter(mAdapter);
}
mHandler.post(mRequestFocus);
mFinishedStart = true;
}
}
Why is it done the way it is here, and not by just running mList.focusableViewAvailable() (the call inside the runnable) directly? Wouldn't the result be the same?
Your concern should not be the Handler. Your concern should be the call to post(). A Handler is not even really needed, as post() is available on View -- this code may pre-date that, though.
post() takes a Runnable and puts it on the message queue for the main application thread. As such, it will not get processed until all other messages that are presently on that queue get processed (FIFO). Presumably, ListActivity needs some other message on the queue to be processed first before focusableViewAvailable() will work successfully.