14

我正在尝试实现滑动删除,并ListView使用扩展Roman Nurik 的 SwipeToDismiss示例的SwipeToDismissUndoList库。

我的问题在于删除动画。由于ListView由 a 支持CursorAdapter,动画会触发onDismiss回调,onAnimationEnd但这意味着动画已经运行并在CursorAdapter使用删除更新之前自行重置。

这最终对用户来说就像一个闪烁,他们通过滑动删除笔记,然后视图返回一瞬间然后消失,因为CursorAdapter已经拾取数据更改。

这是我的OnDismissCallback

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);
            }
        };
    }
};
4

7 回答 7

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

像以前一样按照所有步骤操作,除了:

  • 在 SwipeDismissList.OnDismissCallback.onDismiss() 中,创建新的 CursorWithDelete。
  • 交换新光标
于 2013-08-09T02:07:48.273 回答
4

我认为 SwipeToDismissUndoList 不适合基于光标的适配器。因为适配器依赖于内容提供者(setNotificationUri()registerContentObserver()……)的更改来更新 UI。您不知道数据何时可用。这就是你面临的问题。

我认为有一些类似的伎俩。您可以使用MatrixCursor.

  • onLoadFinished(Loader, Cursor)中,您保留对从内容提供程序返回的游标的引用。您需要稍后手动关闭它。
  • SwipeDismissList.OnDismissCallback.onDismiss(), create newMatrixCursor中,复制当前光标中的所有项目,但要删除的项目除外。
  • swapCursor()使用( not )将新创建的矩阵光标设置为适配器changeCursor()。因为swapCursor()不会关闭旧光标。您需要将其保持打开状态,以便装载机正常工作。
  • 现在 UI 已更新,您可以调用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()将其关闭。
于 2013-03-19T14:12:11.157 回答
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的适配器),我在Constructor中传递了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;
}

监听器与视频中显示的代码基本相同,我不使用背景容器,但这只是我的选择。所以 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,它对我帮助很大!

于 2013-11-06T10:55:37.517 回答
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 行的集合构造即可。

要在您的项目中使用它,您只需扩展此类而不是 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 操作:)

于 2014-05-06T20:57:38.263 回答
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,如 Emanuel 所写:

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;
    }
}

就这样!

于 2014-08-08T11:15:27.470 回答
2

(这个答案是关于Roman Nuriks library的。对于从那里分支的图书馆,它应该是相似的)。

发生此问题是因为这些库想要回收已删除的视图。基本上在一个行项目被动画消失后,库将其重置为其原始位置和外观,以便 listView 可以重用它。有两种解决方法。

解决方案 1

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();
}

最后,在您的主要活动中,当新光标准备好时调用此方法。

解决方案 2

忘记回收行项目(毕竟它只是一行)。而不是重置视图并准备回收,以某种方式将其标记为performDismiss(...)在库的方法中染色。

然后在填充您的 listView 时(通过覆盖适配器的getView(View convertView, ...)方法),检查convertView对象上的该标记。如果它在那里,请不要使用convertView. 例如你可以做(​​以下是伪代码)

if (convertView is marked as stained) {
   convertView = null;
}
return super.getView(convertView, ...);
于 2013-07-30T03:28:29.063 回答
1

基于U Avalos 的回答,我实现了处理多个已删除位置的光标包装器。但是,解决方案尚未经过全面测试,可能包含错误。设置光标时像这样使用它

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);
    }
}
于 2014-04-23T08:57:25.620 回答