CursorAdapter支持ListView删除动画“闪烁”删除

时间:2013-03-18 00:41:33

标签: android android-listview android-animation

我正在尝试使用扩展SwipeToDismissUndoList示例的Roman Nurik's SwipeToDismiss库在ListView中实现滑动以及ListView

我的问题在于删除动画。由于CursorAdapteronDismiss支持,因此动画会在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}}

7 个答案:

答案 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

按照以前的所有步骤进行操作,但不包括:

  • 在SwipeDismissList.OnDismissCallback.onDismiss()中,创建新的CursorWithDelete。
  • 交换新光标

答案 1 :(得分:4)

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

我认为有一些技巧。您可以使用MatrixCursor

  • onLoadFinished(Loader, Cursor)中,您保留对从内容提供商返回的光标的引用。您需要稍后手动关闭它。
  • SwipeDismissList.OnDismissCallback.onDismiss()中,创建新的MatrixCursor,复制当前光标中的所有项目,但正在删除的项目。
  • 使用swapCursor() 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()会将其关闭。

答案 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可以重复使用它。有两种解决方法。

解决方案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, ...);

答案 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);
    }
}