Android RecyclerView getChildAt() and getChildAdapterPosition() - android

I saw a sample code and couldn't understand the meaning of the following method:
public int getAdapterPositionForIndex(RecyclerView parent, int index) {
final View child = parent.getChildAt(index);
return parent.getChildAdapterPosition(child);
}
My understanding is what's returned should always equal to index, but my debugger obviously doesn't say so. Since the docs are not explaining well the difference between getChildAt() and getChildAdapterPostion(), I hope I could get some expert insights here.

Well as per my understanding getChildAt() is a method of ViewGroup . And it does Returns the view at the specified position in the group.
Since RecyclerView is an AdapterView i.e items get recycle when goes out of boundary it returns null for #getChildAt().
I am not sure whats the exact reason may be some should explain this
On other hand #getChildAdapterPosition() Return the adapter position that the given child view added to.
Look at the code below :(Only adding the essential)
findViewById(R.id.b1).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
LinearLayoutManager layoutManager= (LinearLayoutManager) rvNumber.getLayoutManager();
final View child = layoutManager.findViewByPosition(30);
if(child!=null) {
int i = rvNumber.getChildAdapterPosition(child);
Log.i("pos", i + "");
}else{
Log.i("pos", "View is null");
}
}
});
Nothing complex here! I laid down 60 items in RecyclerView just a TextView. In which 10 items are showing at a time in list . So the first time 10 views will be laid down (0-9).
When i call the above code on clicking on button it gives me a null view . Cause Views is not inflated yet for position 30. But after scrolling to position 30 it returns the view and hence its position by getChildAdapterPosition() which will be 30 also .
I think you should make a sample and play around with it for better understanding.

FROM DOCS
getChildAdapterPosition()
Return the adapter position that the given child view corresponds to.
getChildAt()
Returns the view at the specified position in the group.
The Difference
It Means the getChildAdapterPosition() method return the the position of View inside recyclerview adapter
AND
the getChildAt() method returns the View from a viewGroup of specific position
In short
The Both method are different getChildAt() is retuning view from a viewGroup while the other getChildAdapterPosition() retuning the postion of a view in recyclerview adapter

Related

Two items get selected in listview android

I'm trying to implement a custom listview. Everything works fine until I use a if () statement inside the getView() method
Without the if() condition a single item gets selected when I select an item but when I add the if() condition, the views are displayed properly but two items (non-adjacent) get selected (1st and last 1st or and second-last, any such combination).
View getView(...){
....
if (!item.getPriceTo().equals(""))
priceToTV.setText(item.getPriceTo());
else
priceToTV.setText(item.getPriceFrom());
return view;
}
Also I'm using saving the previous view to show the selection so the current selection has a red_border and when it is selected a black_border is set to it.:
subItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Log.d("New Order", "........");
if (previousViewOfSubItems != null && previousViewOfSubItems != view) {
previousViewOfSubItems.setBackgroundResource(R.drawable.black_border);
if (quantity.getText().toString().equals("xx") || quantity.getText().toString().equals("0")) {
viewForVisibility.setVisibility(View.GONE);
layoutForQuantity.setVisibility(View.GONE);
}
}
if (previousViewOfSubItems == view)
return;
previousViewOfSubItems = view;
previousViewOfSubItems.setBackgroundResource(R.drawable.red_border);
viewForVisibility = previousViewOfSubItems.findViewById(R.id.viewForVisibility);
viewForVisibility.setVisibility(View.VISIBLE);
layoutForQuantity = (LinearLayout) previousViewOfSubItems.findViewById(R.id.layoutForQuantity);
layoutForQuantity.setVisibility(View.VISIBLE);
quantity = (TextView) previousViewOfSubItems.findViewById(R.id.subTypeQuantity);
}
});
previousViewOfSubItems = view; seems to be causing the problem,
In Listviews with adapter you should avoid saving view instances, because views are reused by adapters so view can be same for two rows so rather than saving view instance's reference use ViewHolder Design pattern and use view tagging
You need to use ViewHolder Pattern and view tagging to properly identify every view in different position. ListView always recycle the view instead of re-inflating the view again and again.
You can refer to Android Training documentation on how to implement ViewHolder pattern.
ListViews recycle the views in the list. so as you scroll, top views are reused and content replaced using the methods.
Where you set background to Red etc, use Else statements to set it back to your default black.
its beacause it listview reuses view to display items, once the first view is scrolled out the the same view is reused to display the view at bottom of the listview. instead of comparing the view try compairing the position of the view clicked
I have explained about this abnormal behavior in my blog on recyclerview you refer that to solve this problem.
use pojo class to get the status and update the view accordingly

