我正在尝试使用扩展SwipeToDismissUndoList示例的Roman Nurik's SwipeToDismiss库在ListView
中实现滑动以及ListView
。
我的问题在于删除动画。由于CursorAdapter
由onDismiss
支持,因此动画会在onAnimationEnd
中触发CursorAdapter
回调,但这意味着动画已在CursorAdapter
之前运行并重置自身使用删除进行更新。
这最终看起来像是闪烁给用户,他们通过滑动删除了一个音符,然后视图又回来一瞬间然后消失,因为OnDismissCallback
已经拾取了数据更改。 / p>
这是我的private SwipeDismissList.OnDismissCallback dismissCallback =
new SwipeDismissList.OnDismissCallback() {
@Override
public SwipeDismissList.Undoable onDismiss(ListView listView, final int position) {
Cursor c = mAdapter.getCursor();
c.moveToPosition(position);
final int id = c.getInt(Query._ID);
final Item item = Item.findById(getActivity(), id);
if (Log.LOGV) Log.v("Deleting item: " + item);
final ContentResolver cr = getActivity().getContentResolver();
cr.delete(Items.buildItemUri(id), null, null);
mAdapter.notifyDataSetChanged();
return new SwipeDismissList.Undoable() {
public void undo() {
if (Log.LOGV) Log.v("Restoring Item: " + item);
ContentValues cv = new ContentValues();
cv.put(Items._ID, item.getId());
cv.put(Items.ITEM_CONTENT, item.getContent());
cr.insert(Items.CONTENT_URI, cv);
}
};
}
};
:
{{1}}
答案 0 :(得分:6)
我知道这个问题已被标记为“已回答”但正如我在评论中指出的那样,使用MatrixCursor的问题在于效率太低。复制除要删除的行之外的所有行意味着行删除以线性时间运行(列表视图中的项目数为线性)。对于大数据和较慢的手机,这可能是不可接受的。
另一种方法是实现自己的AbstractCursor,忽略要删除的行。这导致假行删除在恒定时间内运行,并且在绘制时性能损失可忽略不计。
示例实施:
public class CursorWithDelete extends AbstractCursor {
private Cursor cursor;
private int posToIgnore;
public CursorWithDelete(Cursor cursor, int posToRemove)
{
this.cursor = cursor;
this.posToIgnore = posToRemove;
}
@Override
public boolean onMove(int oldPosition, int newPosition)
{
if (newPosition < posToIgnore)
{
cursor.moveToPosition(newPosition);
}
else
{
cursor.moveToPosition(newPosition+1);
}
return true;
}
@Override
public int getCount()
{
return cursor.getCount() - 1;
}
@Override
public String[] getColumnNames()
{
return cursor.getColumnNames();
}
//etc.
//make sure to override all methods in AbstractCursor appropriately
按照以前的所有步骤进行操作,但不包括:
答案 1 :(得分:4)
我认为SwipeToDismissUndoList不适合基于游标的适配器。因为适配器依赖于内容提供程序(setNotificationUri()
或registerContentObserver()
...)的更改来更新UI。您不知道数据何时可用。这就是你所面临的问题。
我认为有一些技巧。您可以使用MatrixCursor
。
onLoadFinished(Loader, Cursor)
中,您保留对从内容提供商返回的光标的引用。您需要稍后手动关闭它。SwipeDismissList.OnDismissCallback.onDismiss()
中,创建新的MatrixCursor
,复制当前光标中的所有项目,但正在删除的项目。swapCursor()
(不 changeCursor()
)将新创建的矩阵光标设置为适配器。因为swapCursor()
没有关闭旧光标。您需要保持打开状态,以便装载机正常工作。getContentResolver().delete()
并实际删除了用户想要删除的项目。当内容提供程序完成删除数据时,它会通知原始游标重新加载数据。确保关闭所交换的原始光标。例如:
private Cursor mOrgCursor;
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (mOrgCursor != null)
mOrgCursor.close();
mOrgCursor = data;
mAdapter.changeCursor(mOrgCursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (mOrgCursor != null) {
mOrgCursor.close();
mOrgCursor = null;
}
mAdapter.changeCursor(null);
}
changeCursor()
会将其关闭。答案 2 :(得分:3)
嘿,我有类似的问题并且这样解决了,希望它可以帮到你:
我使用了Chet Haase在这个devbyte中显示的内容:http://www.youtube.com/watch?v=YCHNAi9kJI4
它与Roman的代码非常相似,但是在这里他使用了ViewTreeObserver,因此在从适配器中删除项目之后,但在重新绘制列表之前,您有时间设置关闭间隙的动画,并且它不会闪烁。另一个区别是,他将Listener设置为适配器中列表的每个视图(项目),而不是ListView本身。
我的代码示例:
这是ListActivity的onCreate,这里我把监听器传递给适配器没什么特别的:
ListAdapterTouchListener listAdapterTouchListener = new ListAdapterTouchListener(getListView());
listAdapter = new ListAdapter(this,null,false,listAdapterTouchListener);
这是ListAdapter的一部分(它是我自己的扩展CursorAdapter的适配器), 我在构造函数中传递了Listener,
private View.OnTouchListener onTouchListener;
public ListAdapter(Context context, Cursor c, boolean autoRequery,View.OnTouchListener listener) {
super(context, c, autoRequery);
onTouchListener = listener;
}
然后在newView方法中我将其设置为视图:
@Override
public View newView(final Context context, Cursor cursor, ViewGroup parent) {
View view = layoutInflater.inflate(R.layout.list_item,parent,false);
// here should be some viewholder magic to make it faster
view.setOnTouchListener(onTouchListener);
return view;
}
监听器与视频中显示的代码大致相同,我不使用backgroundcontainer,但这只是我的选择。所以animateRemoval有一个有趣的部分,这里是:
private void animateRemoval(View viewToRemove){
for(int i=0;i<listView.getChildCount();i++){
View child = listView.getChildAt(i);
if(child!=viewToRemove){
// since I don't have stableIds I use the _id from the sqlite database
// I'm adding the id to the viewholder in the bindView method in the ListAdapter
ListAdapter.ViewHolder viewHolder = (ListAdapter.ViewHolder)child.getTag();
long itemId = viewHolder.id;
itemIdTopMap.put(itemId, child.getTop());
}
}
// I'm using content provider with LoaderManager in the activity because it's more efficient, I get the id from the viewholder
ListAdapter.ViewHolder viewHolder = (ListAdapter.ViewHolder)viewToRemove.getTag();
long removeId = viewHolder.id;
//here you remove the item
listView.getContext().getContentResolver().delete(Uri.withAppendedPath(MyContentProvider.CONTENT_ID_URI_BASE,Long.toString(removeId)),null,null);
// after the removal get a ViewTreeObserver, so you can set a PredrawListener
// the rest of the code is pretty much the same as in the sample shown in the video
final ViewTreeObserver observer = listView.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
boolean firstAnimation = true;
for(int i=0;i<listView.getChildCount();i++){
final View child = listView.getChildAt(i);
ListAdapter.ViewHolder viewHolder = (ListAdapter.ViewHolder)child.getTag();
long itemId = viewHolder.id;
Integer startTop = itemIdTopMap.get(itemId);
int top = child.getTop();
if(startTop!=null){
if (startTop!=top) {
int delta=startTop-top;
child.setTranslationY(delta);
child.animate().setDuration(MOVE_DURATION).translationY(0);
if(firstAnimation){
child.animate().setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
swiping=false;
listView.setEnabled(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
firstAnimation=false;
}
}
}else{
int childHeight = child.getHeight()+listView.getDividerHeight();
startTop = top+(i>0?childHeight:-childHeight);
int delta = startTop-top;
child.setTranslationY(delta);
child.animate().setDuration(MOVE_DURATION).translationY(0);
if(firstAnimation){
child.animate().setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
swiping=false;
listView.setEnabled(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
firstAnimation=false;
}
}
}
itemIdTopMap.clear();
return true;
}
});
}
希望这会对你有所帮助,它对我有用!你真的应该看看devbyte,它给了我很多帮助!
答案 3 :(得分:3)
在发布此答案的那一刻,我已尝试从此主题中列出的所有方法。 CursorWrapper在性能方面效率最高,但遗憾的是不安全,因为无法保证被解除项目的位置稳定(如果数据可以从其他来源更改,例如通过后台同步)。 或者,您可以尝试我的基本游标适配器的简单实现:
/*
* Copyright (C) 2014. Victor Kosenko (http://qip-blog.eu.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// your package here
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import com.google.api.client.util.Sets;
import java.util.Set;
/**
* This is basic implementation of swipable cursor adapter that allows to skip displaying dismissed
* items by replacing them with empty view. This adapter overrides default implementation of
* {@link #getView(int, android.view.View, android.view.ViewGroup)}, so if you have custom
* implementation of this method you should review it according to logic of this adapter.
*
* @author Victor Kosenko
*/
public abstract class BaseSwipableCursorAdapter extends CursorAdapter {
protected static final int VIEW_ITEM_NORMAL = 0;
protected static final int VIEW_ITEM_EMPTY = 1;
protected Set<Long> pendingDismissItems;
protected View emptyView;
protected LayoutInflater inflater;
/**
* If {@code true} all pending items will be removed on cursor swap
*/
protected boolean flushPendingItemsOnSwap = true;
/**
* @see android.widget.CursorAdapter#CursorAdapter(android.content.Context, android.database.Cursor, boolean)
*/
public BaseSwipableCursorAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
init(context);
}
/**
* @see android.widget.CursorAdapter#CursorAdapter(android.content.Context, android.database.Cursor, int)
*/
protected BaseSwipableCursorAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
init(context);
}
/**
* Constructor with {@code null} cursor and enabled autoRequery
*
* @param context The context
*/
protected BaseSwipableCursorAdapter(Context context) {
super(context, null, true);
init(context);
}
/**
* @param context The context
* @param flushPendingItemsOnSwap If {@code true} all pending items will be removed on cursor swap
* @see #BaseSwipableCursorAdapter(android.content.Context)
*/
protected BaseSwipableCursorAdapter(Context context, boolean flushPendingItemsOnSwap) {
super(context, null, true);
init(context);
this.flushPendingItemsOnSwap = flushPendingItemsOnSwap;
}
protected void init(Context context) {
inflater = LayoutInflater.from(context);
pendingDismissItems = Sets.newHashSet();
emptyView = new View(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (!getCursor().moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
if (isPendingDismiss(position)) {
return emptyView;
} else {
return super.getView(position, convertView, parent);
}
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
return pendingDismissItems.contains(getItemId(position)) ? VIEW_ITEM_EMPTY : VIEW_ITEM_NORMAL;
}
/**
* Add item to pending dismiss. This item will be ignored in
* {@link #getView(int, android.view.View, android.view.ViewGroup)} when displaying list of items
*
* @param id Id of item that needs to be added to pending for dismiss
* @return {@code true} if this item already in collection if pending items, {@code false} otherwise
*/
public boolean putPendingDismiss(Long id) {
return pendingDismissItems.add(id);
}
/**
* Confirm that specified item is no longer present in underlying cursor. This method should be
* called after the fact of removing this item from result set of underlying cursor.
* If you're using flushPendingItemsOnSwap flag there is no need to call this method.
*
* @param id Id of item
* @return {@code true} if this item successfully removed from pending to dismiss, {@code false}
* if it's not present in pending items collection
*/
public boolean commitDismiss(Long id) {
return pendingDismissItems.remove(id);
}
/**
* Check if this item should be ignored
*
* @param position Cursor position
* @return {@code true} if this item should be ignored, {@code false} otherwise
*/
public boolean isPendingDismiss(int position) {
return getItemViewType(position) == VIEW_ITEM_EMPTY;
}
public boolean isFlushPendingItemsOnSwap() {
return flushPendingItemsOnSwap;
}
/**
* Automatically flush pending items when calling {@link #swapCursor(android.database.Cursor)}
*
* @param flushPendingItemsOnSwap If {@code true} all pending items will be removed on cursor swap
*/
public void setFlushPendingItemsOnSwap(boolean flushPendingItemsOnSwap) {
this.flushPendingItemsOnSwap = flushPendingItemsOnSwap;
}
@Override
public Cursor swapCursor(Cursor newCursor) {
if (flushPendingItemsOnSwap) {
pendingDismissItems.clear();
}
return super.swapCursor(newCursor);
}
}
它基于 HashSet 和默认项ID (getItemId()),因此性能不应成为问题,因为contains()方法有O(1)时间复杂性和实际设置在大多数情况下将包含零个或一个项目。 这也取决于番石榴。如果您不使用Guava,只需更换第91行的set construction。
要在项目中使用它,你可以扩展这个类而不是CursorAdapter,并在onDismiss()中添加几行代码(如果你使用的是EnhancedListView或类似的库):
@Override
public EnhancedListView.Undoable onDismiss(EnhancedListView enhancedListView, int i) {
adapter.putPendingDismiss(id);
adapter.notifyDataSetChanged();
...
}
如果您正在使用列表分隔符,则此解决方案将无效(因为此适配器显示空视图而不是已解除的项目)。您应该在项目布局中添加边距以在项目之间建立间距,并在项目布局中包含分隔线。
此代码可以在将来更新,因此我已将其发布在github gist上:https://gist.github.com/q1p/0b95633ab9367fb86785
另外,我建议您不要在主线程中使用I / O操作,例如:)
答案 4 :(得分:3)
带着同样的问题来到这里,并使用Emanuel Moecklin提供的代码完美轻松地解决。
这很简单: 在onDismiss方法中,执行以下操作:
//Save cursor for later
Cursor cursor = mAdapter.getCursor();
SwipeToDeleteCursorWrapper cursorWrapper = new SwipeToDeleteCursorWrapper(mAdapter.getCursor(), reverseSortedPositions[0]);
mAdapter.swapCursor(cursorWrapper);
//Remove the data from the database using the cursor
然后按照伊曼纽尔的说法创建SwipteToDeleteCursorWrapper:
public class SwipeToDeleteCursorWrapper extends CursorWrapper
{
private int mVirtualPosition;
private int mHiddenPosition;
public SwipeToDeleteCursorWrapper(Cursor cursor, int hiddenPosition)
{
super(cursor);
mVirtualPosition = -1;
mHiddenPosition = hiddenPosition;
}
@Override
public int getCount()
{
return super.getCount() - 1;
}
@Override
public int getPosition()
{
return mVirtualPosition;
}
@Override
public boolean move(int offset)
{
return moveToPosition(getPosition() + offset);
}
@Override
public boolean moveToFirst()
{
return moveToPosition(0);
}
@Override
public boolean moveToLast()
{
return moveToPosition(getCount() - 1);
}
@Override
public boolean moveToNext()
{
return moveToPosition(getPosition() + 1);
}
@Override
public boolean moveToPosition(int position)
{
mVirtualPosition = position;
int cursorPosition = position;
if (cursorPosition >= mHiddenPosition)
{
cursorPosition++;
}
return super.moveToPosition(cursorPosition);
}
@Override
public boolean moveToPrevious()
{
return moveToPosition(getPosition() - 1);
}
@Override
public boolean isBeforeFirst()
{
return getPosition() == -1 || getCount() == 0;
}
@Override
public boolean isFirst()
{
return getPosition() == 0 && getCount() != 0;
}
@Override
public boolean isLast()
{
int count = getCount();
return getPosition() == (count - 1) && count != 0;
}
@Override
public boolean isAfterLast()
{
int count = getCount();
return getPosition() == count || count == 0;
}
}
这就是全部!
答案 5 :(得分:2)
(这个答案与Roman Nuriks library有关。对于从中分支的库,它应该是相似的。)
发生此问题是因为这些库要回收已删除的视图。基本上在行项目动画消失后,库会将其重置为原始位置并查看,以便listView可以重复使用它。有两种解决方法。
在库的performDismiss(...)
方法中,找到重置被解除视图的代码部分。这是部分:
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : mPendingDismisses) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
mPendingDismisses.clear();
删除此部分并将其放在单独的public
方法中:
/**
* Resets the deleted view objects to their
* original form, so that they can be reused by the
* listview. This should be called after listview has
* the refreshed data available, e.g., in the onLoadFinished
* method of LoaderManager.LoaderCallbacks interface.
*/
public void resetDeletedViews() {
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : mPendingDismisses) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
mPendingDismisses.clear();
}
最后,在主活动中,在新光标准备就绪时调用此方法。
忘记回收行项目(毕竟它只是一行)。而不是重置视图并准备回收,不管怎样将其标记为库中的performDismiss(...)
方法。
然后在填充listView时(通过覆盖适配器的getView(View convertView, ...)
方法),检查convertView
对象上的那个标记。如果有,请不要使用convertView
。例如,您可以(下面的部分是伪代码)
if (convertView is marked as stained) {
convertView = null;
}
return super.getView(convertView, ...);
答案 6 :(得分:1)
基于U Avalos answer我实现了Cursor包装器,它处理多个已删除的位置。但是,解决方案尚未经过全面测试,可能包含错误。设置光标
时使用它mAdapter.changeCursor(new CursorWithDelete(returnCursor));
如果你想隐藏列表中的某些项目
CursorWithDelete cursor = (CursorWithDelete) mAdapter.getCursor();
cursor.deleteItem(position);
mAdapter.notifyDataSetChanged();
CusrsorWithDelete.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.database.AbstractCursor;
import android.database.Cursor;
public class CursorWithDelete extends AbstractCursor {
private List<Integer> positionsToIgnore = new ArrayList<Integer>();
private Cursor cursor;
public CursorWithDelete(Cursor cursor) {
this.cursor = cursor;
}
@Override
public boolean onMove(int oldPosition, int newPosition) {
cursor.moveToPosition(adjustPosition(newPosition));
return true;
}
public int adjustPosition(int newPosition) {
int ix = Collections.binarySearch(positionsToIgnore, newPosition);
if (ix < 0) {
ix = -ix - 1;
} else {
ix++;
}
int newPos;
int lastRemovedPosition;
do {
newPos = newPosition + ix;
lastRemovedPosition = positionsToIgnore.size() == ix ? -1 : positionsToIgnore.get(ix);
ix++;
} while (lastRemovedPosition >= 0 && newPos >= lastRemovedPosition);
return newPos;
}
@Override
public int getCount() {
return cursor.getCount() - positionsToIgnore.size();
}
@Override
public String[] getColumnNames() {
return cursor.getColumnNames();
}
@Override
public String getString(int column) {
return cursor.getString(column);
}
@Override
public short getShort(int column) {
return cursor.getShort(column);
}
@Override
public int getInt(int column) {
return cursor.getInt(column);
}
@Override
public long getLong(int column) {
return cursor.getLong(column);
}
@Override
public float getFloat(int column) {
return cursor.getFloat(column);
}
@Override
public double getDouble(int column) {
return cursor.getDouble(column);
}
@Override
public boolean isNull(int column) {
return cursor.isNull(column);
}
/**
* Call if you want to hide some position from the result
*
* @param position in the AdapterView, not the cursor position
*/
public void deleteItem(int position) {
position = adjustPosition(position);
int ix = Collections.binarySearch(positionsToIgnore, position);
positionsToIgnore.add(-ix - 1, position);
}
}