The problem:
(1) add a touch listener to rows in a listview so that on swipe.
(2) swipe animation plays
(3) rows get deleted in the backend, and
(4) the animation plays without any flicker or jerkiness. By "flicker" I mean that the deleted row briefly shows after the animation finished.
I suspect that something funky was happening with the animation listener, so I ended up doing the following (done in the order given):
Do the animation and make it persist by setting setFillAfter and setFillenabled to true
Make the view invisible when the animation ends
Delete the row in the database
Reset the animation
Reload the listview
Make the view visible (but wait an additional 300 ms)
The result deletes the row without jerkiness or flicker BUT it now feels sluggish because of extra 300 ms wait. (I'm also not sure if this delay works across all devices.)
Update: I should point out that the 300 ms delay is what makes it work. That's weird because by that point the animation was reset and the listview has the latest data. There should be no reason why making the view visible makes the old row briefly show, right?
I also tried using a ViewPropertyAnimator (as per Using animation on a ViewPager and setFillAfter) but for some reason the onAnimationEnd listener was called at every step of the animation.
I also read that we should implement a custom view and override its onAnimationEnd listener. (However, I haven't tried that approach yet.)
Update: just tried to add an extra dummy animation at the end (as per Android Animation Flicker). However, that doesn't work
My test phone runs Ice Cream Sandwich. My app is targeting Gingerbread and after.
So what's the proper solution? Am I doing this the wrong way?
Here's the code:
#Override
public boolean onTouch(final View view, MotionEvent event)
{
//...
switch(action) {
//...
case MotionEvent.ACTION_MOVE:
// ...
if (//check for fling
{
view.clearAnimation();
//animation = standard translate animation
animation.setAnimationListener(new AnimationListener() {
//
#Override
public void onAnimationEnd(Animation animation)
{
view.setVisibility(View.INVISIBLE);
flingListener.onFling(cursorPosition, view, velocity);
}
//...
});
view.startAnimation(animation);
}
break;
//
}
The "fling listener":
#Override
public void onFling(int position, final View view, float velocity)
{
//delete row -- its actually a Loader
//the following code runs in the Loader's onLoadFinished
view.clearAnimation();
adapter.notifyDataSetChanged();
adapter.swapCursor(null);
//reload listview -- it's actually a Loader
//the following code runs in the Loader's onLoadFinished
adapter.swapCursor(cursor);
view.postDelayed(new Runnable() {
#Override
public void run()
{
view.setVisibility(View.VISIBLE);
}
}, 300);
}
Update: After comparing Chet Haase's code, we are doing similar things with some important differences: (1) he uses a onPreDraw listener with the ListView tree observer to do the actual deletion, (2) he removes the row not only from the array but also from the listview. After mimicking his code, it still didn't work. The problem is now the Loader---I use a Loader to delete rows asynchronously. The Loader seems to force an additional draw call to the ListView...before the row has been deleted in the backend. And that's (another) cause of the flicker. Still haven't figured out a workaround though.
Chet Haase (Google Engineer) put together a really good DevBytes on this topic I'd suggest watching/taking ideas from his source. If you use NineOldAndroids, I think this'll be backwards compatible to GB.
Check it out here:
http://graphics-geek.blogspot.com/2013/06/devbytes-animating-listview-deletion.html
As I pointed out in the comments, the problem with Chet's code is that its designed for synchronous data access. Once you start asynchronously deleting rows, his code fails.
I found the solution to the flicker problem by combining Chet's code with this answer: CursorAdapter backed ListView delete animation "flickers" on delete
The solution to correctly do a row deletion aynchronously is:
Create a onPreDraw listener for the ListView tree observer to prevent flicker. All code in this listener runs before the list view re-draws itself, preventing flicker.
Find a way to delete the row in the listview (but not yet in the database). There are two approaches (see CursorAdapter backed ListView delete animation "flickers" on delete):
Create a AbstractCursor wrapper that ignores the row to be deleted and swap it in for the real cursor. OR
Mark the row to be deleted as "stained" and act on it appropriately when redrawing the row.
Remove the row for real in the database (asynchronously).
Some pseudo-code using the AbstractCursor wrapper (it's technically called a "proxy"):
//Called when you swipe a row to delete
#Override
public void onFling(final int positionToRemove, final View view)
{
final ViewTreeObserver observer = listView.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
public boolean onPreDraw()
{
observer.removeOnPreDrawListener(this);
//remove the row from the matrix cursor
CursorProxy newCursor = new CursorProxy(cursor,
positionToRemove);
swapCursor(newCursor);
//delete row in database
}
}
}
Related
I need to be able to programmatically dismiss an item inside a RecyclerView without the user actually swiping (instead I want to dismiss the item when they tap a button in the card). A lot of libraries I've seen only seem to support actual swipes.
I've tried using an existing library and just simulate a MotionEvent by creating a swipe on my own programmatically, but this interferes with another horizontal-swipe listener, so I'm wondering how else this could be done, ideally for a RecyclerView but if anyone knows how to for a ListView instead I can try to adapt that.
I've looked at this library as well as others for inspiration but I can't figure out how to trigger the swipes programmatically instead.
Use a ListView or RecyclerView with custom adapter, and call notifyDataSetChanged after removing an item from the datalist:
private void removeListItem(View rowView, final int position) {
Animation anim = AnimationUtils.loadAnimation(this,
android.R.anim.slide_out_right);
anim.setDuration(500);
rowView.startAnimation(anim);
new Handler().postDelayed(new Runnable() {
public void run() {
values.remove(position); //Remove the current content from the array
adapter.notifyDataSetChanged(); //Refresh list
}
}, anim.getDuration());
}
Use one of the libraries that offer the swipe to dissmis funcionality ad extract the animation part, if im not mistaken its at the action_up at the onTouch(). Then call it from your onClick of the button.
I been fighting an odd issue these last few days. I have a custom ExpandableListAdapter where each row contains an ImageView, among other things. I have a class that handles the asynchronous loading of images from the multitude of places they may reside (disk cache, app data, remote server, etc). In my adapter's getView method I delegate the responsibility of returning a View to the list Item itself (I have multiple row types for my group list). I request the image load as follows:
final ImageView thumb = holder.thumb;
holder.token = mFetcher.fetchThumb(mImage.id, new BitmapFetcher.Callback() {
#Override
public void onBitmap(final Bitmap b) {
thumb.post(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});
}
#Override
public void onFailure() {
}
});
Yeah, it's ugly, but I decided against some contract where you have the BitmapFetcher.Callback execute its methods on the UI thread by default.
Anyway, when I load the Activity that contains the ExpandableListView there will often be thumb images missing from different rows in the list. Reloading the Activity may cause some of the missing thumbs to show but others that were previously showing may not be anymore. The behavior is pretty random as far as I can tell. Scrolling the ListView such that the rows with missing images get recycled causes the new thumb images (when the recycled row gets displayed again) to load fine. Scrolling back to rows that previously contained missing images causes the missing images to appear. I can confirm that all the images are loading correctly from my BitmapFetcher (mFetcher) class. I should also mention that I load other images in other places. Every once in awhile they don't appear either.
After pulling most of my hair out, I discovered that changing:
thumb.post(new Runnable() {
to:
mExpListView.post(new Runnable() {
fixes the issue. I originally thought that the issue might be happening because I was using a final reference to a View, but the other locations in the app use non-final references to a view to post messages, and, as I mentioned, sometimes those did not work. I eventually changed everything to use an Activity's runOnUiThread() method (and my own getUiThreadRunner().execute method when inside Fragments) and that seems to fix the issue all around.
So my question remains, in what cases can View.post() to fail to deliver the runnable to the associated ViewRoot's message queue in the proper order? Or, perhaps the invalidate() is happening before the View is returned from getView and thus before it's placed in a ViewGroup that can be reached from the root View. Those are really the only cases I can think of that would prevent the image from showing up. I can guarantee that none of these calls are happening until at least onStart has finished executing. Further, it looks like it's fine to post to a View even if it hasn't been attached to a Window yet:
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(attachInfo.mHandler);
(in performTraversal). The only difference between the runOnUiThread and post seems to be that an Activity has a different Handler than the ViewRootImpl.
Activity:
final Handler mHandler = new Handler();
whereas in ViewRootImpl:
final ViewRootHandler handler = new ViewRootHandler();
But, this should not be a problem provided both Handlers were constructed in the same Thread (or using the same Looper). That leaves me wondering if it is, indeed, a problem to invalidate() a View that has not yet been added to the hierarchy. For this to be the case invalidate should either 1. not do anything if it's not visible, or 2. only be valid for the next performTraversal() that happens.
View.invalidate() checks a nice private method that's not documented called skipInvalidate():
/**
* Do not invalidate views which are not visible and which are not running an animation. They
* will not get drawn and they should not set dirty flags as if they will be drawn
*/
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
It looks like number 1 is more accurate! However, I would think this only pertains to a View's VISIBILITY property. So, is it accurate to assume that a View is considered not VISIBLE if it cannot be reached from the ViewRoot? Or is the VISIBILITY property unaffected by the View's container? If the former is the case (which I suspect it is) it raises a concern. My use of Activity.runOnUiThread is not a solution to the problem. It only happens to work because the invalidate() calls are being sent to a different Handler and being executed later (after getView returns and after the row has been added and made visible on the screen). Has anybody else run into this issue? Is there a good solution?
Hey David I ran into a similar issue long time back. The basic requirement for view.post(Runnable r) is that the view should be attached to the window for Runnable to be executed. However, since you are loading images asynchronously in your first case, therefore there is a probability that imageView aren't attached to window when post request is made and hence, some images fail to load.
Quoting earlier version of docs on the same:
View.post() : Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread. This method can
be invoked from outside of the UI thread only when this View is
attached to a window.
Switching to you next question, what is the best solution to handle this situation ?
Can't comment on the best solution. However, I think both handler.post() and activity.runOnUIThread() are good to go. Since, they basically post runnable in main thread queue irrespective of anything and in general, the request to display list rows would be enqueued prior to our thumb.post(). So, they might work flawlessly for most cases. (Atleast I've never faced a problem with them !). However. if you find a better solution, do share it with me.
Try this : setBitmap() like this :
runOnUiThread(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});
As mentioned here, Android's GridView.scrollTo() doesn't work. The method the solution mentioned, setSelectedPosition, doesn't seem to exist in GridView
smoothScrollToPosition does work, but I really don't want the animation.
For context, I have a CursorAdapter-backed GridView, and I want the view to "reset", i.e. scroll to the top, when I change the cursor.
I've been using setSelection(int position) for this, and it seems to be working just fine. To scroll to the top, just use 0 for position.
From the docs:
If in touch mode, the item will not be selected but it will still be positioned appropriately.
Edit:
Added code to post setSelection as a Runnable:
albumsView.post(new Runnable() {
#Override
public void run() {
albumsView.setSelection(0);
}
});
I have found that in order to reliably use gridView.setSelection(index), I have to first call gridView.setAdapter() (even if the adapter has already been set and has not changed). Like so:
gridView.setAdapter(gridAdapter);
gridView.setSelection(index);
I have a ListView which is taking data from DB. Each row when clicked animates to the right and back as follows:
#Override
public void onItemClick(AdapterView<?> av, View v, int pos, long id) {
...
pos = pos - mList.getFirstVisiblePosition();
mList.getChildAt(pos).startAnimation(anim );
...}
the animation lasts about 1sec and is : slide out right then slide in back
This part works fine if there is no interaction with DB during the animation
Now, each item from the list has a counter in DB. Whenever user clicks on the item from the list the counter should be increased in DB and that is visually presented on the ListView per item as well.
So, DB is visible through ContentProvider
This is the issue :
Whenever I click on an item the animation starts, and then the update is performed to DB through ContentResolver. The problem is that the animation starts at the proper element but just right after it starts it suddenly stops and is finished on other element from the list.
So if I have four elements on the list
----------1----------
----------2----------
----------3----------
----------4----------
and I click element 1, number 1 starts sliding right but after around 200ms it stops and element 4 continues to animate from the place where 1 stopped for the rest of the animation
Same can be reproduced if I press 2 but this time number 3 finishes animation.
If 3 then 2 finishes. If I click 4 then 1.
Assuming only 4 items are visible I can reproduce it throught whole list.
It is always the opposite element. I debug the position in onItemClick, but it is always right.
IMPORTANT
When I comment out the code that makes the update in DB, everything works fine. Which suggests that the problem occurs when SimpleCursorAdapter of ListView is refreshed with new Cursor through LoaderManager from the DB during the animation.
WHAT I did try
using CommonsWare Loaderex instead of ContentProvider. It gives the same effect.
CODE SNIPPETS
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(this.getActivity(), MyContentProvider.CONTENT_URI,
new String[]{}, "uid>0 AND distance<80000", null, "distance LIMIT 100");
}
public void onLoadFinished(Loader<Cursor> ld, Cursor c) {
merchantAdapter.swapCursor(c);
}
OnItemClick has this code :
ContentResolver cr = getActivity().getContentResolver();
ContentValues cv = new ContentValues();
cv.put("counter", counter);
Uri mUri = ContentUris.withAppendedId(MyContentProvider.CONTENT_URI, uid);
cr.update(mUri, cv, null, null);
What also bugs me that I wasnt able to make the LoaderManager update the list for me automatically so after above cr.update(...
I do this to refresh the list after each update
getActivity()).lm.getLoader(0).forceLoad();
Could the animation problem be Android bug? or what I am missing?
Current idea to solve this
I guess my option is to block clicks for animation duration and make cr.update in handler after animation duration has passed so that the refresh will not affect animation.
Please advise
I can now confirm that :
the behaviour only happens on my 2.2 device. And it can be reproduced.
It works fine (the animation finishes properly when rows are updated in the bg) on Android 4.1 device and it can be reproduced.
My current solution to this is
Since I want to support 2.2 devices. I create listener to this animation. when anim is finished I make the update to db. Also I block UI for the duraiton of anim (it is around 300ms so its fine)
NOTE - this does look like Android SDK problem
As for the update problem and using forceLoad
In my content provider implementation I had to add this inside my query method :
cursor.setNotificationUri(getContext().getContentResolver(), uri);
which is registering this uri to ContentResolver. now it works fine without using forceLoad
I have a ListView that potentially can have hundreds of entries. When a selection is made I've been using a smoothScrollToPosition, thusly:
if (lv != null) { //Are we created yet?
lv.post(new Runnable() {
public void run() {
lv.smoothScrollToPosition(k);
}
});
}
but my users have told me they don't like the scrolling animation and would prefer to just instantly go there. So I replaced my smooth scroll with
lv.setSelection(k);
... and now it does nothing at all. FWIW this is all happening right after a notifyDatasetChanged
In searching for a solution I came across this discussion on http://code.google.com/p/android/issues/detail?id=6741 which implies this is a known problem. Is there a workaround or am I just doing this wrong?
Thanks in advance.
The documentation of setSelection says that it only scrolls to the selected position when the ListView is in touch mode. Maybe ListView is no more in touch mode once the data set has changed or maybe setSelection is simply forgotten for the next UI update cycle.
I guess you could try a workaround by calling setSelection with a delay. You could use the postDelayed method with a delay of 100 milliseconds for example. Or you could extend ListView and override layoutChildren or something related that probably gets called when the data set changes in order to re-calculate the list view item measurements. At that point it should be safe to call setSelection and you don't need to rely on guesstimating a delay.