I have a ListView which works with Custom-Item-Templates.
Inside the List-Item Template are ImageViews (used as Buttons) and several Strings. The Data is bound with CustomAdapter.
I want to to do specific things inside the ListView with each Item on clicking different ImageViews.
I come from .NET and WebDevelopment and very new in Android.
How can I achieve to check which Button or Image in which ListItem was clicked.
Normally i would give each element in ListItem custom attribute whith unique_id including a key from the DataItem and get that Key in a Click-Event of Buttom and so on.
Is there any opinion to give a ImageView a Custom Attribute like 'MyCustomID' and get that in the Click-Event from it? Or how is the usual workflow for a situation like this: ListItems with repeating Elements and checking what ListItem belongs to the clicked Element inside.
Please help!
Try with this
listView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// position is the index of clicked item.
}
});
First of all, I'd recommend to change to RecyclerView. It is more performant, flexible and easier to use. Here you can check how to do it.
About your question: I don't remember exactly how to implement ListView, but I believe that you have an adapter. In that adapter, you are creating the view of your items. So, what you can do is create a listener interface that receives the info of your items. Something like:
interface OnImageItemClickListener {
fun onImageClicked(imageInfo: ImageInfo)
}
data class ImageInfo(
val id: Long,
val uri: URI,
)
Then you can pass an implementation of that interface to your adapter, and then use it in the View.setOnClickListener method:
class CustomAdapter(
private val listener: OnImageItemClickListener,
private val info: List<ImageInfo>,
) : ... {
override getView(position: Int, convertView: View, parent: ViewGroup): View {
// Inflate your view...
view.setOnClickListener {
listener.onImageClicked(info[position])
}
}
}
Again, I highly recommend to migrate to RecyclerView. If you implement it, you'll have to follow the same steps.
Related
Can someone explain the logic on how to handle this matter:
I have a fragment that after a WebSocket call inflates 2 Recyclerviews.
Child Recyclerview is nested to Parent Recyclerview and the parent adapter calls the child adapter.
I want to put an Interface for a click listener which handles the click in the Child Items in the Fragment.
Where should I put the interface and which class should implement it?
What you're trying to do has been done multiple times.
There are various approaches you can try, but in general, responsibilities would look something like this:
YourContext (Fragment/Activity)
Inflates a layout with a RecyclerView.
Instantiates YourAdapter
Subscribes, Requests, Waits, for your data and passes it onto YourAdapter.
Maintains an interface for click handling, like:
interface YourThingClickHandler {
fun onThingWasClicked(thing: Thing) // and any other thing you may need.
}
Can be YourContext: YourThingClickHandler or if you want, you can keep an anonymous/local instance of that. I usually do the former and then implement the fun onThingWasClicked(...) in the fragment/activity, it depends what you need to do when the item was clicked.
YourAdapter
Expects a list of Things and one YourThingClickHandler instance. So in your Fragment/Activity you'd do, something like (pseudo code):
// This is called once your ViewModel/Presenter/Repository/etc. makes the data available.
fun onThingsLoaded(things: List<Thing>) {
adapter.setClickHandler(this) // this can be passed when you construct your adapter too via constructor like adapter = YourAdapter(this)
adapter.submitList(things) // if the adapter extends a `ListAdapter` this is all you need.
}
Now that you've passed an outer click handler, you need to deal with the inner list. Now you have a few choices:
1. pass the same click handler all the way in and let the innerAdapter directly talk to this.
2. Have the outerAdapter act as an intermediate between the clicks happening in the innerAdapter and bubble them up via this click handler you just supplied.
Which one you chose, will depend largely on what you want to do with it, and how you want to handle it. There's no right or wrong in my opinion.
Regardless of what you do, you still need to get from the view holder to this click handler...
So in YourAdapter you should have another Interface:
interface InternalClickDelegate {
fun onItemTappedAt(position: Int)
}
This internal handler, will be used to talk from the viewHolder, back to your Adapter, and to bubble the tap up to the external click handler.
Now you can have a single instance of this, defined like so in your adapter class (remember this is Pseudo-Code):
private val internalClickHandler: InternalClickDelegate? = object : InternalClickDelegate {
override fun onItemTappedAt(position: Int) {
externalClickHandler?.run {
onThingWasClicked(getItem(position))
}
}
}
So if the external click handler (the YourThingClickHandler you supplied) is not null, then fetch the item from the adapter's data source, and pass it along.
How do you wire this internal handler with each view holder?
When you do onCreateViewHolder, have a ViewHolder that takes... you guessed, a InternalClickDelegate instance and so...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
// decide which viewHolder you're inflating... and...
return YourViewHolder(whateverViewYouInflate, internalClickHandler)
Now your ViewHolder(s) have a reference to this internal click handler...
so when you do onBindViewHolder(...) you probably call a common ViewHolder method of your choice, for example if your View holder can be of different types, you probably have an Abstract viewHolder with a fun bind(thing: Thing) method or similar that each concrete viewHolder subType will have to implement... in there, you'd do something like this:
override fun bind(thing: Thing) {
if (clickHandler != null) {
someViewYourViewHolderInflated.setOnClickListener(this) // this assumes your ViewHolder implements View.OnClickListener from the framework
}
}
Because your ViewHolder implements View.OnClickListener, you must implement the onClick method in it:
override fun onClick(v: View?) {
clickHandler?.onItemTappedAt(adapterPosition)
}
And this is how your ViewHolder, will receive the tap/click event from Android in the onClick method, if you supplied a click Handler (you did in the adapter onCreateViewHolder when you passed the internalClickHandler), it will simply bubble the tap, passing the position. adapterPosition is the Kotlin equivalent of calling getAdapterPosition() in a RecyclerView adapter.
TOO LONG, DIDN'T READ GRAPH
Fragment: ExternalClickListener -> passes an instance of it to the Adapter.
Adapter: Receives the ExternalClickListener, passes an InternalClickListener to each ViewHolder.
ViewHolder: Receives the internal Click Listener, sets itself as Clickable (either the entire itemView or just any widgets you want to make clickable, if you want the whole cell to be clickable, simply use itemView which is the "whole" view of the ViewHolder.
When the viewHolder's view is tapped, android calls the click listener's onClick method. In there, and because you are in a ViewHolder, you can do getAdapterPosition() and pass this to the internal click handler you received.
The Adapter then can transform that position back into data, and because you supplied an External clickListener, it can pass the actual item back to the external click listener.
Wait, but how about a NESTED RecyclerView.
There's nothing special about that, you simply need to provide the same mechanism, and keep passing things around. What you do or how many of these interfaces you have, depends entirely on what you're trying to achieve; like I said at the beginning, each solution is different and other factors must be taken into account when making architectural decisions.
In general, keep this thing in mind: Separation of Concerns: keep things small and to the point. For E.g.: it may seem crazy to have this double interface, but it's very clear what each does. The internal one, is simply concerned about a "tap" in a "view", and to provide the position in a list where said tap occurred.
This is "all" the adapter needs to fetch the data and make an informed guess at what item was truly tapped.
The fragment doesn't know (or care) about "positions", that's an Adapter's implementation detail; the fact that positions exist, is oblivious to the Fragment; but the Fragment is happy, because it receives the Thing in the callback, which is what most likely needs to know (if you needed to know the position for whatever reason, tailor and modify the externalCallback to have the signature of your choice.
Now replicate the "passing hands" from your OuterAdapter to your InnerAdapter, and you have done what you wanted to do.
Good luck!
1) You should put interface in child adapter and implement that in parent and then pass another one interface (long peocess)
2) Use local broadcast manager
you will add ClickListener in parent adapter and also add it in constructor of adapter
public interface HandleClickListener
{
void onItemClicked(int position, SurveysListModel surveysListModel);
}
Make an instance of your clicklistener and then on holderclick listner get the position of item and its value from your model list
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
handleClickListener.onItemClicked(position, yourModelList.get(position));
});
and get in to your activity like this making an instance of you adapter
adapter = new InstrumentsSearchAdapter(yourModelsList, activity.this, new SearchAdapter.HandleClickListener() {
#Override
public void onItemClicked(int position, Model listModel) {
instumentChild = listModel.getInstrument_title();
Intent intent = new Intent(Activity.this, Listing.class);
intent.putExtra("selectedQuestions", listModel.getQuestions());
startActivityForResult(intent, 5);
}
});
And if you want to go to parent recyclerview class implement onActivityResutlt method and get data back from child through intent and get that intent in onActivityResutlt method
I am using a RecyclerView to show list of products in my app, I need to group the product based on aisle. while the data are fetched for the first time in the list, the products are grouped correctly with respect to aisle. When we scroll the view, the aisle group divider is shown for the wrong item and the divider gets restored to correct position once the onBindViewHolder gets refreshed automatically.
MyAdapter.class
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
}
private fun itemsGrouping(pickItem: PickItem, pickItemView: View, adapterPosition: Int) {
//Based on some condition
if(SomeCondition)
itemDivider(pickItemView,true)
else
itemDivider(pickItemView,false)
}
private fun itemDivider(v: View, boolean: Boolean) {
if(boolean) {
v.visibility = View.VISIBLE
} else {
v.visibility = View.GONE
}
}
Well, you should know that the view holders are reused in the RecyclerView, so it's probable not the right idea to try to determine the visibility of the divider in onBindViewHolder. I would recommend using item decorator for dividers. Here's the question and answer for that
How to add dividers and spaces between items in RecyclerView?
The problem is RecyclerView recycles previous views in order to be efficient.
I guess "SomeCondition" contains artifacts which are from previous holders.
So at
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
you should get pickItem and pickItemView from newly bound holder. You should use like
pickItemView = holder.findViewById(R.id.pickItemView);
Or consider using DataBinding Library
Here is a good example (it's in Kotlin) : DataBoundListAdapter
Once you extend your adapter to DataBoundListAdapter and override bind() method, everything inside bind is executed for every row, so you won't get repeated results.
Note : notice "executePendingBindings()"
I am having trouble debugging a recycler view issue. Here I am trying to delete an item from recycler view using the following method:
when I click on the card (one that is to be deleted), it will store the title of a card in shared preference. (so that in future when I open the app I will know what cards are not to be displayed)
Then I am calling a method which checks if the title in shared preference matches the title of the card and make ArrayList consisting of cards which are not present in shared preference.
Now I am using this ArrayList to fill cards using notifyDataSetChanged
My code is as Follows.
Adapter.
class LocalAdapter(var localCardsInfo : ArrayList<LocalModel>?,var fragment: LocalListingFragment) : RecyclerView.Adapter<LocalHolder>() {
fun refreshDataOnOrientationChange(mLocalCards : ArrayList<LocalModel>?){
if (localCardsInfo!!.size>0)
localCardsInfo!!.clear()
localCardsInfo = mLocalCards
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return localCardsInfo!!.size
}
override fun onBindViewHolder(holder: LocalHolder, position: Int) {
if (localCardsInfo!=null)
holder.updateUI(holder.adapterPosition,localCardsInfo!![holder.adapterPosition])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalHolder {
val card_a : View = LayoutInflater.from(parent.context).inflate(R.layout.card_local_a,parent,false)
return LocalHolder(card_a,fragment)
}
}
ViewHolder
class LocalHolder(itemView : View,val fragment: LocalListingFragment) : RecyclerView.ViewHolder(itemView),OpenedLayoutManagerLocal{
fun updateUI(position : Int , localModel: LocalModel){
moveButton.setOnClickListener({
archived()
})
private fun archived(){
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(fragment.activity!!,model!!.roomName,model!!.deviceName)
itemView.archiveLayout.visibility = View.GONE
itemView.elevateLayoutLocal.visibility = View.GONE
fragment.mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(fragment.activity!!)))
}
For example, if I have six item in recycler view and when I delete one I can see 5 items on the list while debugging, even onbind is called 5 times but only 4 items are displayed.
I tried my best to debug it yet failed to find a solution and I can't think about what to do next. I tried notifyItemRemoved and notifyItemRangeInserted also but still, I am facing the same issue. It would be a great help if someone can suggest a possible cause of such issue.
Thank you.
Edits:
I just experimented by adding dummy button in fragment where recycler view is placed. An to my surprise if I delete element here just element at that position gets deleted and there is no case of disappearing items. So it seems like problem happens when I delete an item from view holder. So I guess I got the cause of the error but can't think of a way to implement it because the original button is in the card so I am forced to use delete procedure inside view holder. Please do suggest if you have any suggestions. Code that I added in fragment is as follow:
v!!.findViewById<Button>(R.id.delete_button).setOnClickListener({
try {
val model = LocalDataService.ourInstance.getNonArchivedItems(activity!!)[0]
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(activity!!,model.roomName,model.deviceName)
mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(activity!!)))
}catch (e : Throwable){}
})
Here on this new dummy button click, it deletes the first item in the list gets deleted while on clicking the button on RecyclerView card it deletes one item and then one more item gets disappeared.
I have a listview. What I've implemented in that listview is that when user clicks a list item a 2 button view is inflated to replace the content of that list item like this:
This works fine but what I want is when I click the second list item the first one should come back to its original layout. Currently, it is like this:
This is my code implemented in onClick method of listview:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TextView planName = view.findViewById(R.id.planNameText);
TextView planDate = view.findViewById(R.id.planDateText);
ImageView planImage = view.findViewById(R.id.homePlanImageView);
planName.setVisibility(View.INVISIBLE);
planDate.setVisibility(View.INVISIBLE);
planImage.setVisibility(View.INVISIBLE);
RelativeLayout rl_inflate = (RelativeLayout)view.findViewById(R.id.rl_inflate);
View child = getLayoutInflater().inflate(R.layout.inflate, null);
rl_inflate.addView(child);
}
});
Thanks.
Maybe you need to initialice a boolean variable to check if is clicked or not and refresh all the views. I really recommend you to use a RecyclerView and use 2 viewHolders. If you want information about this check this. If you implement a recycler with 2 viewholder it will be easier than the way that you want to implement it, and you can use notifyDataSetChanged to refresh the recycler. Whatever, you will need anyways a boolean to check if is clicked or not.
I am building a component based on RecyclerView, allowing user to reorder items by drag and drop.
Once I am on the DragListener side, I need the position it has in the adapter in order to perform correct move, but I only have access to the view.
So here is what I am doing in the adapter view binding :
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Track track = mArray.get(position);
viewHolder.itemView.setTag(R.string.TAG_ITEM_POSITION, position);
}
Does it seem correct to you ?
Because if I move an item like this :
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
}
then position tag is not correct anymore, and if I notifyDataSetChanged(), I lose the fancy animation.
Any suggestion ?
There is a way to preserve fancy animations with just notifyDataSetChanged()
You need to make your own GridLayoutManager with overriden supportsPredictiveItemAnimations() method returning true;
You need to mAdapter.setHasStableIds(true)
The part I find tricky is you need to override you adapter's getItemId() method. It should return value that is truly unique and not a direct function of position. Something like mItems.get(position).hashCode()
Worked perfectly fine in my case - beautiful animations for adding, removing and moving items only using notifyDataSetChanged()
No, it is wrong. First of all, you cannot reference to the position passed to the onBindViewHolder after that method returns. RecyclerView will not rebind a view when its position changes (due to items moving etc).
Instead, you can use ViewHolder#getPosition() which will return you the updated position.
If you fix that, your move code should work & provide nice animations.
Calling notifyDataSetChanged will prevent predictive animations so avoid it as long as you can. See documentation for details.
Edit (from comment): to get position from the outside, get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details
1) You'll use notifyItemInserted(position); or notifyItemRemoved(position); instead of notifyDataSetChanged() for animation.
2) You can just manually fix your problem - using
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
ViewHolder fromHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(from);
ViewHolder toHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(to);
Tag fromTag = fromHolder.itemView.getTag();
fromHolder.itemView.setTag(toHolder.itemView.getTag());
toHolder.itemView.setTag(fromTag);
}
You should move your method to OnCreateViewHolder, then notifyItemRemoved(index) works properly.
I'm able to maintain the touch animations by adding this to my list item's outer element
<View
android:foreground="?android:attr/selectableItemBackground"
...>
I fixed it with using 'notifyItemChanged(int position);' instead of 'notifyDataSetChanged();'
My adapter shows fancy animations perfectly and without any lags
Edit: I got position from onBindViewHolder's position.
as stated by others above, you can have animation while using notifyDataSetChanged on your adapter, although you need to specifically use stable ids. if your items IDs are strings, you can generate a long id for each string id you have and keep them in a map. for example:
class StringToLongIdMap {
private var stringToLongMap = HashMap<String, Long>()
private var longId: Long = 0
fun getLongId(stringId: String): Long {
if (!stringToLongMap.containsKey(stringId)) {
stringToLongMap[stringId] = longId++
}
return stringToLongMap[stringId] ?: -1
}
}
and then in your adapter:
private var stringToLongIdMap = StringToLongIdMap()
override fun getItemId(position: Int): Long {
val item = items[position]
return stringToLongIdMap.getLongId(item.id)
}
another useful thing to consider, if you are using kotlin data class as items in your adapter, and you don't have an id, you can use the hashCode of the data class itself as stable id (if you are sure that the item properties combination are unique in your data set):
override fun getItemId(position: Int): Long = items[position].hashCode().toLong()