web-dev-qa-db-fra.com

Glisser-déposer des éléments dans RecyclerView avec GridLayoutManager

Ce que je veux réaliser: Avoir un RecyclerView avec GridLayoutManager qui prend en charge le glisser-déposer et qui réorganise les éléments tout en les faisant glisser.

Note latérale: La première fois que vous développez quelque chose avec glisser-déposer.

Il existe de nombreux sujets sur la réalisation de cette fonctionnalité à l'aide d'un ListView, par exemple: https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/Android/languagelineup /DynamicListView.Java

Cependant, les exemples utilisent généralement beaucoup de code, créant des bitmaps de la vue déplacée et il semble qu'il devrait être possible d'obtenir le même résultat avec View.startDrag(...) et RecyclerView avec notifyItemAdded(), notifyItemMoved() et notifyItemRemoved() car ils fournissent des animations de réorganisation.

Alors j'ai joué autour de certains et est venu avec ceci:

final CardAdapter adapter = new CardAdapter(list);
adapter.setHasStableIds(true);
adapter.setListener(new CardAdapter.OnLongClickListener() {
    @Override
    public void onLongClick(View view) {
        ClipData data = ClipData.newPlainText("","");
        View.DragShadowBuilder builder = new View.DragShadowBuilder(view);
        final int pos = mRecyclerView.getChildAdapterPosition(view);
        final Goal item = list.remove(pos);

        mRecyclerView.setOnDragListener(new View.OnDragListener() {
            int prevPos = pos;

            @Override
            public boolean onDrag(View view, DragEvent dragEvent) {
                final int action = dragEvent.getAction();
                switch(action) {
                    case DragEvent.ACTION_DRAG_LOCATION:
                        View onTopOf = mRecyclerView.findChildViewUnder(dragEvent.getX(), dragEvent.getY());
                        int i = mRecyclerView.getChildAdapterPosition(onTopOf);

                        list.add(i, list.remove(prevPos));
                        adapter.notifyItemMoved(prevPos, i);
                        prevPos = i;
                        break;

                    case DragEvent.ACTION_DROP:
                        View underView = mRecyclerView.findChildViewUnder(dragEvent.getX(), dragEvent.getY());
                        int underPos = mRecyclerView.getChildAdapterPosition(underView);

                        list.add(underPos, item);
                        adapter.notifyItemInserted(underPos);
                        adapter.notifyDataSetChanged();
                        break;
                }

                return true;
            }
        });

        view.startDrag(data, builder, view, 0);
    }
});
mRecyclerView.setAdapter(adapter);

Ce morceau de code fonctionne bien, mais je suis échangé, mais très instable/tremblant et parfois, quand il rafraîchit, la grille entière est réarrangée dans son ordre original ou aléatoire. Quoi qu'il en soit, le code ci-dessus n'est que ma première tentative rapide. Ce qui m'intéresse davantage, c'est de savoir s'il existe une méthode standard/la meilleure pratique permettant d'effectuer le glisser-déposer avec ReyclerView ou si la manière correcte de la résoudre reste la même. été utilisé pour ListViews depuis des années?

40
patrick.elmquist

Il existe en fait un meilleur moyen d'y parvenir. Vous pouvez utiliser certaines des classes "compagnons" de RecyclerView:

ItemTouchHelper , qui est

une classe d’utilitaires à ajouter pour balayer et déplacer un support vers RecyclerView.

et son ItemTouchHelper.Callback , qui est

le contrat entre ItemTouchHelper et votre application

// Create an `ItemTouchHelper` and attach it to the `RecyclerView`
ItemTouchHelper ith = new ItemTouchHelper(_ithCallback);
ith.attachToRecyclerView(rv);

// Extend the Callback class
ItemTouchHelper.Callback _ithCallback = new ItemTouchHelper.Callback() {
    //and in your imlpementaion of
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // get the viewHolder's and target's positions in your adapter data, swap them
        Collections.swap(/*RecyclerView.Adapter's data collection*/, viewHolder.getAdapterPosition(), target.getAdapterPosition());
        // and notify the adapter that its dataset has changed
        _adapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        //TODO    
    }

    //defines the enabled move directions in each state (idle, swiping, dragging). 
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
                ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.START | ItemTouchHelper.END);
    }
};

Pour plus de détails, consultez leur documentation.

108
stan0

