14

Roman Nurik の SwipeToDismissサンプルを拡張するSwipeToDismissUndoListライブラリをListView使用して、削除するスワイプを実装しようとしています。

私の問題は削除アニメーションにあります。ListViewは によってサポートされているため、アニメーションは でコールバックをCursorAdapterトリガーしますが、これは、削除による更新の前にアニメーションが実行され、それ自体がリセットされたことを意味します。onDismissonAnimationEndCursorAdapter

これは、ユーザーがメモをスワイプして削除するとちらつきのように見え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

4

SwipeToDismissUndoList は、カーソルベースのアダプターには適していないと思います。setNotificationUri()アダプターはコンテンツ プロバイダー (またはregisterContentObserver()…) からの変更に依存してUI を更新するためです。データがいつ利用できるかどうかはわかりません。それがあなたが直面している問題です。

何かコツみたいなものがあると思います。使用できますMatrixCursor

  • ではonLoadFinished(Loader, Cursor)、コンテンツ プロバイダから返されたカーソルへの参照を保持します。後で手動で閉じる必要があります。
  • SwipeDismissList.OnDismissCallback.onDismiss()、新規作成MatrixCursorで、現在のカーソルから、削除されるアイテムを除くすべてのアイテムをコピーします。
  • 新しく作成したマトリックス カーソルを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 を使用しているため、アダプターからアイテムを削除した後、リストが再描画される前に、アニメーションでギャップを埋める時間があり、ちらつきません。もう 1 つの違いは、ListView 自体ではなく、アダプター内のリストの各ビュー (アイテム) にリスナーを設定することです。

だから私のコードのサンプル:

これは ListActivity の onCreate です。ここでは、リスナーをアダプターに特別なものは何も渡しません。

ListAdapterTouchListener listAdapterTouchListener = new ListAdapterTouchListener(getListView());
    listAdapter = new ListAdapter(this,null,false,listAdapterTouchListener);

これは ListAdapter の一部です (CursorAdapter を拡張するのは私自身のアダプターです)。コンストラクターでリスナーを渡します。

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を見るべきです、それは私を大いに助けました!

于 2013-11-06T10:55:37.517 に答える
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

そして、Emanuel が書いたように 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;
    }
}

それで全部です!

于 2014-08-08T11:15:27.470 に答える
2

(この回答はRoman Nuriks ライブラリに関するものです。そこから分岐したライブラリについては、同様のはずです)。

この問題は、これらのライブラリが削除されたビューをリサイクルしたいために発生します。基本的に、行アイテムがアニメーション化されて消えると、ライブラリはそれを元の位置と外観にリセットして、listView が再利用できるようにします。2 つの回避策があります。

解決策 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

行アイテムのリサイクルを忘れてください (結局のところ、それは 1 つの行にすぎません)。ビューをリセットしてリサイクルの準備をするのではなく、どういうわけか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 の回答に基づいて、複数の削除された位置を処理する 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);
    }
}
于 2014-04-23T08:57:25.620 に答える