Androidアプリ開発

RecyclerViewの並び替えと削除 (Drag&Swipe)

この記事は約19分で読めます。
スポンサーリンク

こんにちは、まっさん(@Tera_Msaki)です。

この記事は Android スマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML 構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


 RecyclerViewのアイテムの並び替えと削除で発生する
 予期しない動きに対処する


RecyclerView は、Andoroid アプリで動的リストを作成するライブラリです。
このライブラリの特徴として、スクロール時に View を破棄せず、再利用します。
この為、リソースの有効活用、パフォーマンスの改善、応答性の向上など、
リスト型式の View を作成する際に、必須のライブラリです。

RecyclerView は、便利で強力なライブラリですが、
View を破棄せず、再利用する特徴から、
実装する上で、予期しない動きに嵌(はま)ることがあります。
今回、
RecyclerView でアイテムの並び替えと削除の機能を実装した際に、
嵌った予期しない動きの対処について、説明したいと思います。

予期しない動き(Swipeでアイテム削除すると、アイテムがDrag状態になる現象)の対処前

Drag状態を通知するユーザインタフェース

Drag状態 をユーザに通知する仕掛けとして、
Drag状態のアイテムを半透明にする
UI がよく使用されます。
Drag状態を判別する方法としては、
SimpleCallbackonSelectedChanged を使用します。
actionStateACTION_STATE_DRAG の場合、
Drag状態ですので、アイテムView の アルファ値を半透明(
0.5 )にします。
Drag状態の解除は、SimpleCallbackclearView を使用します。
アイテムView の アルファ値を元(
1.0 )に戻します。

        :
        customSimpleCallback = new CustomSimpleCallback(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, ItemTouchHelper.DOWN) {
            @Override
            public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
                : 
            }
            @Override
            public void onMoved(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, int fromPos, @NonNull RecyclerView.ViewHolder target, int toPos, int x, int y) {
                super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
                : 
            }
            @Override
            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
                : 
            }
            @Override
            public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
                super.onSelectedChanged(viewHolder, actionState);
                if (actionState == ACTION_STATE_DRAG && viewHolder != null) {
                    viewHolder.itemView.setAlpha(0.5f);
                }
            }
            @Override
            public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
                super.clearView(recyclerView, viewHolder);
                viewHolder.itemView.setAlpha(1.0f);
                :
            }
        };
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(customSimpleCallback);
        itemTouchHelper.attachToRecyclerView(todoView);
        :

予期しない動きの対処として、SimpleCallback ではなく、
SimpleCallback を継承して作成した CustomSimpleCallback を使用しています。
 

予期しない動きの対処

RecyclerView にスワイプ操作とドラッグ操作をアプリに実装した場合、
RecyclerView の View を破棄せず、再利用する特徴から、
Swipe操作によるアイテムの削除で、
RecyclerViewの描画の際に、
アイテムが Drag状態になることがあります。
この動作の対策として、
RecyclerView のアイテム描画中は、
ドラッグ操作を無効にすることで回避できます。
ドラッグ操作を無効にする場合、SimpleCallbackを使用しますが、
スコープ外からも、擽(くすぐ)ってあげる必要があるので、
SimpleCallback を継承した新しいクラスを作成します。

public class CustomSimpleCallback extends ItemTouchHelper.SimpleCallback {
    private boolean                     drag = false;
    private int                         dragDirs = 0;
    private int                         swipeDirs = 0;

    // コンストラクタ
    public CustomSimpleCallback(int dragDirs, int swipeDirs) {
        super(dragDirs, swipeDirs);
        this.dragDirs = dragDirs;
        this.swipeDirs = swipeDirs;
    }

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        int dragFlags = drag ? dragDirs : 0;
        return makeMovementFlags(dragFlags, swipeDirs);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return false;
    }
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    }

    public void setDraggable(boolean value) {
        drag = value;
    }
}

setDraggable を使用して、
getMovementFlags のドラッグ操作の向き(dragDirs)を制御することで、
ドラッグ操作の有効・無効を切り替えます。

ドラッグ操作を擽る

RecycelerView の描画中はドラッグ操作を無効にして、
描画が終わったらドラッグ操作を有効にします。

