I have a ListView that is refreshed at a high rate (3 times per second).
I need to catch a long press on such a ListView (as well as on the parent layout); the ListView has it's height set to wrap_content.
I can catch the long click on the parent layout, but I wish the long click on any item to be handled by the parent layout.
OnItemLongClick does not work well due to the high refresh rate, I have tried the onLongClickListener to the ListView but the the long click is not fired.
The rows are set as non-clickable, not-focusable as well as all the items contained in the row.
The question is how to handle a long click anywhere in the ListView if the position / item does not matter?
I faced similar issue (with clicks on ImageButtons) when I was updating progress of my downloads in a ListView.
The solution was to update the individual rows instead of calling adapter.notifyDataSetChanged() (as what I understand is it interferes with your click/touch listeners). So to do this I problem was to find the row accurately. I need two things for this:
mapping of URL strings to my Transfer objects (mPathToTransfers),
list of URLs (mPathKeys).
Here's some code from my adapter:
protected LinkedHashMap<String, BaseTransfer> mPathToTransfers;
protected List<String> mPathKeys;
#Override
public int getCount() {
mPathKeys = new ArrayList<String>(mPathToTransfers.keySet());
return mPathToTransfers.size();
}
#Override
public BaseTransfer getItem(int position) {
return mPathToTransfers.get(mPathKeys.get(position));
}
/**
* Update transfer (download) progress in ListView's specific row
*/
public void setItemTransferredSize(String path, long transferredSize, ListView lv) {
if (mPathToTransfers.containsKey(path)) {
BaseTransfer dItem = (BaseTransfer) mPathToTransfers.get(path);
dItem.setTransferredSize(transferredSize);
Holder holder = getViewHolderForTransfer(path, lv);
if (holder != null)
setTransferProgressUpdate(holder, dItem);
}
}
/**
* Use to get the View Holder of the row of the transfer
*
* #param path
* #param lv
* #return {#linkplain Holder} for row, if it is visible, null otherwise
*/
private Holder getViewHolderForTransfer(String path, ListView lv) {
int rowIndex = mPathKeys.indexOf(path);
if (lv.getFirstVisiblePosition() <= rowIndex && lv.getLastVisiblePosition() >= rowIndex)
return (Holder) lv.getChildAt(rowIndex - lv.getFirstVisiblePosition()).getTag();
else
return null;
}
If you have ambiguities, ask in comments below (or in your question's comments). :)
Related
I am using a browseSupportFragment element in my android tv app having headers enabled and a single row for every header
the problem is when i select the first item of every row by scrolling down, or when i select a header of the header list, the function getSelectedPosition return 0 always
it return the right index of row when i select the second item in the row
i am pretty sure that this is a bug !!
below the code of onItemSelected
#Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object
item, RowPresenter.ViewHolder rowViewHolder, Row row) {
int pos = getSelectedPosition();//this return 0 if i scroll down between headers
}
You will need to override it because for some reason it returns 0 by default.
/**
* Returns the selected position.
*/
public int getSelectedPosition() {
return 0;
}
You might need to override the setter as well because it is empty too.
/**
* Selects a Row.
*/
public void setSelectedPosition(int rowPosition, boolean smooth) {
}
It seems leanback support was left a bit undercooked.
the problem was need to add this peace of code
#Override
public int getSelectedPosition() {
return super.getSelectedPosition();
}
I have a RecyclerView with a Horizontal LinerLayout. It displays numbers from 10 to 1, that is used to rate something.
When I select 10 and scroll back to 1 and select 1. I have to update the UI to remove selection on 10 and update selection on 1. But, when I use findViewHolderForAdapterPosition() to remove the selection on 10 it gives me a NullPointerException
I am getting the position in the ViewHolder with getAdapterPosition().
Then, I use that position to get the ViewHolder by calling findViewHolderForAdapterPosition() on my recycler view object and update the UI to remove the selection from 10.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
vh.textRating.setBackgroundResource(R.drawable.rating_background_selected_orange);;
With some tests, I found out when I try to do the same thing without scrolling it works fine. However, only when I am scrolling it gives me a NullPointerException
How do I fix this?
As requested here is some important code from Adapter class.
#Override
public void onBindViewHolder(RatingRecyclerAdapter.ViewHolder holder, int position) {
String itemText = itemList.get(position);
holder.textRating.setText(itemText);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView textRating;
public ViewHolder(View itemView) {
super(itemView);
textRating = (TextView) itemView.findViewById(R.id.text_rating);
textRating.setOnClickListener(ratingClickListener);
}
private final View.OnClickListener ratingClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (callback != null) {
callback.onClickRating(v, position);
}
}
};
}
Activity Class
#Override
public void onClickRating(View view, int position) {
RatingRecyclerAdapter.ViewHolder vh;
int color;
int previousPosition = mAdapter.getSelectedPosition(); //Get previously clicked postion if any.
if (previousPosition == Constants.NO_ITEM_SELECTED) {
// An item was selected first time
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color);
return;
}
if (position == previousPosition) // Same item was selected
return;
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setUnselectedRatingResource(vh, color); // Remove the previous selected item drawables.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color); // Set the new selected item drawables. Setting some background to indicate selection.
}
As Sevastyan has written in the comment, the RecyclerView immediately recycles the view as soon as the item is out of the screen. So if we call findViewHolderForAdapterPosition() for a view which is outside the screen we get a null value. (I am not confirming this is the actual case. But, this is what it seems to me.)
So I created a class that stores all the data about an item in the RecyclerView and stored all the colours and value of that item in the class. And when we are populating the view, set the all the colours based on data stored in that class.
PS: I THANK Sevastyan for not giving me the answer directly. But, only giving me the reason for getting that Exception.
If your view is out of the screen, it can be recycled OR cached.
In case it's recycled, you can handle in onViewRecycled() method or setup the view again inside onBind() when the view becomes visible (you can save the state on the object of your list if needed).
In case it's not recycled (onViewRecycled method not called for that position), it's probably cached. You can set the cache size to zero to prevent this state from happening.
recycler.setItemViewCacheSize(0)
I have a ListView in an Android Activity and a custom adapter for that listview.
I want to be able to edit a row item and update that row instantly. This works, the modifications of the row is seen But, on scroll i loose all data.
This is my Asynk task from where i get the data and update the list row item:
/**
*
*/
public class EditNewsFeedPostAsyncTask extends AsyncTask<Void, Void, Boolean> {
public Activity context;
public String content;
public int rowPosition;
public ListView listView;
public TextView decriptionTxt;
#Override
protected Boolean doInBackground(Void... params) {
try {
token = Utils.getToken(context);
if (token != null) {
....
// {"status":"true"}
if (result != null) {
....
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
#Override
protected void onPostExecute(final Boolean success) {
if (success) {
updateListView(rowPosition, content);
}
}
public boolean updateListView(int position, String content) {
int first = listView.getFirstVisiblePosition();
int last = listView.getLastVisiblePosition();
if (position < first || position > last) {
return false;
} else {
View convertView = listView.getChildAt(position - first);
decriptionTxt.setText(content);
listView.invalidateViews();
return true;
}
}
private void updateView(int index, TextView decriptionTxt) {
View v = listView.getChildAt(index - listView.getFirstVisiblePosition());
if (v == null)
return;
decriptionTxt.setText(content);
listView.invalidateViews();
}
#Override
protected void onPreExecute() {
super.onPreExecute();
}
#Override
protected void onCancelled() {
}
}
What am i missing? shouldn't the data be persistent?
Thx
You must update the object in your listView adapter, not only the views!
after scrolling, the getView method inside your list's adapter will call and you will return the default view for that.
if you want to change that item permanent, you should update your data set and call notifyDataSetChanged on your adapter.
Make sure you're updating the data, not just the view.
When you modify the row, are you changing the underlying data or just the view? If just the view...
You're probably running into ListView recycling issues. This answer has a great explanation. Basically, ListViews are about efficiency in displaying views based on data, but are not good for holding new data on screen. Every time a ListView item is scrolled out of view, its View is recycled to be used for the item that just scrolled into view. Therefore, if you put "hi" in an EditText and then scroll it off screen, you can say goodbye to that string.
I solved this in my app by ditching ListView altogether and using an array of LinearLayouts (probably a clunky approach, but I had a known list size and it works great now). If you want to continue using a ListView, you'll have to approach it from a "data first" perspective. Like I said, ListViews are great at showing info from underlying data. If you put "hi" in an EditText and simultaneously put that string in the underlying data, it would be there regardless of any scrolling you do. Updating onTextChanged might be cumbersome, so you could also let each row open a dialog in which the user enters their data which then updates the underlying dataset when the dialog closes.
These are just some ideas, based on some assumptions, but editing views in a ListView is, in general, not very in line with how ListViews work.
I have one listview in my application,it contains two rows one for task and another one for alarm,date,severity. Initially first row of the list item only displayed for all list item and the second one is invisible. When i click the list item the second row displayed for that item as well as click another list item at that time the above list item closed that second row. Its working fine for me...My problem is if i open one list item and then swipe the listview at then i click the another list item at that time the above one cannot be closed because the above list item instance will be chnaged.please any one help me how to solve this problem...
int lastselectedPosition == -1
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position,
long id) {
TextView textviewDate=(TextView)view.findViewById(R.id.taskTimeidDaytoDay);
selectedtaskDate=textviewDate.getText().toString().trim();
if (lastselectedPosition == -1) {
Log.i(TAG,"Loopif:"+lastselectedPosition);
TextView twTaskTime = (TextView) view
.findViewById(R.id.taskTimeidDaytoDay);
TextView twSeverity = (TextView) view
.findViewById(R.id.severityidDaytoDay);
TextView twAlarm = (TextView) view
.findViewById(R.id.alarmidDaytoDay);
twAlarm.setVisibility(view.VISIBLE);
twSeverity.setVisibility(view.VISIBLE);
twTaskTime.setVisibility(view.VISIBLE);
lastselectedPosition = position;
lastSelectedItem = arg0.getChildAt(position);
} else {
// Log.i(TAG,"LoopElse:"+lastselectedPosition);
lastSelectedItem.findViewById(R.id.taskTimeidDaytoDay)
.setVisibility(lastSelectedItem.GONE);
lastSelectedItem.findViewById(R.id.severityidDaytoDay)
.setVisibility(lastSelectedItem.GONE);
lastSelectedItem.findViewById(R.id.alarmidDaytoDay).setVisibility(
lastSelectedItem.GONE);
if (lastselectedPosition != position) {
view.findViewById(R.id.taskTimeidDaytoDay).setVisibility(
view.VISIBLE);
view.findViewById(R.id.severityidDaytoDay).setVisibility(
view.VISIBLE);
view.findViewById(R.id.alarmidDaytoDay).setVisibility(
view.VISIBLE);
lastselectedPosition = position;
lastSelectedItem = arg0.getChildAt(position);
} else {
lastselectedPosition = -1;
lastSelectedItem = null;
}
}
GetView():
#Override
public View getView(int position, View view, ViewGroup parent) {
Log.i("XXXX", "Inside getView");
final DaytoDayTaskGetterSetter objDaytoDaygetset=getItem(position);
TextView textviewTask;
TextView txtviewAlarm ,txtviewTaskTime ,txtviewSeverity;
Log.i(TAG,"InsideGetView:"+position);
LayoutInflater inflater=(LayoutInflater)context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if(view==null)
{
view=inflater.inflate(R.layout.daytodaylistlayout,null);
}
Log.i("XXXX", "before first test");
textviewTask=(TextView)view.findViewById(R.id.tasknameidDaytoDay);
txtviewAlarm=(TextView)view.findViewById(R.id.alarmidDaytoDay);
txtviewSeverity=(TextView)view.findViewById(R.id.severityidDaytoDay);
txtviewTaskTime=(TextView)view.findViewById(R.id.taskTimeidDaytoDay);
return view;
}
In first i click the "gdfgdtet" list item it show another row and then i click the second list item "dfgsdgsd" at that time the above list item "gdfgdtet" closed the second row.This is a normal output.Suppose if i open the "gdfgdtet" list item and then swipe the listview at that time both of "gdfgdtet" "dfgsdgsd" will be opened and crashed...because the above one list item reference changed when i am swiping please how to solve this problem...
I'll try to provide you a good answer that explains why you are having this problems, but the general idea is that you have to see this video - http://www.youtube.com/watch?v=wDBM6wVEO70
please take my words kindly - you don't seems to understand what ListView + BaseAdapter recycling mechanism is all about, and I strongly recommend you see the full video I linked you to, and read more about that.
in general, the specific problem in your code is that you are holding reference to listview item (lastSelectedItem), then trying to use it latter assuming it's still representing same list item. that's wrong. in that stage (after scrolling) the view already been recycled to represent another item in the list (based on the adapter implementation).
listView's number of childs is not the size of adapter.getCount()!!!!!!!!
listViews's number of childs = number of visible list items on screen + 1 + headers + footers
let's say you have the 5 first items visible on screen, then you are scrolling down. when you see the 7 item you actually see the same view instance that used to show the first list item and been recycled.
getView will call in this stage with convertView != null and position in the adapter to let you reuse the item by putting new values such different text/image to the same instance
this mechanism provides ability to display list of "infinite" number of items in the list, and holding in memory only a few number of views. imagine that you have list of 5000 items in the list, and each one of them have different view instance - you would get outOfMemory exception in a sec!
complete explanation about that would be hard to write in stackoverflow answer's context.
it just too long trying to explain one of the most important and complex UI components in android, but this links would be a good start:
http://www.youtube.com/watch?v=wDBM6wVEO70
How ListView's recycling mechanism works
http://mobile.cs.fsu.edu/the-nuance-of-android-listview-recycling-for-n00bs/
if you are interstead in "quick" fix for your specific problem, the solution would be:
hold in the data structure represents your list item additional field indicating if it in "close" or "open state. when item been clicked - change the data accordinly and call notifyDatasetChanged(). inside the getView() check if item is open or close and populate it accordinly
by the way - it's not only "quick fix" solution, but also the right thing to do anyway
You should pay attention to Tal Kanel's answer and consider this one to be an extension to it. His advice will help you in the long run.
Add a boolean field to DaytoDayTaskGetterSetter class:
public class DaytoDayTaskGetterSetter {
....
....
boolean open;
public DaytoDayTaskGetterSetter (.., .., boolean o) {
....
....
open = o;
}
....
....
public boolean shouldOpen() {
return open;
}
public void setOpen(boolean o) {
open = o;
}
}
In your getView(), check if the object has its open value set:
DaytoDayTaskGetterSetter obj = (DaytoDayTaskGetterSetter) getItem(position);
if (obj.shouldOpen()) {
// Set visibility to true for the items
} else {
// Set visibility to false for the items
}
On list item click, traverse the list and set open for all list items to false. Use the position to retrieve DaytoDayTaskGetterSetter and set its open to true:
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
for (DaytoDayTaskGetterSetter obj : listContainingObjects) {
obj.setOpen(false);
}
DaytoDayTaskGetterSetter clickedItem = (DaytoDayTaskGetterSetter)
yourAdapter.getItem(position);
clickedItem.setOpen(true);
yourAdapter.notifyDataSetChanged();
}
Edit 1:
#Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
DaytoDayTaskGetterSetter clickedItem = (DaytoDayTaskGetterSetter)
yourAdapter.getItem(position);
if (clickedItem.shouldOpen()) {
clickedItem.setOpen(false);
} else {
for (DaytoDayTaskGetterSetter obj : listContainingObjects) {
obj.setOpen(false);
}
clickedItem.setOpen(true);
}
yourAdapter.notifyDataSetChanged();
}
I am trying to insert list items dynamically into the list view. As list view is created and displayed on the screen now suppose i got one items from the server or some where, now i want to add this item in the same list view what i have displayed. How to do that ?? Is there any way to insert items dynamically in the displayed list view without creating the list agtain and again. And is there any way to change the status of list item, that means can we interact while it is displaying?? your's reply will be appreciated. Thnx in advance !!
Add whatever you want to the data structure (meaning the List) that is being used by your Adapter and then call notifiyDataSetChanged() on your Adapter
With a regular ArrayAdapter it would be something like:
MyAdapter adapter = new MyAdapter(this, R.layout.row, myList);
listView.setAdapter(adapter);
...
//make a bunch of changes to data
list.add(foo);
listView.getAdapter().notifyDataSetChanged();
I can provide a more complicated example with a BaseAdapter as well.
EDIT
I created a little sample because this question seems to be pretty common.
In my sample I did everything in one class, just to make it a bit easier to follow it all in one place.
In the end, it's very much a model-view-controller type situation. You can even run the actual project by cloning it from here: https://github.com/levinotik/Android-Frequently-Asked-Questions
The essence of it is this:
/**
* This Activity answers the frequently asked question
* of how to change items on the fly in a ListView.
*
* In my own project, some of the elements (inner classes, etc)
* might be extracted into separate classes, but for clarity
* purposes, I'm doing everything inline.
*
* The example here is very, very basic. But if you understand
* the concept, it can scale to anything. You have complex
* views bound to complex data wit complex conditions.
* You could model a facebook user and update the ListView
* based on changes to that user's data that's represented in
* your model.
*/
public class DynamicListViewActivity extends Activity {
MyCustomAdapter mAdapter;
#Override
public void onCreate(Bundle state) {
super.onCreate(state);
ListView listView = new ListView(this);
setContentView(listView);
/**
* Obviously, this will typically some from somewhere else,
* as opposed to be creating manually, one by one.
*/
ArrayList<MyObject> myListOfObjects = new ArrayList<MyObject>();
MyObject object1 = new MyObject("I love Android", "ListViews are cool");
myListOfObjects.add(object1);
MyObject object2 = new MyObject("Broccoli is healthy", "Pizza tastes good");
myListOfObjects.add(object2);
MyObject object3 = new MyObject("bla bla bla", "random string");
myListOfObjects.add(object3);
//Instantiate your custom adapter and hand it your listOfObjects
mAdapter = new MyCustomAdapter(this, myListOfObjects);
listView.setAdapter(mAdapter);
/**
* Now you are free to do whatever the hell you want to your ListView.
* You can add to the List, change an object in it, whatever.
* Just let your Adapter know that that the data has changed so it
* can refresh itself and the Views in the ListView.
*/
/**Here's an example. Set object2's condition to true.
If everyting worked right, then the background color
of that row will change to blue
Obviously you would do this based on some later event.
*/
object2.setSomeCondition(true);
mAdapter.notifyDataSetChanged();
}
/**
*
* An Adapter is bridge between your data
* and the views that make up the ListView.
* You provide some data and the adapter
* helps to place them into the rows
* of the ListView.
*
* Subclassing BaseAdapter gives you the most
* flexibility. You'll have to override some
* methods to get it working.
*/
class MyCustomAdapter extends BaseAdapter {
private List<MyObject> mObjects;
private Context mContext;
/**
* Create a constructor that takes a List
* of some Objects to use as the Adapter's
* data
*/
public MyCustomAdapter(Context context, List<MyObject> objects) {
mObjects = objects;
mContext = context;
}
/**
* Tell the Adapter how many items are in your data.
* Here, we can just return the size of mObjects!
*/
#Override
public int getCount() {
return mObjects.size();
}
/**
* Tell your the Adapter how to get an
* item as the specified position in the list.
*/
#Override
public Object getItem(int position) {
return mObjects.get(position);
}
/**
* If you want the id of the item
* to be something else, do something fancy here.
* Rarely any need for that.
*/
#Override
public long getItemId(int position) {
return position;
}
/**
* Here's where the real work takes place.
* Here you tell the Adapter what View to show
* for the rows in the ListView.
*
* ListViews try to recycle views, so the "convertView"
* is provided for you to reuse, but you need to check if
* it's null before trying to reuse it.
* #param position
* #param convertView
* #param parent
* #return
*/
#Override
public View getView(int position, View convertView, ViewGroup parent) {
MyView view;
if(convertView == null){
view = new MyView(mContext);
} else {
view = (MyView) convertView;
}
/**Here's where we utilize the method we exposed
in order to change the view based on the data
So right before you return the View for the ListView
to use, you just call that method.
*/
view.configure(mObjects.get(position));
return view;
}
}
/**
* Very simple layout to use in the ListView.
* Just shows some text in the center of the View
*/
public class MyView extends RelativeLayout {
private TextView someText;
public MyView(Context context) {
super(context);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(CENTER_IN_PARENT);
someText = new TextView(context);
someText.setTextSize(20);
someText.setTextColor(Color.BLACK);
someText.setLayoutParams(params);
addView(someText);
}
/**
* Remember, your View is an regular object like any other.
* You can add whatever methods you want and expose it to the world!
* We have the method take a "MyObject" and do things to the View
* based on it.
*/
public void configure(MyObject object) {
someText.setText(object.bar);
//Check if the condition is true, if it is, set background of view to Blue.
if(object.isSomeCondition()) {
this.setBackgroundColor(Color.BLUE);
} else { //You probably need this else, because when views are recycled, it may just use Blue even when the condition isn't true.
this.setBackgroundColor(Color.WHITE);
}
}
}
/**
* This can be anything you want. Usually,
* it's some object that makes sense according
* to your business logic/domain.
*
* I'm purposely keeping this class as simple
* as possible to demonstrate the point.
*/
class MyObject {
private String foo;
private String bar;
private boolean someCondition;
public boolean isSomeCondition() {
return someCondition;
}
MyObject(String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public void setSomeCondition(boolean b) {
someCondition = b;
}
}
}
If you grasp the concept here, you should be able to adapt (no pun intended) this to ArrayAdapters, etc.
Yes, by using an adapter, you fill in the ListView, update items whenever needed, add new items, etc.
If you're pulling data over the net, you can start by using a plain ArrayAdapter (usually by subclassing and overriding the getView() method to build your layout) and then adding and removing items from the list it provides. If you add items to the list, they will appear at the end of list when you scroll down (or immediately if you are at the bottom). If you modify an item, the list will immediately update on screen.
If you use a setter on the model objects, the adapter won't know about that, but you can call notifyDataSetChanged(). You will probably also want to look at the setNotifyOnChange() method in case you want to make several changes to the list at once without causing flicker on the screen.
use notifyDataSetChanged() method.