ListViewの項目をドラッグして並び替え可能にしてみた
先日の記事(こんなに簡単だとは思わなかった!Viewのドラッグ方法)で、Viewをドラッグ・アンド・ドロップしてみました。今回は、発展させて、ListViewの項目をドラッグ・アンド・ドロップして並び替えられるようにしてみました。少し長いですが、作成したプログラムを紹介します。
プログラム構成
画面とJavaクラスの構成を図にしました。DragListViewは、AndroidのListViewを継承し、ドラッグ・アンド・ドロップの主な制御を行なっています。DragListAdapterは、ListViewに表示する項目とその並び順を管理しています。PopupViewは、ドラッグ時に選択項目をポップアップ表示するビューです。
プログラム詳細(レイアウトXML)
レイアウトXML(drag_list_activity.xml)は、次のようになります。今回作成したDragListViewを配置しています。ドラッグ時に表示する選択項目のポップアップは、実行時に作成するため、レイアウトXMLには出て来ません。レイアウトやビューに余白や背景色を付けているのは、画面上でビューの配置をわかりやすくするためです。
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/FrameLayout1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#555555" android:padding="20dip" > <com.kurukurupapa.tryandroidui.draglist.DragListView android:id="@+id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#000000" > </com.kurukurupapa.tryandroidui.draglist.DragListView> </FrameLayout>
プログラム詳細(アクティビティ - DragListActivity)
アクティビティのクラス(DragListActivity.java)です。上記のレイアウトXMLを呼び出し、DragListViewとDragListAdapterを紐付けています。
public class DragListActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.drag_list_activity); DragListAdapter adapter = new DragListAdapter(this); DragListView listView = (DragListView) findViewById(R.id.list); listView.setAdapter(adapter); } }
プログラム詳細(リストビュー - DragListView)
ドラッグ・アンド・ドロップの制御を行うクラス(DragListView.java)です。AndroidのListViewクラスを継承しています。タッチイベントが発生すると、DragListAdapterとPopupViewの、ドラッグ開始、ドラッグ中、ドラッグ終了メソッドを各々呼び出し、画面再描画しています。リスト項目の長押しでドラッグ開始するようにしています。
public class DragListView extends ListView implements AdapterView.OnItemLongClickListener { private static final int SCROLL_SPEED_FAST = 25; private static final int SCROLL_SPEED_SLOW = 8; private DragListAdapter adapter; private PopupView popupView; private MotionEvent downEvent; private boolean dragging = false; public DragListView(Context context, AttributeSet attrs) { super(context, attrs); popupView = new PopupView(context); setOnItemLongClickListener(this); } @Override public void setAdapter(ListAdapter adapter) { if (adapter instanceof DragListAdapter == false) { throw new RuntimeException("引数adapterがDragListAdapterクラスではありません。"); } super.setAdapter(adapter); this.adapter = (DragListAdapter) adapter; } /** * 長押しイベント<br> * ドラッグを開始する。当イベントの前に、タッチイベント(ACTION_DOWN)が呼ばれている前提。 */ @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { return startDrag(downEvent); } /** * タッチイベント<br> * ドラッグしている項目の移動や、ドラッグ終了の制御を行う。 */ @Override public boolean onTouchEvent(MotionEvent event) { boolean result = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: storeMotionEvent(event); break; case MotionEvent.ACTION_MOVE: result = doDrag(event); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: result = stopDrag(event); break; } // イベントを処理していなければ、親のイベント処理を呼ぶ。 // 長押しイベントを発生させるため、ACTION_DOWNイベント時は、親のイベント処理を呼ぶ。 if (result == false) { result = super.onTouchEvent(event); } return result; } /** * 長押しイベント時に、タッチ位置を取得するため、ACTION_DOWN時のMotionEventを保持する。 */ private void storeMotionEvent(MotionEvent event) { downEvent = event; } /** * ドラッグ開始 */ private boolean startDrag(MotionEvent event) { dragging = false; int x = (int) event.getX(); int y = (int) event.getY(); // イベントから position を取得 // 取得した position が 0未満=範囲外の場合はドラッグを開始しない int position = eventToPosition(event); if (position < 0) { return false; } // アダプターにドラッグ対象項目位置を渡す adapter.startDrag(position); // ドラッグ中のリスト項目の描画を開始する popupView.startDrag(x, y, getChildByIndex(position)); // リストビューを再描画する invalidateViews(); dragging = true; return true; } /** * ドラッグ処理 */ private boolean doDrag(MotionEvent event) { if (!dragging) { return false; } int x = (int) event.getX(); int y = (int) event.getY(); int position = pointToPosition(x, y); // ドラッグの移動先リスト項目が存在する場合 if (position != AdapterView.INVALID_POSITION) { // アダプターのデータを並び替える adapter.doDrag(position); } // ドラッグ中のリスト項目の描画を更新する popupView.doDrag(x, y); // リストビューを再描画する invalidateViews(); // 必要あればスクロールさせる // 注意:invalidateViews()後に処理しないとスクロールしなかった setScroll(event); return true; } /** * ドラッグ終了 */ private boolean stopDrag(MotionEvent event) { if (!dragging) { return false; } // アダプターにドラッグ対象なしを渡す adapter.stopDrag(); // ドラッグ中のリスト項目の描画を終了する popupView.stopDrag(); // リストビューを再描画する invalidateViews(); dragging = false; return true; } /** * 必要あればスクロールさせる。<br> * 座標の計算が煩雑になるので当Viewのマージンとパディングはゼロの前提とする。 */ private void setScroll(MotionEvent event) { int y = (int) event.getY(); int height = getHeight(); int harfHeight = height / 2; int harfWidth = getWidth() / 2; // スクロール速度の決定 int speed; int fastBound = height / 9; int slowBound = height / 4; if (event.getEventTime() - event.getDownTime() < 500) { // ドラッグの開始から500ミリ秒の間はスクロールしない speed = 0; } else if (y < slowBound) { speed = y < fastBound ? -SCROLL_SPEED_FAST : -SCROLL_SPEED_SLOW; } else if (y > height - slowBound) { speed = y > height - fastBound ? SCROLL_SPEED_FAST : SCROLL_SPEED_SLOW; } else { // スクロールなしのため処理終了 return; } // 画面の中央にあるリスト項目位置を求める // 横方向はとりあえず考えない // 中央がちょうどリスト項目間の境界の場合は、位置が取得できないので、 // 境界からずらして再取得する。 int middlePosition = pointToPosition(harfWidth, harfHeight); if (middlePosition == AdapterView.INVALID_POSITION) { middlePosition = pointToPosition(harfWidth, harfHeight + getDividerHeight()); } // スクロール実施 final View middleView = getChildByIndex(middlePosition); if (middleView != null) { setSelectionFromTop(middlePosition, middleView.getTop() - speed); } } /** * MotionEvent から position を取得する */ private int eventToPosition(MotionEvent event) { return pointToPosition((int) event.getX(), (int) event.getY()); } /** * 指定インデックスのView要素を取得する */ private View getChildByIndex(int index) { return getChildAt(index - getFirstVisiblePosition()); } }
プログラム詳細(リストデータ管理 - DragListAdapter)
リスト項目の並び替え行うクラス(DragListAdapter.java)です。AndroidのBaseAdapterクラスを継承しています。データの並び替えを行う処理を実装しています。ドラッグ中の選択項目は、ポップアップ表示する方針ですので、当クラスでは非表示にしています。
public class DragListAdapter extends BaseAdapter { private static final String[] items = { "Android 1.0(APIレベル1)", "Android 1.1(APIレベル2)", "Android 1.5(APIレベル3)", "Android 1.6(APIレベル4)", "Android 2.0(APIレベル5)", "Android 2.0.1(APIレベル6)", "Android 2.1(APIレベル7)", "Android 2.2(APIレベル8)", "Android 2.3(APIレベル9)", "Android 2.3.3(APIレベル10)", "Android 3.0(APIレベル11)", "Android 3.1(APIレベル12)", "Android 3.2(APIレベル13)", "Android 4.0(APIレベル14)", }; private Context context; private int currentPosition = -1; public DragListAdapter(Context context) { this.context = context; } @Override public int getCount() { return items.length; } @Override public Object getItem(int position) { return items[position]; } @Override public long getItemId(int position) { return position; } /** * リスト項目のViewを取得する */ @Override public View getView(int position, View convertView, ViewGroup parent) { // View作成 if (convertView == null) { convertView = new TextView(context); } TextView textView = (TextView) convertView; textView.setTextSize(30); // データ設定 textView.setText((String) getItem(position)); // ドラッグ対象項目は、ListView側で別途描画するため、非表示にする。 if (position == currentPosition) { textView.setVisibility(View.INVISIBLE); } else { textView.setVisibility(View.VISIBLE); } return textView; } /** * ドラッグ開始 * * @param position */ public void startDrag(int position) { this.currentPosition = position; } /** * ドラッグに従ってデータを並び替える * * @param newPosition */ public void doDrag(int newPosition) { String item = items[currentPosition]; if (currentPosition < newPosition) { // リスト項目を下に移動している場合 for (int i = currentPosition; i < newPosition; i++) { items[i] = items[i + 1]; } } else if (currentPosition > newPosition) { // リスト項目を上に移動している場合 for (int i = currentPosition; i > newPosition; i--) { items[i] = items[i - 1]; } } items[newPosition] = item; currentPosition = newPosition; } /** * ドラッグ終了 */ public void stopDrag() { this.currentPosition = -1; } }
プログラム詳細(ポップアップ表示ビュー - PopupView)
ドラッグ中の選択項目をポップアップ表示するクラス(PopupView.java)です。AndroidのImageViewクラスを継承しています。このビューはWindowManager上に表示しています。WindowManager上では座標系が変わることに注意しなければならないため、getLocationInWindow()メソッドを使用しています。
public class PopupView extends ImageView { private static final Bitmap.Config DRAG_BITMAP_CONFIG = Bitmap.Config.ARGB_8888; private static final int BACKGROUND_COLOR = Color.argb(128, 0xFF, 0xFF, 0xFF); private static final int Y_GAP = 20; private WindowManager windowManager; private WindowManager.LayoutParams layoutParams; private boolean dragging = false; private int baseX; private int baseY; private int[] itemLocation = new int[2]; public PopupView(Context context) { super(context); windowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); // レイアウトを初期化する initLayoutParams(); } /** * ドラッグ開始 * * @param itemView */ public void startDrag(int x, int y, View itemView) { // ドラッグ終了処理が未完了の場合、前回のドラッグ処理が不正に終了しているかもしれない。 // 念のため、ドラッグ終了処理を行う。 if (dragging) { stopDrag(); } // ドラッグ開始座標を保持する baseX = x; baseY = y; // ドラッグする項目の初期位置を保持する itemView.getLocationInWindow(itemLocation); // ドラッグ中の画像イメージを設定する setBitmap(itemView); // WindowManagerに登録する updateLayoutParams(x, y); windowManager.addView(this, layoutParams); dragging = true; } /** * ドラッグ中処理 * * @param x * @param y */ public void doDrag(int x, int y) { // ドラッグ開始していなければ中止 if (dragging == false) { return; } // ImageViewの位置を更新 updateLayoutParams(x, y); windowManager.updateViewLayout(this, layoutParams); } /** * ドラッグ項目の描画を終了する */ public void stopDrag() { // ドラッグ開始していなければ中止 if (dragging == false) { return; } // WindowManagerから除去する windowManager.removeView(this); dragging = false; } /** * ドラッグ中の項目を表す画像を作成する */ private void setBitmap(View itemView) { Bitmap bitmap = Bitmap.createBitmap(itemView.getWidth(), itemView.getHeight(), DRAG_BITMAP_CONFIG); Canvas canvas = new Canvas(); canvas.setBitmap(bitmap); itemView.draw(canvas); setImageBitmap(bitmap); setBackgroundColor(BACKGROUND_COLOR); } /** * ImageView 用 LayoutParams の初期化 */ private void initLayoutParams() { // getLocationInWindow()と座標系を合わせるためFLAG_LAYOUT_IN_SCREENを設定する。 // FLAG_LAYOUT_IN_SCREENを設定すると、端末ディスプレイ全体の左上を原点とする座標系となる。 // 設定しない場合、ステータスバーを含まない左上を原点とする。 layoutParams = new WindowManager.LayoutParams(); layoutParams.gravity = Gravity.TOP | Gravity.LEFT; layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; layoutParams.format = PixelFormat.TRANSLUCENT; layoutParams.windowAnimations = 0; } /** * ImageView 用 LayoutParams の座標情報を更新 */ private void updateLayoutParams(int x, int y) { // ドラッグ中であることが分かるように少し上にずらす layoutParams.x = itemLocation[0] + x - baseX; layoutParams.y = itemLocation[1] + y - baseY - Y_GAP; } }