GridView get view by position, first child view different

Android GridView is quite interesting, it reuses the child views. The ones scrolled up comes back from bottom. So there is no method from GridView to get the child view by its position. But I really need to get view by its position and do some work on it. So to do that, I created an SparseArray and put views by their position in it from getView of BaseAdapter.
SparseArray<View> ViewArray = new SparseArray<View>();
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null)
view = li.inflate(layoutID, null);
ViewArray.put(position, view);
}
Now, I can get all visible views by their position. Everything works perfect as it should but in some devices, the first child view(position 0) is not same as the one in array. I logged the getView and found that for position 0, getView got called many times and each time array was set with different view. I have no idea why GridView is calling getView for position 0 many times and that happens only on few devices. Any solution ?
After reading source of getPositionForView, I have wrote this method in own GridView, works perfect by API 18
public View GetViewByPosition(int position) {
int firstPosition = this.getFirstVisiblePosition();
int lastPosition = this.getLastVisiblePosition();
if ((position < firstPosition) || (position > lastPosition))
return null;
return this.getChildAt(position - firstPosition);
}
You can't reach the views directly because of the recycling. The view at position 0 may be re-used for the position 10, so you can't be sure of the data present in a specific view.
The way to go is to use the underlying data. If you need to modify data at position 10, then do it in the List or array under your adapter and call notifyDataSetChanged() on the adapter.
if you need to have different views for different data subtypes, you can override the two following method in your adapter: getItemViewType() and getViewTypeCount()
Then, in getView() you can 1) decide which layout to inflate 2) know the type of view recycled using getItemViewType()
You can find an example here:
https://stackoverflow.com/a/5301093/990616
There was an issue reported for this. Here is the link. This issue has been closed as WorkingAsIntended. Wish means we can expect the GridView to call getView() on pos 0 multiple times.
My work around is as follow:
public class GridAdapter extends BaseAdapter {
...
int previouslyDisplayedPosition = -1;
...
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(previouslyDisplayedPosition != position) {
....
previouslyDisplayedPosition = position;
}
return convertedView;
}
What I am trying to do here is returning the same 'convertView' if same pos is called again and again. There by preventing any logic within getView() (eg setting image view etc)to be executed again and again.

Android Listview: getView being called multiple times on unobservable views

(I have already seen this similar question)
I have a ListView for which I've written a custom adapter, and an onitemclicklistener. I'm having an issue where, when any element of the list is selected, getView is called (twice) for each of the top 4 elements of the ListView, even if those elements are not visible. This happens even if I don't call notifyDataSetChanged on the adapter - those first 4 views are fetched twice regardless. Is this normal behavior? My issue is not as much that it's being called twice for them, but that it is being called at all when updating them is not needed.
By the way, I am not using wrap_content for the height or width of the listview - the height is match_parent and the width is a fixed number of dp.
The onItemClick() method of the OnItemClickListener is here:
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
mPicture = pictures[position];
mPicturesAdapter.setCurrentPicture(mPicture);
mPicturesAdapter.notifyDataSetChanged();
}
getView() from my custom Adapter (which extends BaseAdapter) is here:
public View getView(int position, View convertView, ViewGroup parent) {
Log.v("tag", "Getting view for position "+position);
LayoutInflater inflater = LayoutInflater.from(mContext);
LinearLayout layout = (LinearLayout)
inflater.inflate(R.layout.picture_thumbnail, parent, false);
// set up the linearlayout here ...
return layout;
}
On any item click, getView() is called for positions 0 - 3 twice regardless of which item was clicked.
Just by modifying the adapter via
mPicturesAdapter.setCurrentPicture(mPicture);
the ListView already tries to update itself. I'm guessing the onClick method will still do it's job without you calling notifyDataSetChanged
Actually whatever List/Group you are using to populate the ListView, you need to first empty it and then recall it. For example, if you use ListA to populate the ListView, in the second or any consecutive update you need to empty the ListA first and then add items and then populate using it.
if (convertView != null){
Then populate list
}

