Android: Determine which View clicked within ListView row - android

I have a ListView with lots of icons which may be clicked to change the state of their respective row items. I realise that I can create onClick handlers for all of them but I would like to a generic means of identifying which icon (View) has been clicked. (e.g. determine View at touch coords x,y?
Any idea how I can do that?
I am getting row clicks with the below handler:
lvClickListener = new AdapterView.OnItemClickListener()
{
#Override
public void onItemClick( AdapterView<?> arg0, View arg1, int position, long arg3 )
...
On each row I have 5 ImageViews.

I assume all rows are being inflated with the same layout and you are using the holder pattern (http://developer.android.com/training/improving-layouts/smooth-scrolling.html).
This way, you can have a single OnClickListener to "listen" for any of your ImageView (or whatever they are). You only need to set the listener in the momento you actually inflate the view (not when you reuse it), and then set for all ImageView in the same row the row position as the Tag, so, then in the onClick method, you can chech to which row they belong to (by the tag) and which ImageView is it (by the id)
class MyAdapter extends BaseAdapter implements OnClickListener
[...]
#Override
public View getView (int position, View convertView, ViewGroup parent)
{
MyHolder holder = null;
if(convertView != null)
{
holder = new MyHolder();
convertView = inflater.inflate(...)
holder.imageView1 = (ImageView)convertView.findViewById(R.id.[...] )
holder.imageView1.setOnClickListener(this);
holder.imageView2 = (ImageView)convertView.findViewById(R.id.[...] )
holder.imageView2.setOnClickListener(this);
[...]
convertView.setTag(holder);
}
else
{
holder = (HyHolder)convertView.getTag();
[...]
}
holder.imageView1.setTag(position);
holder.imageView2.setTag(position);
[...]
return convertView;
}
#Override
public void onClick(View v)
{
int id = v.getId();
Integer position = (Integer)v.getTag();
[...]
}
Of course you can then optimize this, put the views in an array or whatever or put the OnClickListener outside the Adapter (in the activity, or an instance variable), but this is the basic idea.
As a note, you should check what happens with the onItemClickListener, I'm not pretty sure, but it may cause problems intercepting touches (or it may no, I don't know), but if you're not going to use the whole row click, then you can remove it

You can do this in multiple ways, You can tag each ImageView to your AsyncTask(or whatever) you use. Or, refer https://github.com/square/picasso

Related

ListView: how to access Item's elements programmatically from outside?

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.

Why are the first and last items of my ListView telepathically linked?

I'm trying to get a ListView to toggle the background color of its elements when they are selected (this is for selecting songs to add to a playlist). I've been mostly successful, but for some reason whenever the ListView is longer than the screen, selecting either the first or last item also toggles the background of the other. I keep track of whether an item is selected or not in an array of booleans called selectedStatus, and there's not a problem there, so it's purely a UI problem.
Here's the relevant section of the code:
boolean selectedStatus{} = new boolean[songsList.size()];
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
lv = getListView();
// listening for song selection
lv.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
int viewPosition = position - lv.getFirstVisiblePosition();
if(selectedStatus[position]){
selectedStatus[position] = false;
View currentEntry = lv.getChildAt(viewPosition);
currentEntry.setBackgroundResource(R.color.footercolor);
}
else{
selectedStatus[position] = true;
View currentEntry = lv.getChildAt(viewPosition);
currentEntry.setBackgroundResource(R.color.selected);
}
}
});
}
There must be some implementation detail about ListViews that I'm missing, but I can't figure out why this would be happening.
EDIT: I've realized with more testing that it actually links all list elements with positions which are equal mod 12, I just wasn't looking at a long enough list. This seems much less weird, it's just reusing elements, and I'll have to look into this ViewHolder idea.
Since a few people asked, this is all the code I have for making an adapter and populating the list:
// Adding items to ListView
ListAdapter adapter = new ArrayAdapter<String>(getActivity(),
R.layout.playlist_builder_item, songnamesList);
setListAdapter(adapter);
Sounds like you might need to create a proper ListAdapter and implement a ViewHolder.
The ViewHolder avoids the layout reuse that Android ListView implements, so as you can do slightly more complicated things by relying on the Views being the same as before.
You should hold onto the View that you're changing in a static class. For example:
static class ViewHolder {
ImageView backgroundItem;
}
Then in your Adapter's getView method you want to get hold of that ViewHolder and if we are creating a new view, we set it to be new, otherwise we reuse the old view that we have set as a tag to the original.
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
// Inflate the view as we normally would
// Create a new ViewHolder
// Set our ImageView to be equal to viewHolder's backgroundItem
// final step
convertView.setTag(viewHolder);
}else{
// use the original ViewHolder that was already saved as a tag
viewHolder = (ViewHolder) convertView.getTag();
}
// Set the background as per your own code
// Return the convertView
return convertView;
}
Don't forget to set your Adapter to your listview by calling the ListView setAdapter(ListAdapter adapter) method
A decent tutorial which incorporates creating an Adapter and a ViewHolder can be found here.
Further reading:
Vogella article on ListViews
Android Developer post on ListViews

