読者です 読者をやめる 読者になる 読者になる

江戸一番のジャスタウェイ職人のブログ

江戸一番のジャスタウェイ職人

Justaway for Android の並び替え機能の実装

ListViewをDrag and Dropで並び替えたかったので一から書いてみました。

サンプルソース: Sortable ListView on Drag and Drop

  • サクサク並び替えたいのでドラッグ開始はタッチ(指が画面に触れた瞬間)を起点にしている
  • ListView自体のスクロールを考慮する必要がある

という2つのポイントを抑えるため、リストの右端にソート用のハンドルをつけています。ハンドル以外のエリアはListViewのスクロール、ハンドルはソートという使い分ける為ですね。

スクロールを考慮する必要がなければハンドルではなく行全体を当たり判定にして良いでしょう。

f:id:s-aska:20140408184919p:plain

サンプルソース解説付き

package com.example.sortable.app;

import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;

public class MainActivity extends Activity {

    private MyArrayAdapter mAdapter;
    private ListView mListView;

    /**
     * ハンドルをタップするとtrue、
     * 指が画面から離れるとfalseになります。
     */
    private boolean mSortable = false;

    /**
     * ドラッグ中のオブジェクトそのものです、
     * これをremoveしたりinsertする事で並び替えを実現しています。
     */
    private String mDragString;

    /**
     * MotionEventは1pxでも動いたら発火します、
     * リストを跨いだ時だけ処理するためにタップ位置を持っています。
     */
    private int mPosition = -1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mListView = (ListView) findViewById(R.id.listView);

        // いつものコード
        mAdapter = new MyArrayAdapter(this, R.layout.row_string);

        // ダミーデータもりもり
        for (int i = 0; i < 100; i++) {
            mAdapter.add("Dummy ".concat(String.valueOf(i)));
        }

        // いつものコード
        mListView.setAdapter(mAdapter);

        // 大事な所
        mListView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                if (!mSortable) {
                    return false;
                }
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: {
                        break;
                    }
                    case MotionEvent.ACTION_MOVE: {

                        /**
                         * リストの何番目上をタップしているのか容易に取得できるのでとても簡素です。
                         */
                        int position = mListView.pointToPosition((int) event.getX(), (int) event.getY());
                        if (position < 0) {
                            break;
                        }

                        /**
                         * 指がリストを跨いだ瞬間に入れ替え
                         */
                        if (position != mPosition) {
                            mPosition = position;
                            mAdapter.remove(mDragString);
                            mAdapter.insert(mDragString, mPosition);
                        }
                        return true;
                    }
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                    case MotionEvent.ACTION_OUTSIDE: {
                        stopDrag();
                        return true;
                    }
                }
                return false;
            }
        });
    }

    /**
     * mDragString を設定した後に notifyDataSetChanged() する事で Adapter の getView が呼ばれ、
     * mDragString とその行の String との比較によってハイライト状態が更新されます。
     */
    public void startDrag(String string) {
        mPosition = -1;
        mSortable = true;
        mDragString = string;
        mAdapter.notifyDataSetChanged();
    }

    public void stopDrag() {
        mPosition = -1;
        mSortable = false;
        mDragString = null;
        mAdapter.notifyDataSetChanged();
    }

    /**
     * ViewHolderパターン
     */
    static class ViewHolder {
        TextView title;
        TextView handle;
    }

    public class MyArrayAdapter extends ArrayAdapter<String> {

        private ArrayList<String> mStrings = new ArrayList<String>();
        private LayoutInflater mInflater;
        private int mLayout;

        public MyArrayAdapter(Context context, int textViewResourceId) {
            super(context, textViewResourceId);
            this.mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            this.mLayout = textViewResourceId;
        }

        @Override
        public void add(String row) {
            super.add(row);
            mStrings.add(row);
        }

        @Override
        public void insert(String row, int position) {
            super.insert(row, position);
            mStrings.add(position, row);
        }

        @Override
        public void remove(String row) {
            super.remove(row);
            mStrings.remove(row);
        }

        @Override
        public void clear() {
            super.clear();
            mStrings.clear();
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            ViewHolder holder;

            View view = convertView;
            if (view == null) {
                view = mInflater.inflate(this.mLayout, null);
                assert view != null;
                holder = new ViewHolder();
                holder.title = (TextView) view.findViewById(R.id.title);
                holder.handle = (TextView) view.findViewById(R.id.handle);
                view.setTag(holder);
            } else {
                holder = (ViewHolder) view.getTag();
            }

            final String string = mStrings.get(position);

            holder.title.setText(string);

            /**
             * ドラッグハンドルのタップでソートを開始します
             * onClickでは指を離すまでイベントが発火しないのでドラッグ出来ません
             */
            holder.handle.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
                        startDrag(string);
                        return true;
                    }
                    return false;
                }
            });

            /**
             * ドラッグ行のハイライトです、力技ですね。
             */
            if (mDragString != null && mDragString.equals(string)) {
                view.setBackgroundColor(Color.parseColor("#9933b5e5"));
            } else {
                view.setBackgroundColor(Color.TRANSPARENT);
            }

            return view;
        }
    }
}