I am trying to build a horizontal recycler view and each item is the same parent type but can include up to ten different subviews attached to it. I have looked at this answer which explains how to override
getItemViewType(int position)
and
createViewHolder(ViewGroup parent, int viewType)
But this only works if you have a limited number of views for each item. The list of items I have to display can have a size of 40 and each one can include up to 8 images each in a different position on the parent view.
My original plan was to create each item view dynamically in the Recycler View Adapter in
onBindViewHolder(ViewHolder viewHolder, int i)
This caused issues because I had to create all the individual views and attach them to the parent view each time I saw the item. So then I created a list of views in the activity which holds the recycler view and instead of passing in the list of objects to create the views, I just passed in the views themselves. This worked much better due to the fact I never had to read each item every time and create a custom view for each item every time it loaded.
My problem now is
It is still not as smooth as I would like. If you have any solutions or ideas on how I could do this faster or more proper. let me know.
Here is an example image of three possible different views for the recycler view. So these views only include the photos and shapes. But other views include text and colors.
Each item view has the option for up to 8 images, borders or no borders around >these images, up to 4 text boxes with striped, solid line, no borders, clip art >and the option for different colours on every one view items. Also different >sizes and different positions on each item. The possibilities are pretty much endless of what the item can look like
We can actually simplify this to
I have view with maximum 8 images and 4 text, that can have different
style and position.
Then there is only 45 possibilities for picking how many images and text there will be (counting possibility of no image or text).
So if i make simple RelativeLayout with enought elements, and then will move them as we need...
Hovewer saying 45 possible types are well...
Way too much. You see making any adapter carry too many item types are not very smart, simply put when adapter is set for recycler view (or ListView) one of first thing it does is creating "small" ViewPool (RecyclerViewPool), its basically simple sparse array , that will keep any removed ViewHolder, until its time to be reused will come, and if the heap is too big it will be removed. That is theory, in practice there are limit of how many scap childs can be at specified time (for recycler its around 5 i think), so if we will be creating view holders with many types we want be hitting that cache most of the time, and since there is no scrap view holder for our type createViewHolder will be called, and in fact this will feel more like we dont have view holder at all.
One more thing.
So then I created a list of views in the activity which holds the recycler view >and instead of passing in the list of objects to create the views, I just >passed in the views themselves. This worked much better due to the fact I never >had to read each item every time and create a custom view for each item every >time it loaded.
Recycler view is used to to unload items (views) that are no longer needed, or should i say that are not visible to user, and also to reause old views us much as possible to reduce number of layout inflatings, and searches for id.
And by loading them all at all times, this functionality is ommited.
If im not mistaken this might be just a bit slower than creating ScrollView with LinearLayout (Vertical) and putting all views inside.
Hovewer biggest drawback here is that all your views are loaded at all times, and if they are simple i dont really see why not use ScrollView + LinearLayout, but if not well this gets complicated, on new phones, memory is not such big problem, there are phones with 500+ mb, but even so not every phone will give you 300 mb for your application.
My Answer
I can guess it want be what you wanted to hear :)
The thing that slows down layout inflating, isnt actually setting texts, background or position but loading resources to do that, and searching for views.
So lets start with:
Use cache for your loaded resources to reuse them us much as you can, (i added small section about image loading at the end).
FindViewById is really slow, i mean it, just by using viewholder pattern and removing searching for view you can get a nice speed up.
You want be getting away from setting layout for each element.
Idea is pretty simple create groups of layouts depending on amount of your main hierarchy Views (this time) Images and text. While creating ViewHolder create RelativeLayout, push enough images/text to suit your need for element at position. While binding data move elements and style them as needed.
To reduce number of types, lets use number of images/text rounded to 2.
Updating LayoutParams does not cost us much as most think, it cost some, dont get me wrong, this basically forces parent to make additional child measurement and layout update, thats all, and while view is going to be diplayed it has to make this measurement and layout update anyway :)
Lets say we have this two groups
Images (max 2) id: 0
Images (max 4) id: 1
Images (max 6) id: 2
Images (max 8) id: 3
And
Texts (max 2) id: 0
Texts (max 4) id: 1
And this is how i will be "naming" types
int type = (img_id)<<1 | (txt_id)
And total type count is actually
int type_count = (4 * 2) = 8
End our "base" for adapter is something like this
public class MyAdapter<MyType extends MyAdapter.MyTypeBase> extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
Context context;
ArrayList<MyType> elements;
public MyAdapter(Context context, ArrayList<MyType> elements) {
this.context = context;
this.elements = elements;
}
public int getItemCount() { return elements.size(); }
public MyType get(int position) { return elements.get(position); }
public int getItemViewType(int position) {
int imgs = get(position).getImageCount();
int txts = get(position).getTextCount();
imgs = (imgs/2) * 2 + (imgs % 2 == 0 ? 0 : 1);
txts = (txts/2) * 2 + (txts % 2 == 0 ? 0 : 1);
return imgs << 1 | txts;
}
#Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
return new ViewHolder(new RelativeLayout(context), position); // or use inflater to inflate some base layout
}
class ViewHolder extends RecyclerView.ViewHolder {
int textCount, imageCount;
RelativeLayout root;
ImageView [] imageViews;
TextView [] textViews;
public ViewHolder(RelativeLayout view, int position) {
super(view);
root = view;
textCount = (position & 1) * 2;
imageCount = (position >> 1) * 2;
textViews = new TextView[textCount];
imageViews = new ImageView[imageCount];
for (int i = 0; i < imageCount; i++) {
root.addView(imageViews[i] = new ImageView(context)); // or use inflater
}
for (int i = 0; i < textCount; i++) {
root.addView(textViews[i] = new TextView(context)); // or use inflater
}
}
}
interface MyTypeBase {
int getImageCount();
int getTextCount();
}
And yes somethings is missing :)
public void onBindViewHolder(ViewHolder viewHolder, int position) {
MyTypeBase element = get(position);
vh.textViews[vh.textCount-1].setVisibility(element.getTextCount()%2 == 0? View.VISIBLE : View.GONE);
vh.imageViews[vh.imageCount-1].setVisibility(element.getImageCount()%2 == 0? View.VISIBLE : View.GONE);
// now this is your place to shine
}
Since i dont know what exactly you can get as "data" for your element cant help you much here. So in on bindViewHolder, you viewHolder will have enough images and text as your element needs, reload any image and move them.
Last notes
I should also add fev notes here and there :)
Be lazy. Yes i mean it when you want to be lazy you try to make your solution as simple as possible making code more easy to read, and less likely to have major bugs :)
Also please use library for displaying and caching images instead of loading them each time. There are quite few good ones like UniwersalImageLoader, Picasso or Volley. I personally use UIL so heres example.
// This might be done in helper class ^^
DisplayImageOptions OPTIONS_DISC_AND_CACHE = new DisplayImageOptions.Builder()
.cacheOnDisk(true)
.cacheInMemory(true)
.build();
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(context)
// .memoryCacheSize((int) (Runtime.getRuntime().maxMemory() / 1024) / 16) // if you want to reduce it some more ^^
.threadPoolSize(4)
.tasksProcessingOrder(QueueProcessingType.LIFO)
.build();
imageLoader.init(configuration);
// And thats how to use it
ImageLoader.getInstance().displayImage("MY URL", (ImageView) myImageView, Tools.OPTIONS_DISC_AND_CACHE);
And why you should use such library, dunno... Images are loaded asynchronusly (not on ui, making it more resposible), you dont care for how they are actually loaded, and when, adding placeholders is easy, cache in memory and cache in for disk allows for faster loading times.
And lastly, english is not my primary language, so if i misspelled something, forgot comma, or wrote sentence in wrong manner, please do make corrections.
Cheers.
1 Create a list of lists each item should contain 1 or more image
List<List<image>> list
2 pass it to your adapter
3 in the getItemView method use if to set the type according to the
list.get(position).size();
4 use if again or isinstanceof or I think the viewHolder has a method to get its class and cast your viewHolder to the right class .
Related
I am using recycler view in a chat app, now as you all know in a chat room we have 2 different views.
Right view : the one that you sent.
Left view : the one that you received.
Now I managed to achieve what I want by using one layout item and inside that item I used 2 relative layouts, one layout for the right view and another for the left view.
And in order to know what layout to show I did this in onBindViewholder:
onBindViewHolder(){
if(/*I sent the message*/){
right_layout.setVisibility(view.VISIBLE);
left_layout.setVisibility(view.GONE);
}
else {
right_layout.setVisibility(view.GONE);
left_layout.setVisibility(view.VISIBLE);
}
}
I don't have any problem with using the above method. But my question is why others use that thing that is called multiple view types in which they use 2 view holders? Should I use it instead?
First: in your case, I'm not sure whether it's really necessary to use two view types. A typical RecyclerView on a mobile phone will show between seven and ten rows at a time and it will generate some more just to be able to scroll smoothly should the need arise. Compared to a gaming app, there's next to nothing to do UI-wise. But you asked why someone might want to use multiple view types, so here goes:
Right now 50% of the Views you're inflating will be set to GONE, so it's like a continous waste of CPU time and (worse on a mobile device) it's draining the battery unnecessarily. By using two view types, you avoid this (to a certain extent, I suppose that determining the view type takes less energy).
Another reason is better performance which results in a better user experience: RecyclerView was developed to efficiently deal with lists of items. By telling it which row can be reused for which list position, you are making the best use of its recycling algorithm.
From a developer point of view, I'd keep your approach as long as the difference between the two types is no more than two or three lines of code. But there are lists where the difference is more like twenty lines of code. In this case, using different view types, layout files and ViewHolders improves the readability of your code.
Yes, you should implement different a ViewHolder for each type of view. This provides a few advantages, like not having to maintain the relevant state, decluttering layouts, and generally easier to reason about once additionally functionality is included in the future.
It's also easy to implement:
#Override
public int getItemViewType(int position) {
Message message = messages.get(position);
if (message instanceof ReceivedMessage) {
return VIEWTYPE_RECEIVED;
}
if (message instanceof SentMessage) {
return VIEWTYPE_SENT;
}
return 0;
}
then:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(mContext);
switch (viewType) {
case VIEWTYPE_RECEIVED:
View receivedView = inflater.inflate(R.layout.item_received, parent, false);
ReceivedViewHolder receivedViewHolder = new ReceivedViewHolder(receivedView);
return receievedViewHolder;
case VIEWTYPE_SENT:
// ...
}
}
finally:
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case VIEWTYPE_RECEIEVED:
// ...
}
}
My android is very rusty, so this is the best way I can explain this:
A card contains an image, a value, and a URL
I have an array of values, a parallel array of images, and of URLs (values[i] <-> images[i] <-> URLs[i])
Have a GridView that I want to use to display many of these cards
The problem:
I have a class that extends BaseAdapter to create a custom view to display the three elements of the card
Using the getView method of said adapter, I use the "i" expected by getView as a mental index of which card we are talking about.
Unfortunately I realized that i=0 means the currently visible first card, I thought it meant the overall first card. This makes it useless as a system to keep track of the overall position of cards.
So, the visible elements are populated correctly in the view. But, if I scroll down and then back up, some internal elements have been jumbled up. So clicking a card might now lead to the URL of a card that was initialized after it.
What I need help with:
A better way to index or populate each card's content that will be permanent.
I am wildly confident I am doing this in a horrendous way. I'm imagining there must be some way to say that:
When GridView is created -> populate each card's details and fill in GridView.
Current Main Activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_card_list);
gv = (GridView) findViewById(R.id.cardGridView);
gv.setAdapter(new CardView(this, cardURLs, cardNames, cardPrices, cardImages));
}
Current CardView Activity:
public CardView(CardListActivity mainActivity, String[] cardURLs, String[] cardNames, Double[] cardPrices, int[] cardImages){
//...
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public class Holder{
TextView priceTV;
ImageView cardIV;
String cardName;
}
#Override
public View getView(final int i, View convertView, ViewGroup parent) {
//...
View rowView;
rowView = inflater.inflate(R.layout.card_item_view, null);
//HERE IS WHERE I SET THE PRICE AND IMAGE USING i
holder.priceTV.setText("$" + prices[i].toString());
holder.cardIV.setImageResource(images[i]);
//...
return rowView;
}
Turns out the problem was something else.
The actual problem ended up being caused by these Dialogs I would create to verify if the user wanted to open the website.
I was creating them inside getView, all in the same variable, which meant that the last elelemnt to get initialized would be the one used in the dialog.
I fixed this by moving the dialog creation into the onClick for the view.
Firstly, You should wrap your contents into objects so that each CardContent object contains a url, an image and a value, Then pass those into your adapter. That will be much easier on you, you only need to maintain 1 List of CardContent rather than 3 individual lists and hoping the order doesn't get messed up.
Secondly, This sounds like a case for a Recyclerview. You can use a GridLayoutManager with a Recyclerview instead of a GridView so that your views get recycled and you have less overhead. Luckily the code is largely the same.
See https://developer.android.com/training/material/lists-cards.html for pretty much what you want.
As far as I know, the position returns specific chosen item from the whole list. So how does the adapter use the position and transfers all of the items without some kind of loop? I guess there is a basic mistake in my sight regarding lists and positions.
This is the code:
(THANK YOU IN ADVANCE):
public View getView(int position, View convertView, ViewGroup parent) {
viewHolder holder;
if (convertView==null){
convertView= LayoutInflater.from(mContext).inflate(R.layout.customupdatestatus, null);
holder=new viewHolder();
holder.statusHomePage=(TextView)convertView.findViewById(R.id.statusUploaded);
holder.userNameHomePage=(TextView)convertView.findViewById(R.id.userNameUpdate);
convertView.setTag(holder);
}else{
holder=(viewHolder) convertView.getTag();
}
ParseObject statusObject= mStatus.get(position);
String username= statusObject.getString("userName");
holder.userNameHomePage.setText(username);
String status=statusObject.getString("newStatus");
holder.statusHomePage.setText(status);
return convertView;
}
Ok, this is something that was bugging me for some time. So, when the view is created, you can see certain amount of rows. From row one to the last row in the view, adapter is counting. When you scroll down, the counter resets, or, I would say, it starts again. So, lets say you have 10 rows on screen when view is created. When you scroll down, if you select to check row number 5, it will select on the first view, when you scroll, it will show the other element is checked too. I tried to find solution to this, but couldn't.
So how does the adapter use the position and transfers all of the items without some kind of loop?
In short, it doesn't. This is exactly what happens.
The Android framework has separated the process of creating a list into a few different pieces. The ones we care about here are the ListView and the Adapter.
The ListView's job is to ask the adapter how to map a position to a View, then use that information to correctly layout, measure, and draw those Views.
The Adapter's job is to tell the ListView how to map a position in the list to a View instance. Because the Android framework takes care of looping through the items for you in ListView, you don't need to worry about doing that yourself- you just need to provide the mapping.
Here is the general idea of what ListView does:
// Figure out how many children we have
int numChildren = adapter.getCount();
for (int i = 0; i < numChildren; i++) {
// Get the view for this position
View childView = adapter.getView(i, null, this);
// Add it to ourselves
addView(childView);
}
This is of course highly simplified, but is a good high level idea of what is happening with your adapter behind the scenes.
I am new to Android development and reading through some example code. I have copied one method from the sample code in an Adapter class (derived from ArrayAdapter), the derived class has a checkbox in addition to the text view:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);
CheckedTextView checkMark = null;
ViewHolder holder = (ViewHolder) listItem.getTag();
if (holder != null) {
checkMark = holder.checkMark;
} else {
checkMark = (CheckedTextView) listItem.findViewById(android.R.id.text1);
holder = new ViewHolder(checkMark);
listItem.setTag(holder);
}
checkMark.setChecked(isInCollection(position));
return listItem;
}
private class ViewHolder {
protected final CheckedTextView checkMark;
public ViewHolder(CheckedTextView checkMark) {
this.checkMark = checkMark;
}
}
The sample code is to optimize the getView by caching the View within a ViewHolder object.
Where I am confused is I thought the convertView, if not null, would be re-purposed and then the View data is populated into it and returned.
If this is the case, then how could the setTag / getTag methods called in the code be relied upon? It would seem that the same object would have to be retrieved in order for it to work?
perhaps view returned from getTag on a subsequent call is for a different list item, and returns the wrong view
Adapters use a RecycleBin. This class allows the ListView to only create as many row layouts as will fit on the screen, plus one or two for scrolling and pre-loading. So if you have a ListView with 1000 rows and a screen that only displays 7 rows, odds are the ListViiew will only have 8 unique Views.
Now to your question using my example above: only eight row layouts and 8 subsequent ViewHolders are ever created. When the users scrolls no new row layouts are ever created; only the content of the row layout changes. So getTag() will always have a valid ViewHolder that references the appropriate View(s).
(Does that help?)
You're on the right track, here's some information that may help make more sense of how ListViews work:
A simple implementation of the getView() method has two goals. The first is inflating the View to be shown on the list. The second is populating the View with the data that needs to be shown.
As you stated, ListViews re-purpose the Views that compose the list. This is sometimes referred to as view recycling. The reason for this is scalability. Consider a ListView that contains the data of 1000 items. Views can take up a lot of space, and it would not be feasible to inflate 1000 Views and keep them all in memory as this could lead to performance hits or the dreaded OutOfMemoryException. In order to keep ListViews lightweight, Android uses the getView() method to marry Views with the underlying data. When the user scrolls up and down the list, any Views that move off the screen are placed in a pool of views to be reused. The convertView parameter of getView() comes from this list. Initially, this pool is empty, so null Views are passed to getView(). Thus, the first part of getView should be checking to see if convertView has been previously inflated. Additionally, you'll want to configure the attributes of convertView that will be common to all list items. That code will look something like this:
if(convertView == null)
{
convertView = new TextView(context);
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
The second part of an implementation of getView() looks at your underlying data source for the list and configures this specific instance of the View. For example, in our test list, we may have an Array of Strings to set the text of the view, and want to set the tag as the current position in the Data of this View. We know which item in the list we're working with based on the position parmeter. This configuration comes next.
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
This allows us to minimize the amount of time we spend inflating/creating new views, a costly operation, while still being able to quickly configuring each view for display. Putting it all together, your method will look like this:
#Override
public View getView(int position, View convertView, ViewGroup)
{
if(convertView == null)
{
convertView = new TextView(context);
//For more complex views, you may want to inflate this view from a layout file using a LayoutInflator, but I'm going to keep this example simple.
//And now, configure your View, for example...
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
//Configure the View for the item at 'position'
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
//Finally, we'll return the view to be added to the list.
return convertView;
}
As you can see, a ViewHolder isn't needed because the OS handles it for you! The Views themselves should be considered temporary objects and any information they need to hold onto should be managed with your underlying data.
One further caveat, the OS does nothing to the Views that get placed in the pool, they're as-is, including any data they've been populated with or changes made to them. A well-implemented getView() method will ensure that the underlying data keeps track of any changes in the state of views. For example, if you change text color of your TextView to red onClick, when that view is recycled the text color will remain red. Text color, in this case, should be linked to some underlying data and set outside of the if(convertView == null) conditional each time getView() is called. (Basically, static setup common for all convertViews happens inside the conditional, dynamic setup based on the current list item and user input happens after) Hope this helps!
Edited - Made the example simpler and cleaned up the code, thanks Sam!
I have extended CursorAdapter and associated it with a ListView. I have two different layouts for the rows and have implemented getViewTypeCount, getItemViewType, newView and bindView. The data comes from an SQL table where each row has a layout column that indicates what kind of layout this item should have.
My getViewTypeCount looks like this:
#Override
public int getItemViewType(int position) {
int layout = mCursor.getInt(mLayoutIndex);
if (layout == DataHelper.LIST_LAYOUT_ADD_PROJECT)
return 0;
else
return 1;
}
But this seems to screw up the recycling of views.
How can this be accomplished? Or is this a bad way of implementing this?
First of all, you shouldn't store the layout integer ID in persistent storage, these IDs can (and will) change with later compilations based on the order and names of things, so it will most likely come bite you later. This might actually be the source of your troubles.
The other thing that I see is that you don't use the position argument that was provided to you, so how do you know that the data in mCursor corresponds to the current position being queried?