ListViews ask Adapters for Views before they can be seen on screen. The default implementation of an Adapter actually inflates your views when the ListView asks for them.
When using an Object extends ArrayAdapter<MyObject>, we could actually store all the views in a HashMap<MyObject, SoftReference<View>> and override ArrayAdapter.getView so we can inflate our Views only once, but if the GarbageCollector passes and makes our Views null (or my SoftReferences are made null for any strange reason) we inflate them again.
#Override
public View getView(final int position, final View convertView,
final ViewGroup parent)
{
final MyObject mo = getItem(position);
if (hashMap.get(mo) == null || hashMap.get(mo).get() == null)
{
final MyCustomView mcv = new MyCustomView(getContext());
hashMap.put(mo, new SoftReference<MyObject>(mcv));
}
return hashMap.get(mo).get();
}
I've tried it and it works fine and nice. Is this implementation disencouraged in any way?
we could actually store all the views
You should use ViewHolder pattern. It allows you not to create all the views, but only those you need to display on the screen at the moment.
Related
I need to call the BaseExpandableListAdapter method
#Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent)
or something like that to get the view that the method returns. The problem is, if I call that method, I have to pass null to convertView, which will reinflate the view, which is the view I need.
I need the view because it contains a custom view with code I need to retrieve from it without resetting it to a new instance of itself. Take this method for example:
public ArrayList<CustomViewData> getCustomViewData ()
{
ArrayList<CustomViewData> customViewDatas = new ArrayList<>();
for(int i = 0; i < getGroupCount(); i++)
if(listView.isGroupExpanded(i))
{
View v = //getConvertView
customViewDatas.add((CustomViewData) v.findViewById(R.id.customView);
}
return customViewDatas;
}
How can I get the actual view from the ExpandableListView (or its base adapter) without creating a new view?
Never store the Views generated by getView(). This is a very dangerous practice. You risk leaking Views. Additionally the adapter recycles Views as you scroll, so there's a chance what you are storing will not be representative of what you need.
You are thinking in the wrong direction. The adapter binds data to Views. Meaning, how a View is rendered is based upon the data itself. If you need to update one of the Views as a result of some event...then the data representative of that View must be updated. The adapter can then re-render it's View accordingly.
When solving such problems, just keep telling yourself "If I need to modify the View, then I need to modify it's data."
The solution to this problem is not an easily apparent one.
There is no way to replicate behaviors like ArrayAdapter's capabilities for RecyclerView Adapters.
Instead, if you need to get data from a custom view that is not automatically stored, override
#Override
public void onViewDetachedFromWindow (ViewHolder holder)
{
//STORE VIEW DATA
super.onViewDetachedFromWindow(holder);
}
#Override
public void onViewRecycled(ViewHolder holder)
{
//STORE VIEW DATA
super.onViewRecycled(holder);
}
Use those methods to get the data from the view stored in your ViewHolder.
You must get the data from the visible views after you are done with the recycler. Those visible views are not recycled automatically so you have to retreive the view data from the visible views still on the adapter.
To do this, see this post.
I have the following situation.
I have a ListView, each item of the ListView is comprised of different widgets (TextViews, ImageViews, etc...) inflated form a Layout in the getView() method of the custom adapter.
Now, I would like to achieve the following:
when a certain event is triggered I want to change the background of a View which is inside the item.
Please how do I do it?
This is the the Item Layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/cardlayout"
android:layout_width="320dp"
android:layout_height="130dp"
android:background="#android:color/transparent"
android:orientation="vertical"
android:paddingBottom="5dp"
android:paddingRight="5dp"
android:paddingTop="5dp" >
<FrameLayout
android:layout_width="320dp"
android:layout_height="117dp" >
<View
android:id="#+id/card"
android:layout_width="320dp"
android:layout_height="117dp"
android:background="#drawable/card_selector" />
</FrameLayout>
</LinearLayout>
I need to change the background of card
I have tried doing this:
View v=lv.getAdapter().getView(index, null, lv);
View card =(View)v.findViewById(R.id.card);
card.setBackgroundResource(R.drawable.pressed_background_card);
But no success :-((
When your event is triggered you should just call a notifyDataSetChanged on your adapter so that it will call again getView for all your visible elements.
Your getView method should take into account that some elements may have different background colors (and not forget to set it to normal color if the element doesn't need the changed background, else with recycling you would have many elements with changed background when you scroll)
edit :
I would try something like this :
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null)
{
convertView = LayoutInflater.from(getContext()).inflate(R.layout.card, parent, false);
}
//This part should also be optimised with a ViewHolder
//because findViewById is a costly operation, but that's not the point of this example
CardView cardView =(CardView)convertView .findViewById(R.id.card);
//I suppose your card should be determined by your adapter, not a new one each time
Card card = getItem(position);
//here you should check sthg like the position presence in a map or a special state of your card object
if(mapCardWithSpecialBackground.contains(position))
{
card.setBackgroundResource(specialBackground);
}
else
{
card.setBackgroundResource(normalBackground);
}
cardView.setCard(card);
return convertView;
}
And on the special event i would add the position of the item into the map and call notifyDataSetChanged.
Use the onitemclicklistener which has method onclicksomething..that takes four or five parameters. (View parent, View view, int position, int id). Use the view parameter to customize your background.
Update
Here's some of my code, If you don't understand I recommend to read about recycling and ViewHolder pattern.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
{
ViewHolder viewHolder;
// If convertView isn't a recycled view, create a new.
if(convertView == null){
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.row_gallery_frame, parent, false);
viewHolder = new ViewHolder();
// Here you must be able to find your Widget inside convertView and set a listener to it I guess?
viewHolder.nameHolder = (TextView) convertView.findViewById(R.id.nameTv);
// Set a reference to newly inflated view
convertView.setTag(viewHolder);
}
// If it is, then get the ViewHolder by tag
else{
viewHolder = (ViewHolder)convertView.getTag();
}
// Set the data
GalleryFrame galleryFrame = galleryFrameArrayList.get(position);
viewHolder.nameHolder.setText(galleryFrame.getName());
return convertView;
}
}
// Viewholder pattern which holds all widgets used
public static class ViewHolder{
public TextView nameHolder;
}
I assume you have a model object that you use to "draw" the list item , and for example the background color is determined based on a boolean or something.
All you need to do, is change the value on which you base your decision which background color should that TextView have.
Your getView() method should have code like that
if (myModelObj.isBrown()) {
myTextView.setBackgroundResource(R.drawable.brown_bg);
else
myTextView.setBackgroundResource(R.drawable.not_brown_bg);
All you should do when ur event is triggered, is set the value of the brown boolean in your model
and call notifyDataSetChanged() on your adapter
EDIT
If for some reason you don't wanna call nofitfyDataSetChanged(), althought it won't move the scroll position of your list and with the right recyclying it won't cause bad performance
You can find the View object that represent the list item you want to edit-if it's visisble-, and simply change the background in it, without refreshing the list at all.
int wantedPosition = 10; // Whatever position you're looking for
int firstPosition = listView.getFirstVisiblePosition() - listView.getHeaderViewsCount();
int wantedChild = wantedPosition - firstPosition
if (wantedChild < 0 || wantedChild >= listView.getChildCount()) {
// Wanted item isn't displayed
return;
}
View wantedView = listView.getChildAt(wantedChild);
then use wantedView to edit your background
This answer can be found here
try this one:
View v=lv.getAdapter().getView(index, null, lv);
View card =(View)v.findViewById(R.id.card);
card.setBackgroundResource(R.drawable.pressed_background_card);
card.invalidate();
v.invalidate();
those function force your views to redraw itself and they will render again.
look at invalidate()
What I normally do is this:
public static class EventDetailsRenderer {
private TextView title;
private TextView description;
private Event item;
public EventDetailsRenderer(View view) {
extractFromView(view);
}
private final void extractFromView(View view) {
title = (TextView) view.findViewById(R.id.EventTitle);
description = (TextView) view.findViewById(R.id.Description);
}
public final void render() {
render(item);
}
public final void render(Event item) {
this.item= item;
title.setText(item.getTitle());
description.setText(item.getDescription());
}
}
private class EventsAdapter
extends ArrayAdapter<Event> {
public EventsAdapter(Context context) {
super(context, R.layout.list_node__event_details, 0);
}
public void addAllItems(Event... services) {
for (int i = 0; i < services.length; i++) {
add(services[i]);
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Event event = getItem(position);
EventDetailsRenderer eventRenderer;
if (convertView != null && convertView.getTag() != null) {
eventRenderer = (EventDetailsRenderer) convertView.getTag();
} else {
convertView = getActivity().getLayoutInflater().inflate(R.layout.list_node__event_details, null);
eventRenderer = new EventDetailsRenderer(convertView);
convertView.setTag(eventRenderer);
}
eventRenderer.render(event);
return convertView;
}
}
NOTE: that this example might not compile I pasted it from some code I have and deleted some lines to show an example but the logic it the same.
And then when you want to render it, just get the children from the list, iterate over them, check if the renderer contains the card you want to flip and call its render method... then you render a specific item in the list without effecting the rest of the items.
Let me know if this works...
Adam.
User EasyListViewAdapters library https://github.com/birajpatel/EasyListViewAdapters
Features
Easier than implementing your own Adapter (ie handling
BaseAdaper#getView).Very Easier to provide multi-row support.
Library takes care of recycling all views, that ensures performance
& helps your list view scroll smoothly.
Cleaner code. By keeping different RowViewSetter classes for
different row-types makes your code easy to manage & easy to reuse.
No data browsing, Library takes care of browsing data through
data-structure when View is being drawn or event occurs so that
Users does not have to look for their data to take actions.
Just by passing correct row-types library will Auto-map your
data-types to row-types to render views. Row views can be created by
using XML or Java (doesn't restrict to XML-Only Approach).
Load More callbacks can be registered to implement paginatation
support to your list.
Handling children viewclicks, you can also register for
Children(present inside your rows) view click events.
All these Views are registered with single OnClickListner so that
this mechanism is very memory efficient when click event occurs
users you gets clickedChildView, rowData,int eventId as callback
params.
So, I am making this application. The application parses a website, or more specifically a vbulletin-board. When I'm parsing a thread in the forum, I have divided it up so that when I parse each post in that thread, I get the actual content of the post in sections such as this, and I store the sections in the correct order in an array:
[Plain text]
[Quote from somebody]
[Plain text]
[Another quote]
[Another quote again]
[Some more plain text]
However, a post can be arranged in any order as you might know, and can consist of more or fewer sections than in the example, and it doesn't have to have quotes in it either, or it might just be one or several quotes. Anything is possible.
When I list the posts in my application, I am using a ListView. Each row of this listview will then always consist of a header, and any combination of the previously mentioned sections.
The way I was thinking of doing it after googling a bit about it is to have one "Base-layout" with just a layout-tag in one XML-file, and a separate layout for each section, stored in separate XML-files, and at each call to getView() in my adapter, look at the post at that position in my "Post-list", and then loop through the sections in that particular post, and inflate a new "Quote-layout" for each quote-section stored in the post, and inflate a "Plain-text-layout" for each plain-text-section in the post. And for each of those I fill in all the content belonging to that post.
I think this would work, but there might be a performance problem? As I understand it layout inflation is quite expensive, and I won't be able to recycle the View passed in to getView() either, since it might have a bunch of sections added to it that I might not need in another call to getView().. That is, if I understand getView() and the recycling somewhat.
This is a basic example of what I mean with the getView() method of the adapter:
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
// Inflate the base-layout, which the others are added to.
view = mInflater.inflate(R.layout.forum_post,null);
View header = mInflater.inflate(R.layout.post_header_layout, null);
View message = mInflater.inflate(R.layout.post_text_layout, null);
View quote = mInflater.inflate(R.layout.post_quote_layout, null);
((ViewGroup)view).addView(header);
((ViewGroup)view).addView(message);
((ViewGroup)view).addView(quote);
return view;
}
And then inflate more quote-views/message-views as needed when I extract the data from my list of saved posts.
The base-layout is just a LinearLayout-tag
The layouts I inflate are just RelativeLayouts with some TextViews and an ImageView added.
This code produces this result, where I have a Header with
username, picture, etc.., One section of Plain text, and one Quote-section.
This doesn't seem to work properly all the time though, because when I tried it out just now a copy of the list seemed to get stuck on the background and another one scrolled on top of it..
http://s14.postimg.org/rizid8q69/view.png
Is there a better way to do this? Because I imagine this isn't very efficient
You need to override getViewItemType and getViewTypeCount.
getItemViewType(int position) - returns information which layout type you should use based on position
Then you inflate layout only if it's null and determine type using getItemViewType.
Example :
private static final int TYPE_ITEM1 = 0;
private static final int TYPE_ITEM2 = 1;
private static final int TYPE_ITEM3 = 2;
#Override;
public int getItemViewType(int position)
{
int type;
if (position== 0){ // your condition
type = TYPE_ITEM1; //type 0 for header
} else if(position == 1){
type = TYPE_ITEM2; //type 1 for message
}else {
type = TYPE_ITEM3; //type 2 for Quote
}
return type;
}
#Override
public int getViewTypeCount() {
return 3; //three different layouts to be inflated
}
In getView
int type= getItemViewType(i); // determine type using position.
switch (type) {
case TYPE_ITEM1:
view= mInflater.inflate(R.layout.post_header_layout, null); // inflate layout for header
break;
case TYPE_ITEM2:
view = mInflater.inflate(R.layout.post_text_layout, null); // inflate layout for quote
break;
case TYPE_ITEM3:
quote = mInflater.inflate(R.layout.post_quote_layout, null); // inflate layout for message
break;
....
You need to use a View Holder for smooth scrolling and performance.
http://developer.android.com/training/improving-layouts/smooth-scrolling.html
You can check the tutorial below
http://android.amberfog.com/?p=296
First of all you want to reuse convertView that has been passed as one of the argument. This way you can avoid inflating the item View.
Secondly, you could use something as ViewHolder to store references to your inner Views. Using ViewHolder will increase performance whether you are inflating view or finding them by id as both methods are very expensive.
Set the ViewHolder as a Tag on item View.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
// if possible reuse view
if (convertView == null) {
final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(resource, parent, false);
viewHolder = new ViewHolder(mInflater.inflate(R.layout.post_header_layout, null));
view.setTag(viewHolder);
} else {
// reuse view
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
//set text, listeners, icon, etc.
return view;
}
The ViewHolder is just private inner class storing referenced to view.
private static class ViewHolder {
private final View view;
private ViewHolder(View view) {
this.view = view;
}
}
Talk about ListView usage was given at Google IO 2010.
The inflater needs to know the real type of the futur parent ViewGroup, therefore the following code is erroneous:
view = mInflater.inflate(R.layout.forum_post,null);
and instead, you should use this one:
view = mInflater.inflate(R.layout.forum_post,viewGroup,false);
Same thing for the other inflate: use the real parent (view in this case) or another viewGroup which is of the same type as the (futur) parent; otherwise the LayoutParameters will not be set to the right type and the values that you have specified in your XML code will be lost (never used).
Is it possible to use one adapter class (and it's instance) (e.g. own sub-class of ArrayAdapter<ownDataType> for two (or more) different views?
In my Activity, I have two Spinner objects that are used to show (and select) related data: A list of parent categories and a list of sub-categories of the selected parent category. Of course, the list in the sub-category-Spinner is updated (setting a new ArrayAdapter), when the parent category is changed. However, I also want to be able to set a sub-category directly (and choose the parent category automatically) when the Activity is created. This makes things complicated.
My idea for a simple and clean solution now is to create ONE own Adapter-Class (e.g. derived from ArrayAdapter<CategoryPair>, where CategoryPair is a simple class that holds a pair of parent- and sub-category) and this own adapter class handles all the logic to keep data consistent (much simpler there).
In the getView(int position, View convertView, ViewGroup parent) method of the Adapter-class I need to recognize reliably for which of the two Spinner objects in my activity I need to create a view.
Can I rely on the parent parameter?
Do you think my idea is a good one?
Edit: Code example:
My adapter class:
public class CategoryPair
{
Category parent;
Category sub;
}
public class CategoriesAdapter extends ArrayAdapter<CategoryPair> {
private Spinner parentSpinner;
private Spinner subSpinner;
public CategoriesAdapter(Context context, int textViewResourceId, Spinner par, Spinner sub) {
super(context, textViewResourceId);
parentSpinner = par;
subSpinner = sub;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (parent == parentSpinner)
{
//
}
else if (parent == subSpinner)
{
//
}
else
{
Log.e(TAG, "Invalid parent in CategoriesAdapter");
}
return super.getView(position, convertView, parent);
}
Yup, that's fine. Each view should use a different instance of your Adapter (make sure of this), so, unless your Adapter code is buggy and uses static variables, everything should work just fine.
I'm trying to give the users of my app the option to change how they want their results displayed.
I've created a different layout item for each view and extended from BaseAdapter like so:
public View getView(int index, View recycledCompatibleView, ViewGroup parent)
{
// Just in case the view can be reused
View toReturn = recycledCompatibleView;
if(toReturn == null)
{
LayoutInflater inflator = LayoutInflater.from(parent.getContext());
toReturn = inflator.inflate(layoutId, null);
}
...
}
public void setResultsListStyle(int layoutId)
{
this.layoutId = layoutId;
}
Calling notifyDataSetChanged() is (observable through debug) refreshing the view because the new view is being inflated and returned from getView() method.
However the view on screen is not changing...
Is there something I'm missing ?
The problem here is that ListView may cache already inflated Views that are of old "format". You need to somehow reset this View "cache". For this you can use next trick:
mListView.setAdapter(mListView.getAdapter());
Instead of calling notifyDataSetChanged().