Voici ma solution avec la réorganisation de bases de données:

    ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            final int fromPosition = viewHolder.getAdapterPosition();
            final int toPosition = target.getAdapterPosition();
            if (fromPosition < toPosition) {
                for (int i = fromPosition; i < toPosition; i++) {
                    Collections.swap(mAdapter.getCapitolos(), i, i + 1);
                }
            } else {
                for (int i = fromPosition; i > toPosition; i--) {
                    Collections.swap(mAdapter.getCapitolos(), i, i - 1);
                }
            }
            mAdapter.notifyItemMoved(fromPosition, toPosition);
            return true;
        }

        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
            MyViewHolder svH = (MyViewHolder ) viewHolder;
            int index = mAdapter.getCapitolos().indexOf(svH.currentItem);
            mAdapter.getCapitolos().remove(svH.currentItem);
            mAdapter.notifyItemRemoved(index);
            if (emptyView != null) {
                if (mAdapter.getCapitolos().size() > 0) {
                emptyView.setVisibility(TextView.GONE);
                } else {
                emptyView.setVisibility(TextView.VISIBLE);
                }
            }
        }

        @Override
        public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
            reorderData();
        }
    };

    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
    itemTouchHelper.attachToRecyclerView(recList);

Il existe une fonction de support utilisant AsyncTask:

private void reorderData() {
    AsyncTask<String, Void, Spanned> task = new AsyncTask<String, Void, Spanned>() {
        @Override
        protected Spanned doInBackground(String... strings) {
            dbService.deleteAllData();
            for (int i = mAdapter.getCapitolos().size() - 1; i >= 0; i--) {
                Segnalibro s = mAdapter.getCapitolos().get(i);
                dbService.saveData(s.getIdCapitolo(), s.getVersetto());
            }
            return null;
        }

        @Override
        protected void onPostExecute(Spanned spanned) {
        }
    };
    task.execute();
}
16
Flavio Barisi

Ici, j’ai fait un échantillon complet dans Kotlin ( ici ) et, si vous le souhaitez, vous pouvez activer le balayage. -à-rejeter sur elle. Voici le code complet de celui-ci:

build.gradle

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'com.google.Android.material:material:1.1.0-alpha08'
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta01'

grid_item.xml

<TextView
    Android:id="@+id/textView" xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:layout_width="match_parent" Android:layout_height="100dp" Android:gravity="center"/>

activity_main.xml

<androidx.recyclerview.widget.RecyclerView
    Android:id="@+id/recyclerView" tools:listitem="@layout/grid_item"  xmlns:Android="http://schemas.Android.com/apk/res/Android" xmlns:app="http://schemas.Android.com/apk/res-auto"
    Android:layout_width="match_parent" Android:layout_height="match_parent"
    Android:orientation="vertical" app:spanCount="3" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>

manifeste

<manifest package="com.sample.recyclerviewdraganddroptest" xmlns:Android="http://schemas.Android.com/apk/res/Android"
          xmlns:tools="http://schemas.Android.com/tools">

    <application
        Android:allowBackup="true" Android:icon="@mipmap/ic_launcher" Android:label="@string/app_name"
        Android:roundIcon="@mipmap/ic_launcher_round" Android:supportsRtl="true"
        Android:theme="@style/AppTheme.NoActionBar" tools:ignore="AllowBackup,GoogleAppIndexingWarning">
        <activity
            Android:name=".MainActivity" Android:label="@string/app_name" Android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action Android:name="Android.intent.action.MAIN"/>

                <category Android:name="Android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val items = ArrayList<Int>(100)
        for (i in 0 until 100)
            items.add(i)
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.grid_item, parent, false)) {}
            }

            override fun getItemCount() = items.size

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                val data = items[position]
                holder.itemView.setBackgroundColor(if (data % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                holder.itemView.textView.text = "item $data"
            }
        }
        val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
            override fun isLongPressDragEnabled() = true
            override fun isItemViewSwipeEnabled() = false

            override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
                val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
                val swipeFlags = if (isItemViewSwipeEnabled) ItemTouchHelper.START or ItemTouchHelper.END else 0
                return makeMovementFlags(dragFlags, swipeFlags)
            }

            override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                if (viewHolder.itemViewType != target.itemViewType)
                    return false
                val fromPosition = viewHolder.adapterPosition
                val toPosition = target.adapterPosition
                val item = items.removeAt(fromPosition)
                items.add(toPosition, item)
                recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
                return true
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                val position = viewHolder.adapterPosition
                items.remove(position)
                recyclerView.adapter!!.notifyItemRemoved(position)
            }

        })
        itemTouchHelper.attachToRecyclerView(recyclerView)
    }

}
2
android developer

Pour échanger deux vues dans une grille, essayez d’utiliser

_adapter.notifyDataSetChanged();

au lieu de

_adapter.notifyItemMoved(viewHolder.getAdapterPosition(), 
target.getAdapterPosition());

Celui-ci m'a aidé

0
Progga Ilma