ListView backgroung color for each row android

Im changing the background color of each row overriding onListItemClick with this piece of code
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// First time touching the item
if(mLastPosition==-1) {
mLastPosition = position;
} else {
mLocalBG.remove(String.valueOf(mLastId));
v = l.getChildAt(mLastPosition);
v.setBackgroundColor(Color.WHITE);
mLastPosition = position;
}
mLocalBG.add(String.valueOf(id));
/* The issue: v is returning null right here, but only when touch a row that was below the first ones displayed, rows that needed to be scrolled down */
v = l.getChildAt(position);
v.setBackgroundColor(Color.LTGRAY);
}
As noted, it works perfectly till i scroll down to the next rows, after that View v returns null everytime. Why is that happening?
Thanks in advance
Well this is years old, but this line doesn't do what you think it does:
v = l.getChildAt(position);
See ViewGroup#getChildAt(). This method isn't overridden for ListView (though perhaps it should be) and only gives you visible views. Why? Off-scroll views don't exist! Remember, the same handful of views are recycled to save memory at the expense of weirdness like this.
Try this:
v = l.getChildAt(position - l.getFirstVisiblePosition());
You'll also want to set the correct background in your adapter's getView().
Adding more info:
IMHO, it's probably happening in the rows that newView wasn't called. :/

ListActivity registers correct position in onClick, but calls to its ListAdapter's getView() always count from 0

