background
When choosing an item from a listView, I change its data and call notifyDataSetChanged.
The problem
Since it's the same listView, when I click the item, the effect stays for the view that will be used after the notifyDataSetChanged.
This is especially noticeable on Android Lollipop, where the ripple can continue after the listView gets refreshed with new data.
The code
Here's a sample code showing the problem:
public class MainActivity extends ActionBarActivity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView=(ListView)findViewById(R.id.listView);
listView.setAdapter(new BaseAdapter()
{
int pressCount=0;
#Override
public int getCount()
{
return 100;
}
#Override
public Object getItem(int position)
{
return null;
}
#Override
public long getItemId(int position)
{
return 0;
}
#Override
public View getView(int position,View convertView,ViewGroup parent)
{
View rootView=convertView;
if(rootView==null)
{
rootView=LayoutInflater.from(MainActivity.this).inflate(android.R.layout.simple_list_item_1,parent,false);
rootView.setBackgroundResource(getResIdFromAttribute(MainActivity.this,android.R.attr.selectableItemBackground));
rootView.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
pressCount++;
notifyDataSetChanged();
}
});
}
TextView tv=(TextView)rootView;
tv.setText("text:"+(pressCount+position));
return rootView;
}
});
}
public static int getResIdFromAttribute(final Activity activity,final int attr)
{
if(attr==0)
return 0;
final TypedValue typedvalueattr=new TypedValue();
activity.getTheme().resolveAttribute(attr,typedvalueattr,true);
return typedvalueattr.resourceId;
}
}
The question
How can I temporarily stop the selection effect till the next time anything is clicked on the listView (but also resume allowing it for the next time the user clicks an item) ?
OK, I've found the answer. It seems it's a known issue, and the solution is quite simple (shown here) :
ViewCompat.jumpDrawablesToCurrentState(view);
Weird thing is, it works for me only when I call it via Handler.post(...) .
Wonder why (as the view is already during animation), and if there's a better solution.
Related
Update #1
Added hasStableIds(true) and updated Picasso to version 2.5.2.
It does not solve the issue.
Reproduction:
RecyclerView with GridLayoutManager (spanCount = 3).
List items are CardViews with ImageView inside.
When all the items does not fit the screen calling notifyItemChanged on one item causes more than one calls to onBindViewHolder().
One call is for position from notifyItemChanged others for items not visible on the screen.
Issue:
Sometimes the item at position passed to the notifyItemChanged is loaded with an image belonging to an item that is not on the screen (most likely due to recycling of the view holder - although I would assume that if the item remains in place then the passed viewholder would be the same).
I have found Jake's comment on other issue here about calling load() even if the file/uri is null. Image is loaded on every onBindViewHolder here.
Simple sample app:
git clone https://github.com/gswierczynski/recycler-view-grid-layout-with-picasso.git
Tap on an item calls notifyItemChanged with parameter equal to the position of that item.
Code:
public class MainActivity extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment())
.commit();
}
}
public static class PlaceholderFragment extends Fragment {
public PlaceholderFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
RecyclerView rv = (RecyclerView) rootView.findViewById(R.id.rv);
rv.setLayoutManager(new GridLayoutManager(getActivity(), 3));
rv.setItemAnimator(new DefaultItemAnimator());
rv.setAdapter(new ImageAdapter());
return rootView;
}
}
private static class ImageAdapter extends RecyclerView.Adapter<ImageViewHolder> implements ClickableViewHolder.OnClickListener {
public static final String TAG = "ImageAdapter";
List<Integer> resourceIds = Arrays.asList(
R.drawable.a0,
R.drawable.a1,
R.drawable.a2,
R.drawable.a3,
R.drawable.a4,
R.drawable.a5,
R.drawable.a6,
R.drawable.a7,
R.drawable.a8,
R.drawable.a9,
R.drawable.a10,
R.drawable.a11,
R.drawable.a12,
R.drawable.a13,
R.drawable.a14,
R.drawable.a15,
R.drawable.a16,
R.drawable.a17,
R.drawable.a18,
R.drawable.a19,
R.drawable.a20);
#Override
public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
return new ImageViewHolder(v, this);
}
#Override
public void onBindViewHolder(ImageViewHolder holder, int position) {
Log.d(TAG, "onBindViewHolder position: " + position + " | holder obj:" + holder.toString());
Picasso.with(holder.iv.getContext())
.load(resourceIds.get(position))
.fit()
.centerInside()
.into(holder.iv);
}
#Override
public int getItemCount() {
return resourceIds.size();
}
#Override
public void onClick(View view, int position) {
Log.d(TAG, "onClick position: " + position);
notifyItemChanged(position);
}
#Override
public boolean onLongClick(View view, int position) {
return false;
}
}
private static class ImageViewHolder extends ClickableViewHolder {
public ImageView iv;
public ImageViewHolder(View itemView, OnClickListener onClickListener) {
super(itemView, onClickListener);
iv = (ImageView) itemView.findViewById(R.id.iv);
}
}
}
public class ClickableViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
OnClickListener onClickListener;
public ClickableViewHolder(View itemView, OnClickListener onClickListener) {
super(itemView);
this.onClickListener = onClickListener;
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
#Override
public void onClick(View view) {
onClickListener.onClick(view, getPosition());
}
#Override
public boolean onLongClick(View view) {
return onClickListener.onLongClick(view, getPosition());
}
public static interface OnClickListener {
void onClick(View view, int position);
boolean onLongClick(View view, int position);
}
}
I spent more time than I'd like to admit to work around oddities with RecyclerView and the new adapter that comes with it. The only thing that finally worked for me in terms of correct updates and making sure notifyDataSetChanges and all of its other siblings didn't cause odd behavior was this:
On my adapter, I set
setHasStableIds(true);
In the constructor. I then overrode this method:
#Override
public long getItemId(int position) {
// return a unique id here
}
And made sure that all my items returned a unique id.
How you achieve this is up to you. For me, the data was supplied from my web service in the form of a UUID and I cheated by converting parts of the UUID to long using this:
SomeContent content = _data.get(position);
Long code = Math.abs(content.getContentId().getLeastSignificantBits());
Obviously this is not a very safe approach but chances are it works for my lists which will contain < 1000 items. So far I haven't run into any trouble with it.
What I recommend is to try this approach and see if it works for you. Since you have an array, getting a unique number for you should be simple. Maybe try returning the position of the actual item (and not the position that is passed in the getItemId()) or create a unique long for each of your records and pass that in.
Have you tried calling the mutate() method on the Drawable? See here, for instance.
here a working solution but has graphics glitches when calling notifyDataSetChanged()
holder.iv.post(new Runnable() {
#Override
public void run() {
Picasso.with(holder.iv.getContext())
.load(resourceIds.get(position))
.resize(holder.iv.getWidth(), 0)
.into(holder.iv);
});
it works because at this point image has a width, unfortunately when I need to update all the checkboxes the in the viewholder (like a select all action), and I call notifyDataSetChanged() and the effect is very ugly
still searching for a better solution
edit:
this solution works for me:
holder.iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
holder.iv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
else
holder.iv.getViewTreeObserver().removeGlobalOnLayoutListener(this);
Picasso.with(holder.iv.getContext())
.load(resourceIds.get(position))
.resize(holder.iv.getMeasuredWidth(), 0)
.into(holder.iv);
}
});
I'm a beginner in android development and developing a simple notepad application where in I've a Navigation Drawer. Everything is ok, I am able to see navigation drawer and it works fine. now I'm stuck with how to update the counter value in drawer. Nav Drawer displays the categories of the notes and their counts.
How can I update the counter values in the Navigation Drawer when a note is either deleted or added (Notes are saved in db with category value - So I can get the count by doing a simple query ). I've searched thoroughly on internet but couldn't find any example to do it.
Any help would be appreciated.
You can add an extra method like loadData() in your adapter class and write your codes inside this method. Just get your current data(no. of notes/trash) from database and set it to specific navigation drawer item and finally call notifyDataSetChanged() to refresh the dataset.
public class NavDrawerListAdapter extends BaseAdapter {
public NavDrawerListAdapter(Context context, ArrayList<NavDrawerItem> navDrawerItems)
{
this.context = context;
this.navDrawerItems = navDrawerItems;
}
#Override
public int getCount()
{
return navDrawerItems.size();
}
#Override
public Object getItem(int position) {
return navDrawerItems.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Write your code
return convertView;
}
public void loadData()
{
DatabaseHelper db = new DatabaseHelper(context);
// Notes and trash counts
int countNotes = db.getAllActiveNotes().size();
int countTrash = db.getAllTrashNotes().size();
String strCountNotes = String.valueOf(countNotes);
String strCountTrash = String.valueOf(countTrash);
// Set counter value to Navigation drawer items
navDrawerItems.get(0).setCount(strCountNotes);
navDrawerItems.get(1).setCount(strCountTrash);
notifyDataSetChanged();
}
}
Finally, call loadData() from method onDrawerOpened() where navigation drawer implemented most probably in your main FragmentActivity class. As a result, every time when you open the navigation drawer you will see the updated counter value on drawer items.
public void onDrawerOpened(View drawerView) {
// Update notes and trash counter
adapter.loadData();
getActionBar().setTitle(mDrawerTitle);
invalidateOptionsMenu();
}
Hope this will help~
Navigation Drawer is getting items from adapter (since you have custom items, I assume you have your own custom adapter). So once you do any action with the data your adapter serving, you should just push new data to your adapter and call notifyDataSetChanged().
EDIT How this adapter can looks like:
public class CustomAdapter extends BaseAdapter {
private ArrayList<Object> data;
#Override
public int getCount() {
//do stuff
}
#Override
public Object getItem(int position) {
//do stuff
}
#Override
public long getItemId(int position) {
//do stuff
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
//do stuff
}
public void updateData(Cursor cursor){
data = //fetch data from Cursor
notifyDataSetChanged();
}
}
I'm Settings up a Custom ListView.
The pull-to-refresh feature comes straight from https://github.com/chrisbanes/Android-PullToRefresh
The ListView displayes Images, so i created a custom Adapter:
class mAdapter extends BaseAdapter{
public mAdapter(Context context){
// nothing to do
}
#Override
public int getCount() {
return mValues.size();
}
#Override
public Object getItem(int position) {
return mValues.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public boolean areAllItemsEnabled()
{
return false;
}
#Override
public boolean isEnabled(int position)
{
return false;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if(v == null){
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.list_item, null);
}
ImageView iv = (ImageView) v.findViewById(R.id.imageView);
if(iv != null){
displayImageInView(iv);
iv.setClickable(true);
iv.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(context, "ImageView", Toast.LENGTH_SHORT).show();
}
});
}
return v;
}
}
in onCreate(), i get the listView and assign the adapter:
mListView = (PullToRefreshListView) findViewById(R.id.listView);
mListView.setAdapter(new mAdapter(context));
After that i add an image to mValues (url for image to load from web) and call notifiyDataSetChanged on the adapter.
in mListView.onRefresh(), i add an image to mValues.
This works smoothly for adding the first image, or even the first bunch of images (before calling mAdapter.notifyDataSetChanged()).
The refresh indicator shows and hides as intended.
The weird things start happening when i try to add another image (or bunch) after that.
The refresh indicator shows, the image is displayed in the list view.
BUT : the refresh indicator never hides again after that. "onRefreshComplete()" gets called, but seems not to work properly the second time.
The UI Thread is not blocking, so operation is still possible.
If i delete all items in mValues, notify the adapter and pull to refresh again, the image is added properly, and the refresh indicator is hidden properly.
Conclusion: The pull-to-refresh only hides properly if the list was empty before refreshing.
I really don't know where to look for a solution for this weird error.
Maybe someone familiar with the Pull-To-Refresh Library from Chirs Banes can help me out here.
Thank You !
I just figured it out myself -.-
For anyone interested:
You have to set onRefreshComplete from the UI Thread.
Use a Handler to .post it from inside onRefresh(). <- which by the way runs on a separate thread.
Have a nice day.
I've found 2 ways:
Dynamically, when you need pulltorefreshview to stop do task on pull up, you can set a custom AsyncTask, for example:
private class GetDataTask extends AsyncTask<Void, Void, String[]> {
#Override
protected String[] doInBackground(Void... params) {
return null;
}
#Override
protected void onPostExecute(String[] result) {
lv.onRefreshComplete();
showToast(getResources().getString(R.string.no_more));
super.onPostExecute(result);
}
}
Dynamically call setMode to the pulltorefreshView
ptrlv.setMode(Mode.Both); // both direction can be used
ptrlv.setMpde(Mode.PULL_FROM_START); // only pull down can be used.
I have the following, very simple test program for using a ListView. I create a ListView and set it as the content view. I set a ListAdapter which supplies the rows. There are 30 rows, and each row consists of a LinearLayout ViewGroup. Into that ViewGroup, I place a TextView and a Button. When I run the program, I find that I cannot select rows of the list. I can, however, scroll the list and click the button.
If I remove the button from the LinearLayout (so that it contains only the TextView), then I am able to select rows of the list. I would
like to be able to have buttons on my individual row views, and still be able to select rows of the list. On another forum, someone said that this was possible, but I am at a loss as to how to accomplish it.
Can anyone give me a clue?
Thanks.
public class ListViewTest extends Activity implements ListAdapter
{
int m_count;
DataSetObserver m_observer;
public ListViewTest()
{
m_count = 30;
m_observer = null;
}
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ListView lv = new ListView(this);
lv.setAdapter(this);
lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
setContentView(lv);
}
#Override
public boolean areAllItemsEnabled() {
return true;
}
#Override
public boolean isEnabled(int position) {
return true;
}
#Override
public int getCount()
{
return m_count;
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public int getItemViewType(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
LinearLayout vg = new LinearLayout(this);
TextView tv = new TextView(this);
tv.setText("ListItem");
Button bv = new Button(this);
bv.setText("Button");
vg.addView(tv);
vg.addView(bv);
return(vg);
}
#Override
public int getViewTypeCount() {
return 1;
}
#Override
public boolean hasStableIds() {
return false;
}
#Override
public boolean isEmpty() {
return false;
}
#Override
public void registerDataSetObserver(DataSetObserver observer)
{
m_observer = observer;
}
#Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
}
As the other answers point out, whether or not you can select ListView rows as full items on their own depends on whether or not those rows contain focusable items. However, the solution is usually not setting focusable=false on your buttons or the like. That will prevent your app from being navigable with a d-pad, trackball, arrow keys, or what have you.
You want your list items to be able to control their own focus properties. You want setItemsCanFocus. This will disable the special focus/selection handling that ListView normally uses to treat list items as a single unit.
Now you can set a listener on the layout you use as the top-level element in your rows, set a stateful background drawable on it to display focus/press state, as well as focusLeft/Right properties to control focus shifting within the item itself.
Note: As of Jellybean the gallery widget is deprecated. A ViewPager should be used instead.
I'd like to programmatically move between images in the Gallery widget, with animation.
I can change the currently displaying image using the setSelection(int position) method, however that does not animate. Then there's setSelection(int position, bool animate) but the extra boolean on the end there doesn't appear to do anything.
In the source of Gallery it appears that it can handle DPAD key-presses, so a work-around I thought of was to fake the key-presses. Eg.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT))
However I can't get this working for some reason. Anyone tried this?
I notice three of the widget's methods I'd love to use moveNext(), movePrevious() and scrollToChild() are all private and unusable.
Does anyone know how I might be able to do this?
Just call the key press handler for the gallery directly:
public boolean onKeyDown(int keyCode, KeyEvent event)
i.e
Gallery gallery = ((Gallery) findViewById(R.id.gallery));
gallery.onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT, new KeyEvent(0, 0));
One important thing - this solution works only if child that is on left/right was already created, which means that it has to be 'visible'. If you have your image on fullscreen - consider setting spacing to -1 value.
You can Animate using dispatchKeyEvent or calling onFling directly.
Here is sample code for dispatchKeyEvent:
KeyEvent evtKey = new KeyEvent(0, KeyEvent.KEYCODE_DPAD_RIGHT);
dispatchKeyEvent(evtKey);
Use gallery.setSelected(int); Here is a simple example.
public class Splash extends Activity {
ArrayList objects = new ArrayList();
Gallery g;
int i = 0;
#Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.photos);
g = (Gallery) findViewById(R.id.gallery);
objects.add(getResources().getDrawable(R.drawable.icon));
objects.add(getResources().getDrawable(R.drawable.icon));
objects.add(getResources().getDrawable(R.drawable.icon));
objects.add(getResources().getDrawable(R.drawable.icon));
objects.add(getResources().getDrawable(R.drawable.icon));
objects.add(getResources().getDrawable(R.drawable.icon));
g.setAdapter(new CustomAdapter(this, objects));
g.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView arg0, View arg1,
int arg2, long arg3) {
Log.i("", "selected " + arg2);
}
#Override
public void onNothingSelected(AdapterView arg0) {}
});
}
#Override
public void onBackPressed() {
g.setSelection(i++);
}
private class CustomAdapter extends BaseAdapter {
private Context mCtx;
private List objects;
public int getCount() {
return this.objects.size();
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public CustomAdapter(Context context, ArrayList objects) {
super();
mCtx = context;
this.objects = objects;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView row = (ImageView) convertView;
if (row == null) {
row = new ImageView(mCtx);
row.setBackgroundDrawable(objects.get(position));
}
return row;
}
}
}
In the end I wrote my own version of the Gallery widget with the help of the code at this site.
I then wrote my own method which uses mFlingRunnable.startUsingDistance(distance);
Now I can programmatically animate the gallery between images.
Try this
mGallery.onFling(null,null, velocity,0);
http://groups.google.com/group/android-developers/browse_thread/thread/9140fd6af3061cdf#