CursorAdapter backed ListView удаляет анимацию "мерцает" при удалении - программирование
Подтвердить что ты не робот

CursorAdapter backed ListView удаляет анимацию "мерцает" при удалении

Я пытаюсь выполнить прокрутку для удаления и в ListView с помощью библиотеки SwipeToDismissUndoList, которая расширяет Образец Roman Nurik SwipeToDismiss.

Моя проблема заключается в анимации удаления. Поскольку ListView поддерживается CursorAdapter, анимация запускает обратный вызов onDismiss в onAnimationEnd, но это означает, что анимация запущена и reset сама перед обновлениями 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);
            }
        };
    }
};
4b9b3361

Ответ 1

Я знаю, что этот вопрос был помечен как "ответ", но, как я отметил в комментариях, проблема с использованием 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.
  • изменить новый курсор

Ответ 2

Я думаю, что SwipeToDismissUndoList не подходит для адаптеров на основе курсора. Поскольку адаптеры полагаются на изменения от поставщиков контента (setNotificationUri() или registerContentObserver()...) для обновления пользовательского интерфейса. Вы не знаете, когда данные доступны или нет. Это проблема, с которой вы столкнулись.

Я думаю, что есть что-то вроде трюка. Вы можете использовать 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() закроет его.

Ответ 3

Эй, у меня была аналогичная проблема, и решил так, надеюсь, это поможет вам:

Я использовал то, что показал Чет Гаазе в этом деббите: http://www.youtube.com/watch?v=YCHNAi9kJI4

Он очень похож на римский код, но здесь он использует 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 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, это очень помогло мне!

Ответ 4

В момент публикации этого ответа я пробовал все перечисленные подходы из этого потока. 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 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 и идентификаторе элемента по умолчанию (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

Кроме того, я хочу рекомендовать вам не использовать операции ввода-вывода в основном потоке, как в вашем примере:)

Ответ 5

Просто приходите сюда с той же проблемой и легко и легко справитесь с кодом, предоставленным Эмануэлем Меклин.

Это действительно просто: Внутри метода 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;
    }
}

Что все!

Ответ 6

(Этот ответ относится к библиотеке Roman Nuriks. Для библиотек, разветвленных от этого, он должен быть похож).

Эта проблема возникает из-за того, что эти библиотеки хотят переработать удаленные представления. В основном после того, как элемент строки анимируется, чтобы исчезнуть, библиотека сбрасывает его в исходное положение и выглядит так, чтобы 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, ...);

Ответ 7

Основываясь на Ответу U Avalos, я применил Cursor wrapper, который обрабатывает несколько удаленных позиций. Однако решение еще не полностью протестировано и может содержать ошибки. Используйте его так, когда вы устанавливаете курсор

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