How can I handle onListItemClick(...) based on what view inside the row was clicked?

Question
I have a listView inside a DialogFragment and I want to fire certain callbacks only when certain particular items inside a row are fired. How can I do that?
Basically, I want to do something like this
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final int viewId = view.getId();
if ((viewId == R.id.textView1) || (viewId == R.id.textView2)) {
// do something...
}
which I can't. Read further if you don't know why.
What I tried
I tried to look into the documentation, but the OnItemClickListener callback doesn't offer as a parameter the exact clicked view (the View you can see in the signature is the whole row).
Also, I tried to set a simple onClick callback on the single view in the adapter, but this overrides the listSelector and other behavior a list should have. Reading in the documentation, I found it's explicitly written that we should set callbacks via the onListItemClick(...) method (not via onClick(...)), so I'm looking for a way to do that, using this method, not to override any default list behavior.
I was trying to get this done by working on the xml. To my surprise, I found that if I set a view android:clickable property to true, the onListItemClick callback won't fire (I thought it was the opposite),
so a partial solution would be to set to android:clickable=true every view in the row apart from the one I want to fire the callback, but that is not a solution because if the user clicks where there is padding or white space, the callback will fire. Also, I found that if I set the parent of the row's view to android:clickable=true and the child views I want to handle with the callback to android:clickable=false, this won't work, because apparently the property is not overwritten.
EDIT Sorry for the really bad title this question had before, I didn't even noticed I submitted the question.
new Answer, hope I understood now :)
In your adapters getView, attach an OnClickListener to any view in your layout you want to fire. (more pseudocode)
public class Adapter extends ArrayAdapter<XYZ> {
private int resource;
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView==null) convertView = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(this.resource, parent, false);
((Button)convertView.findViewById(R.id.YOUR_BUTTON_IN_LAYOUT)).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
DOSTUFF();
}
});
return convertView ;
}
}
old Answer:
The position indicates where you are in the list (pseudocode).
listView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> arg0, View arg1, final int position,long arg3) {
YOUR_ITEM_BACKED_BY_ADAPTER item = listView.getItemAtPosition(position);
if(item==THE_FIRST_ITEM_IN_LIST) doSomething();
else if(item == THE_LAST_ITEM_IN_LIST) doSomethingElse();
}
});
You can set listeners for other views inside the adapter's getView
public class MyAdapter extends ArrayAdapter<MyItem> implements View.OnClickListener {
public View getView(int position, View convertView, ViewGroup parent) {
// setup the converView inflating it, for simplicity I've removed that code
MyItem item = getItem(position);
text1 = (TextView)convertView.findViewById(R.id.text1);
text2 = (TextView)convertView.findViewById(R.id.text2);
text1.setOnClickListener(this);
// pass the item to use when clicked
text1.setTag(item);
text2.setOnClickListener(this);
text2.setTag(item);
}
public void onClick(View v) {
MyItem item = v.getTag();
switch (v.getId()) {
case R.id.text1:
download(item);
break;
case R.id.text2:
upload(item);
break;
}
}
}
Instead of hardcoding action (eg download) inside the adapter you can pass to it an interface and for example the calling activity can implement that interface

ListViewAdapter - when listeners, attached to view are collected?