I have a ListActivity whose ArrayAdapter-subclass creates a (droid-fu) WebImageView. The problem is that whenever the list grows longer than can be displayed on a single page, whatever it is that calls getView() always starts from 0 and counts to 4 or 5 (depending on whether or not partial rows are visible), even when it's actually displaying an item like #12 or 14 at the top of the screen... but onClick() registers properly.
Put another way, suppose the list has 27 rows in it, and I have the app littered with Log.d statements to document everything it's doing. When the ListActivity begins, I can see it call getView with position = 0, 1, 2, 3, 4, and 5. If I touch one of the rows, onClick() fires with the correct position 0..4 (5 is offscreen) as well. So far, so good.
Now, flick the list upward by a few rows and watch the log output. Once again, getView() gets called for position=0, 1, 2, 3, 4, and 5. But if I touch one of the rows, onClick() fires for a position like 9 or 14 (which, in fact, is correct).
In other words, somewhere along the way, whatever is responsible for making the call to the ListAdapter's getView() method to render the list is forgetting about its current offset, and is always counting from 0 instead of from whatever row is, in fact, at the top of the screen.
Does anybody have any idea where to even begin looking for what might be going wrong here? Perhaps a clue about where those calls to getView() are coming from, and how the value of the position arg used for that call is determined? God forbid, is it possible to put a breakpoint at the start of getView(), then step BACKWARDS with Eclipse to see where the call to the ArrayAdapter's getView() method came from?
Update: forgot about github & managed to get a copy of my program now:
private class ChatInfoAdapter extends ArrayAdapter<ChatInfo> {
private ArrayList<ChatInfo> items;
public ChatInfoAdapter(Context context, int resourceID, ArrayList<ChatInfo> items) {
super(context, resourceID, items);
this.items = items;
// ^^^hmmm... could THIS be the problem somehow?
}
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("getView", "postion=" + position);
View v = convertView;
if (v==null) {
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.row, null);
}
ChatInfo info = items.get(position);
// ^^^ grabbing from local "items", not superclass' ArrayList. Hmmm...
if (info != null) {
TextView x = (TextView) v.findViewById(R.id.x);
TextView y = (TextView) v.findViewById(R.id.y);
//...
String url = info.getUrl();
//...
WebImageView wiv = new WebImageView(getBaseContext(), iconUrl, true, arg, arg2);
// ^^^ my slightly-hacked droid-fu WebImageView that takes some extra args
// Even if I completely screwed it up, this class should have no effect on
// the value of "position" when getView gets called at runtime, right?
// Or is there a method of ImageView that might be getting mangled somewhere
// along the line that SHOULD be reporting its position, but instead might
// be returning 0 (which might explain why position always starts from 0).
LinearLayout placeholder = (LinearLayout) v.findViewById(R.id.placeholder);
placeholder.addView(wiv);
v.setTag(info);
}
}
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Log.d("onListItemClick", "position=" + position);
//...
}
}
So... going back to the example list with ~20+ items... when it first appears, I see a flurry of activity like this in the log:
getView: position=0
getView: position=1
getView: position=2
getView: position=3
getView: position=4
getView: position=5
(possibly in a different order, or repeated a few times)
At that point, if I click on the last row, I'll see a message in the log like:
onListItemClick: position=4
So far, so good. But if I scroll the list a bit, so an item ~2/3 of the way down is shown at the top of the screen, and I click the row at the bottom of the screen, I'll see a flurry of activity like THIS in the log:
getView: position=0
getView: position=1
getView: position=2
getView: position=3
getView: position=4
getView: position=5
(possibly in a different order, or repeated a few times)
...
onListItemClick: position=20
In other words, it knows I clicked row 20, but the positions shown by the calls to getView never make it above position=6 or position=7, and they always begin from position=0.
(added other comments below, and made video illustrating problem)
How exactly do you check what positions are being used with getView()? ListView would not work at all if getView() did not receive the correct positions. And from what you are saying your list is displaying the correct items so the adapter must have received the correct positions in getView(). Are you talking about getChildAt()/getChildCount() maybe?
First of all, I don't know how WebImageView works but it seems to have a callback and within its implementation a method must update a view.
Working with holders is a bit dangerous because if you bind a holder to a method (i.e. setting it final) and you scroll the list, it's probable that this holder is being reused with another item. Within the callback you are using a holder, but because it is being reused by another item, you will be updating that item, therefore a wrong view.
I recommend to everyone that follow this one very basic rule:
1. Never use a holder object retrieved by getView() method. Instead, use this method when you have to update a view:
private ViewHolder getHolder (int position){
//position is the "real" position in the list
int first = listView.getFirstVisiblePosition ();
int last = listView.getLastVisiblePosition ();
if (position >= first && position <= last){
//The item is visible, so you have to update the view with the changes that you consider
return (ViewHolder)listView.getChildAt (position - first).getTag ();
}else{
//The item is not visible, so you don't have to update any view. If you want you can update the linked item returned by getItem()
return null;
}
}
Usage:
aMethod (new aListener (){
#Override
public void onCallback (int position){
//Update item if you want
Item item = getItem (position);
...
//Obtain the holder
ViewHolder holder = getHolder (position);
//If it's null you don't have to update anything because the item is not visible
if (holder != null){
//Update view
...
}
});
As you can see, the callback returns the position as a parameter, but is not necessary, it can be final and passed as a parameters through the method or similar. I've been studing and testing how the holders are working for about 2 days (16 hours aprox) and this method has solved me all the problems.
Note: You can use the holder directly if you are inside the getView() and want to do immediate updates. Inside callbacks you always have to use my getHolder() method.

Categories

Resources