初回表示のアイテムタッチでドラッグ操作を有効 にするために、
onResume
RecycelerViewの描画( updateTodoList )では、
初回( resume == 0 )のみドラッグ操作を有効にします。
onBindViewのアイテム描画で、
アイテム( imageView )のタッチをリスナーでフックして、
ドラッグ操作を有効にします。
初回の表示でアイテムがない場合( todoList.size() == 0 )も、
ドラッグ操作を有効にします。
onPause でドラッグ操作を有効にします。
初回以降 は、
RecycelerViewの描画の最初で、
ドラッグ操作を無効( new CustomSimpleCallback )にして、
clearViewで有効にします。

    : 
    private HorizontalListView          todoView;
    private HorizontalListView.Adapter  todoAdapter;
    private CustomSimpleCallback        customSimpleCallback;
    private ArrayList<Todo>             todoList = new ArrayList<>();
    private int                         resume = 0;
    private boolean                     drag = false;
    :
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        :
        todoView = view.findViewById(R.id.todoView);
        return view;
    }

    @Override
    public void onResume() {
        :
        // データ読込
        mainActivity.assistantsDatabaseHelper.selTodoList(true, resume == 0);
        resume++;
    }

    @Override
    public void onPause() {
        if (!drag) {
            customSimpleCallback.setDraggable(true);
            drag = true;
        }
        :
        super.onPause();
    }

    :
    @SuppressLint("NotifyDataSetChanged")
    public void updateTodoList(List<Todo> todos, boolean draggable) {
        todoList = (ArrayList<Todo>) todos;
        todoAdapter = new HorizontalListView.ArrayAdapter(todoList) {
            @Override
            public View getView(ViewGroup parent) {
                return LayoutInflater.from(parent.getContext()).inflate(R.layout.item_horizontal2, parent, false);
            }
            @SuppressLint("ClickableViewAccessibility")
            @Override
            public void onBindView(View view, Object data, int position) {
                if (todoList.size() > position) {
                    ImageView imageView = view.findViewById(R.id.image2);
                    // イメージ表示 
                    imageView.setOnTouchListener((v,event) -> {
                        if (event.getAction() == MotionEvent.ACTION_DOWN) {
                            if (draggable && !drag) {
                                customSimpleCallback.setDraggable(true);
                                drag = true;
                            }
                        }
                        return false;
                    });
           view.setAlpha(1.0f);
                }
            }
        };
        todoView.initialize(context);
        todoView.setAdapter(todoAdapter);
        customSimpleCallback = new CustomSimpleCallback(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, ItemTouchHelper.DOWN) {
            @Override
            public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
                todoAdapter.notifyItemMoved(viewHolder.getBindingAdapterPosition(), target.getBindingAdapterPosition());
                return true;
            }
            @Override
            public void onMoved(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, int fromPos, @NonNull RecyclerView.ViewHolder target, int toPos, int x, int y) {
                super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
                todoAdapter.notifyDataSetChanged();
                // アイテムの入れ替え
                : 
            }
            @Override
            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
                // アイテムの削除
         :
            }
            @Override
            public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
                super.onSelectedChanged(viewHolder, actionState);
                if (actionState == ACTION_STATE_DRAG && viewHolder != null) {
                    viewHolder.itemView.setAlpha(0.5f);
                }
            }
            @Override
            public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
                super.clearView(recyclerView, viewHolder);
                viewHolder.itemView.setAlpha(1.0f);
                setDraggable(true);
                drag = true;
            }
        };
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(customSimpleCallback);
        itemTouchHelper.attachToRecyclerView(todoView);
        if (!drag && draggable && todoList.size() == 0) {
            customSimpleCallback.setDraggable(true);
            drag = true;
        }
    }
    :

左右にスクロールさせるため、RecyclerViewではなく、
RecyclerView を継承して作成したHorizontalListViewを使用しています。 

customSimpleCallback.setDraggable は、1回だけ実行するのがポイントです。 

◎AssistantsDatabaseHelper(DBアクセス)

    :
private List<Todo> todoList = new ArrayList<>();
    :
    public void selTodoList(boolean load, boolean draggable) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            try {
         // DB読み込み
                 todoList = todoDao.selectTaskList();   
         :
            } catch (Exception e) {
                e.printStackTrace();
            }
            handler.post(() -> {
                if (load) mainActivity.getHomeFragment().updateTodoList(todoList, draggable);
            });
        });
    }
    :

対処後の動画

予期しない動きの対処として、
RecycelerView を描画中はドラッグ操作を無効にして、
ACTION_STATE_DRAG
を発生しないようにします。
Drag状態を解除する方法として、
NotifyDataSetChanged を実行する方法もあります。

予期しない動き(Swipeでアイテム削除すると、アイテムがDrag状態になる現象)の対処後

今回は、ここまでです。

参考 :SimpleCallback

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちら
から、お願いいたします♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

初めての Android のアプリ開発では、アプリケーション開発経験がない方や、
アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、
書籍などによる独学ではアプリ開発できるようになるには、
かなりの時間がかかりますので、オンラインスクールでの習得をおススメします。

未経験者からシステムエンジニアを目指すのに最適かと、
まずは無料相談から♪

無料でJava言語を学べるのは、かなり魅力的♪
でも、応募資格は35歳以下です、、、
2022年12月から説明会が土曜日開催が追加されていますよ♪
説明会では、希望者に対してプログラミング体験もできるよ♪

未経験者からプログラマーを目指すのに最適かと、
まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています、
お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適かと、
まずは無料オンライン相談から♪

コメント欄

タイトルとURLをコピーしました