I am using Custom ListViewAdapter for displaying, well, a list.
Each row in the list has 3 buttons, i.e. listeners attached.
But I am finding it very disturbing, that during each scroll the new OnClickListeners are being created, even for those rows, where convertView exists, as a non-null value.
// The most common approach to convert view, as I understand:
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.listview_item, parent, false);
} else {
view = convertView;
}
final TextView textView = (TextView) view.findViewById(R.id.txtProduct);
textView.setText(name);
textView.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
...
}
});
... two more listeners, with the same approach
return view;
as per my experience with Java, Spider-sense "ting-a-lings" - seems that creating and throwing away the same listener approach is garbage-collector abusing.
I am not sure when the old listener had been collected, if it had...
Is there a way to use the old listener, instead of creating a new one? (some kind of cache data structure)
You can set your List Adapter class to inherit from the View.OnClickListener interface. Then, simply set
textView.setOnClickListener(this);
And handle the click in your adapter class' onClick method. To know, for example, which row is clicked, add this line prior to the one above:
textView.setTag(position);
Then, in onClick, you can know which position in the list you are handling by getting this tag:
public void onClick(View v) {
Object item = myList.get((Integer) v.getTag());
//handle click event
}

How can I get the position of the item within a listview when I click on a specific view?

As the title says I want to know the exact position of the item when I click on a view that is inside the item.
Suppose I have the following code within the getView() method from ArrayAdapter:
...
holder = new ViewHolder ();
holder.iconAction = (ImageView)convertView.findViewById (R.id.download_item_iconAction);
holder.iconAction.setOnClickListener (new View.OnClickListener(){
#Override
public void onClick (View v){
//Item X is clicked
}
});
...
Within onClick() I know the view that is clicked, v, but I don't know the position of the item.
Let's do a trick. I'm going to save the position in a ViewHolder when getView() creates the view:
public View getView (int position, View convertView, ViewGroup parent){
ViewHolder holder;
if (convertView == null){
holder = new ViewHolder ();
holder.iconAction = (ImageView)convertView.findViewById (id);
holder.iconAction.setOnClickListener (new View.OnClickListener(){
#Override
public void onClick (View v){
int pos = (Integer)v.getTag ();
}
});
holder.iconWait.setTag (position);
...
}else{
holder = (ViewHolder)convertView.getTag ();
}
...
}
This code works... but not always. If you have to scroll the list to see all the items the views are recycled. Suppose that the list has 10 elements and only 5 are visible (visible means: if we see 1 pixel line of an item, then this item is visible). Now, if we scroll down we will see the sixth element but the first (0) is still visible. We scroll a little more and the first element will be hidden and we will see that the seventh element appears, BUT the view of this new element is the view of the first element (0). So I'm saving the positions: 0, 1, 2, 3, 4 and 5. The seventh element (6) will have saved the position 0: Wrong.
Another way to get a click callback is using the ListView's OnItemClickListener listener:
listView = getListView ();
listView.setOnItemClickListener (new AdapterView.OnItemClickListener(){
#Override
public void onItemClick (AdapterView<?> parentView, View childView, int position, long id){
...
}
});
If I scroll down I get the exact position, but with this way I receive callbacks when the item is clicked, no matter the clicked child view.
Thanks.
You're very nearly there, all you need to do is set the tag after your if/else clause. This is to make sure the tag is updated when the view is recycled as well as when it is created from new.
e.g
if (convertView == null){
holder = new ViewHolder ();
...
}else{
holder = (ViewHolder)convertView.getTag ();
}
holder.iconWait.setTag (position);
Local anonymous classes can reference final local variables.
Make position final by changing your getView() method to read:
public View getView (final int position, View convertView, ViewGroup parent) {
and then you can reference position from within the OnClickListener:
holder.iconAction.setOnClickListener (new View.OnClickListener(){
#Override
public void onClick (View v){
... use position here ...
}
});
Your second answer (setting a tag on an element) would work fine if you moved holder.iconWait.setTag (position) outside of the if/then statement -- that is, you should set the tag on recycled rows too, not just on newly-inflated ones.
I also needed to add other data to pass to onClick and it worked with strings too.
holder.iconWait.setTag ("String data here");

Categories

Resources