I'm trying to recreate ListView (and AbsListView) logic, with view recycling.
I need this but we can say it's only for understand Android logic.
Suppose my children items are the same (same layout), using fixed height RelativeLayout.
During scrolling, I'm reusing ghost children view and set properties for current item.
It's working fine, since I'm using View.offsetTopAndBottom() and invalidate() instead of requesting layout during scroll for optimization.
My problem is updating the children (RelativeLayout).
Depends of item, I want to hide or show ImageView on this item. For that, I'm just using iconImage.setVisibility( GONE ) and iconImage.setVisibility( VISIBLE ).
Since I'm blocking requestLayout, it seems to be setVisibility() does not work properly.
If I use requestLayout, all the tree will measure and layout itself, and it's not a good way for a scrolling user experience.
Is there a way for only request layout on recycle child item ?
You need to notify your listView with changed of view so call notifyDataSetChanged(); from Uithread like this :
final ArrayAdapter adapter = ((ArrayAdapter) getListAdapter());
runOnUiThread(new Runnable() {
#Override
public void run() {
adapter.notifyDataSetChanged();
}
});
Related
I have a RecyclerView. Each ViewHolder in the RecyclerView contains two elements whose visibility I want to set to GONE, upon calling of a method. What I'm trying to do is to wait for the RecyclerView to finish laying out using recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(), change the visibilities for each item, and then remove the GlobalLayoutListener. Here is a part of my code:
public void deselectMessages() {
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
for (int i = 0; i < messages.size(); i++) {
recyclerView.findViewHolderForAdapterPosition(i).itemView.findViewById(R.id.frame_layout_selected).setVisibility(View.GONE);
recyclerView.findViewHolderForAdapterPosition(i).itemView.findViewById(R.id.selected_button).setVisibility(View.GONE);
}
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
I get the following error:
java.lang.NullPointerException: Attempt to read from field 'android.view.View androidx.recyclerview.widget.RecyclerView$ViewHolder.itemView' on a null object reference
What can I do to make this work?
I have thought that maybe I can't modify the ViewHolders which are not currently being displayed. But the problem is that when I scroll to those Views, the elements of those ViewHolders have their visibility set to VISIBLE. If I don't modify the visibilities of views currently not on the screen, when I do scroll to those views, the elements of the ViewHolders will have their visibilities set to VISIBLE. What can I do about this? By the way, I am okay with modifying the visibilities in the xml files through code when the deselectMessages() method is called.
It is not guaranteed that the RecyclerView will return a ViewHolder for a given position. RecyclerView only creates some ViewModels that are enough to fill the RecyclerView and few buffer ones. What you need to do is basically maintain a state object within your Adapter. That adapter can then use this state information to render/update the item according to your need in onBindViewHolder function.
In your case you can keep a HashSet flaggedItems. And keep the position or ID of elements you want to show differently.
If you update this set, make sure to call notifyDataSetChanged in your adapter. Adapter will make sure the items that are updated.
And all your logic to decide how the item should look like should go in onBindViewHolder.
I am using RecyclerViews in my app project and setting OnClickListeners with their ViewHolders (in their constructors like mentioned in a StackOverflow Q&A).
Then I have this question: how can I remove OnClickListeners from RecyclerView's ViewHolders when they are disposed.
Usually, we can remove an OnClickListener by doing this:
view.setOnClickListener(null);
And if it is a ViewPager's PagerAdapter, we can do so in destroyItem method.
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
View view = container.findViewById(R.id.viewId);
view.setOnClickListener(null);
}
Where can I do so with RecyclerView? --Or, I need not do so?
If you want to null set the onCLickListener() of the views of RecyclerView.Adapter when the view goes off the screen, you can do so by overriding the http://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#onViewDetachedFromWindow(VH) in your recyclerView's adapter. You will receive the holder as parameter that has just gone off the screen. You can null set onClickListener of any view available in that holder.
Or if you just want to do the same when it becomes visible on the screen, you can do in onBindViewHolder(). But this does not make sense as instead you can avoid setting listener.
Points to remember, related to this answer:
Setting the listener to null may be ther requirement when you do not want to set click listener to view for every data set but to only few. In this case, it is always better to set listenrs to null as and when they go off the screen. Otherwise, as RecyclerView will re-use (recycle) the holder objects that were gone away to represent the new dataset that is becoming visible. In this process, data set (view in a holder) that you did not set the listener to may have the listener set because of recycling.
All in all, while getting the advantage of smooth scrolling due to recycling, it is dev's responsibility to reset the views (clearing image views, text views etc..) and null setting the onCLickListener etc.
If you are using RecyclerView and binding OnClickListeners to it's every row root view, there is no need to dispose them.
But if some views shouldn't respond to click event's just use setOnClickListener(null)
There are some topics about RecyclerView inside RecyclerView but I see most of them do not fit my case. My case is I have a RecyclerView (verticle linear layout management) displays a list of CardView, each Cardview contains a inner RecyclerView (horizontal linear layout management). The problem is all about performance when scrolling, it is not smooth at all. I notice if I comment the setAdapter for the Inner Recyclerview, the scrooling become smooth, but I make the CardView not updated the new list. The code is something similar to this:
onBindViewHolder...{
holder.innerRecycler.setAdapter(new InnerAdapter((data));
// comment that line make the outer recyclerview smoothly but the CardView data not updated thanks to the view recycling.
}
I know a scrollable view inside a scrollable view is not a good idea but I dont see any other choices. Anyone face to this kind of layout before?. Thanks.
UPDATE (add more code).
// init outer recyclerview
mOuterRecyclerView = (RecyclerView) findViewById(...);
mOuterRecyclerView.setLayoutManagement(new LinearLayoutManagement(this));
mOuterRecyclerView.setHasFixedSize(true);
mOuterRecyclerView.setAdapter(new OuterAdapter(dataList));
// The adapter class for the outer one
onBindViewHolder(...){
final dataItem = mItems.get(position);
holder.innerRecycler.setAdapter(new InnerAdapter(dataItem.getList()));
}
// the holder for the outer
class MyHolder extends ViewHolder{
RecyclerView innerRecycler;
public MyHolder(View view){
super(..);
innerRecycler = findViewById(...);
}
}
// the adapter for the inner
onBindViewHolder(...){
final dataItem = mItems.get(pos);
// async loading
holder.tvTitle.setText(dataItem.getTitle);
}
The layout is pretty simple so I dont post the fully code here. :)
#Paul i had the same requirement and the below trick worked.
Put the parent recyclerview inside NestedScrollView and in your onCreate method,
set.
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setNestedScrollingEnabled(false);
This prevents scroll of parent recyclerview and scroll becomes absolutely smooth.
There is no issue in such an approach as long as they scroll along different axes.You could enable RecyclerView.startNestedScroll(int) and also handle situations like overscroll.This delay would be because you are reinitiating an adpater everytime. You could try something different like maintaining a map of adpaters and calling RecyclerView.swapAdapter(args...) in bindVH.
Another good step could also be using a common pool for recycled views using RecyclerView.setRecycledViewPool(args...) .
I have created and used a list of 100+ plus items with a nested (different axis) recycler and have not faced issues .
If you would provide more code( where you have written async loading) I could help you more.But I suggest you go through the API and the design patterns and that should solve your issue.
Since there are some user ask me how to archive, I'm going to post what I do.
The sugession that #Droidekas seems a good point but I dont follow that.
What I did is:
Each horizontal recycler view is a item of a vertical recycler view
When I need to set the data for the particular vertical item (onBindViewHolder), I use the horizontal recycler adapter setNotifyDataSetChanged instead of setting a completely new adapter or swapping it. This way, I got the vertical recyler run very smoothly.
The vertical recyler adapter structure could be:
Vertical Item view holder -> Vertical Item[Name, List<Horizontal item>,...]
Vertical Item view holder -> Vertical Item[Name, List<Horizontal item>,...]
In OnBindViewHolder
holder.setData(VerticalItem)
In the Holder, I have a setData method looks like:
setData(VerticalItem item){
mItems = item.getHorizontalItems();
mHorizontalAdapter.notifyDataSetChanged();
}
Hope it helps. :D
What I am trying to do is populate a listView with views/elements that could be a textview/imageview. Potentially, these textviews are expandable. I want the listview to resize and wrap the content if the textview is expanded.
There are several solutions out there already:
1. Make the elements of the ListView element invisible so that the view in ListView is already the size you want. In this case, create the text view with the original long string and then collapse it.
2. Resize the element in the adapter then call notifyDataSetChanged();
However I'm looking for a solution where the view in ListView automatically resizes, even when its visible. I have tried using Solution #2 and while it works for changing values inside the view such as a string, it doesn't resize the view altogether unless the view is scrolled off screen then comes back into view.
The relevant part of my code is as follows:
holder.text.setTrim(!holder.text.getTrim());
holder.text.setText();
holder.text.requestFocusFromTouch();
notifyDataSetChanged();
holder.text.invalidate();
vi.invalidate();
vi.requestLayout();
I put this inside an OnClickListener inside an adapter's getView function.
Anyone have any solutions?
PS. Has anyone tried doing a method with multiple adapters? This one doesnt seem to work too well for me because I have potentially hundreds of elements.
Edit:
So I've come up with part of a stupid solution that is absolutely ridiculous. The behavior makes no sense. Below (primaryTextView is the same text as above eg. holder.primaryTextView == holder.text)
holder.primaryTextView.setTrim(!holder.primaryTextView.getTrim());
if(!holder.primaryTextView.getTrim()){
holder.primaryTextView.setVisibility(View.GONE);
}
else{
holder.primaryTextView.setVisibility(View.VISIBLE);
}
notifyDataSetChanged();
vi.invalidate();
This allows you to expand the view while viewing it but does not allow you to shrink the view once the text is collapsed. Furthermore, View.GONE and View.VISIBLE are NOT interchangable. Finally, the textView does not even dissapear in the event that View.GONE is triggered. Perhaps this might be a clue?
it doesn't resize the view altogether unless the view is scrolled off screen then comes back into view.
It is working, but you are not refreshing the listview, when you scroll off & return, listview recreates the updated view.
You should notifyDataSetChanged(); adapter.notifyDataSetChanged();
Nevermind found the problem:
In my layout, I was using height="0dp" instead of "wrap_content"
So I have a fairly complex activity the parent being a linearlayout with a table with some basic info and buttons. Then a couple listviews that the user can add items to. So these listviews grow more and more as the user uses the app. The problem I'm running into is the Linearlayout is bigger then the resolution of the screen and so it needs to scroll. So the scrolling doesn't work on the Listviews. I've tried playing with changing the layout_height of the listview and its child element with no success. Is there a way to make these couple listviews expand out to the amount of children? Or am I going about this all wrong? If so what other controls can I use?
I'm open to any ideas. I really just don't know how to go about this.
Thanks
The problem I'm running into is the
Linearlayout is bigger then the
resolution of the screen and so it
needs to scroll. So the scrolling
doesn't work on the Listviews.
You need to rethink the way you are building your UI, as you cannot have ListViews inside of a ScrollView.
One possibility is to put everything in the ListView, marking some rows as being non-clickable, so ListView handles your scrolling. You can use my MergeAdapter for cases like this.
If you have a simple layout then you can replace your ListView component with a LinearLayout and manually program together the functionality you need to add/remove and click the items within it.
Here is a simple example of my own. refreshStudentList() has taken the place of notifyDataSetChanged() being called on the adapter, recentConnections is a LinearLayout and adapter is an instance of an Adapter (You can use the adapter you were using for your ListView):
private void refreshStudentList() {
recentConnections.removeAllViews();
for(int i=0; i < adapter.getCount(); i++) {
View toAdd = adapter.getView(i, null, recentConnections);
final int position = i;
toAdd.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// onItemClick functionality here
}
});
recentConnections.addView(toAdd, new LayoutParams(LayoutParams.MATCH_PARENT,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics())));
}
recentConnections.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
As you can see, instead of an onItemClickListener I've set a onClick listener to each list item. This could easily be changed to be an onTouch listener if you wanted to implement the look and feel of clicking on a list item (by changing the view's background on MotionEvent.ACTION_DOWN and MotionEvent.ACTION_UP).
The other thing you need to note is I have hard coded a dp value for the height of each value (50dp in this case). When you are creating and inflating views programatically they don't have a "height" value so "wrap_content" becomes 0.
also, notifyDataSetChanged() on your adapter will do nothing.
As I said, this is a simple solution but it's the start of building up your own ExtendingListView custom component if that's what you're eventually aiming for.