ユーザがソート可能なListViewをすこしリッチにしてみた

ネタ元は

こちら

ユーザがソート可能なListView
http://d.hatena.ne.jp/vvakame/20100718#1279453854

id:vvakame さんが素晴らしいコードを書いていたので、勝手に改変しましたすこしリッチにしてみました。

ごめんなさい

変数名とかだいぶいじっちゃいました。
すこしだけ変えるつもりが、たくさん変えちゃった(えへ)
コメント少なくてごめんなさい。途中でくじけました。

apk

Android1.6-2.2
HT-03A(Android1.6)で動作確認しました。
DragonDropList.apk 直

使い方

  • 初期状態だと普通のListViewです。
  • sort toggleチェックボックスをオンにするとソートモードに入ります。
  • 要素の入れ替えは直感的に分かるはず。
  • 上の×印にドロップすると要素が削除されます。
  • sort toggleチェックボックスをオフにすると通常のListViewに戻ります。

足りないところ

ソート中にスクロールできないのでどうにかしたいけど、いい方法思いつかない
ソート用のボタンを押すと掴むことができるとか、長押しすると掴むことができるとか
スマートな方法ないですかね・・・?

ソース

DragnDropActivity.java
package jp.tomorrowkey.android.dragondroplist;

import java.util.ArrayList;
import java.util.Arrays;

import jp.tomorrowkey.android.dragondroplist.DragnDropListView.SortableAdapter;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

public class DragnDropActivity extends Activity implements OnClickListener {

  private static String[] items = { "01.北海道", "02.青森県", "03.岩手県", "04.宮城県", "05.秋田県", "06.山形県", "07.福島県", "08.茨城県", "09.栃木県", "10.群馬県", "11.埼玉県", "12.千葉県", "13.東京都", "14.神奈川県", "15.新潟県", "16.富山県", "17.石川県", "18.福井県", "19.山梨県", "20.長野県", "21.岐阜県", "22.静岡県", "23.愛知県", "24.三重県", "25.滋賀県", "26.京都府", "27.大阪府", "28.兵庫県", "29奈良県", "30.和歌山県", "31.鳥取県", "32.島根県", "33.岡山県", "34.広島県", "35.山口県", "36.徳島県", "37.香川県", "38.愛媛県", "39.高知県", "40.福岡県", "41.佐賀県", "42.長崎県", "43.熊本県", "44.大分県", "45.宮崎県", "46.鹿児島県", "47.沖縄県" };
  private static final int SELECTED_BG_COLOR = Color.argb(128, 255, 255, 255);
  private static final int HOVER_BG_COLOR = Color.argb(128, 255, 102, 0);

  private ArrayList<String> array;
  private StringArrayAdapter adapter = null;

  private DragnDropListView list;
  private ImageView imgRemoveTile;
  private CheckBox chkSort;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    array = new ArrayList<String>(Arrays.asList(items));
    adapter = new StringArrayAdapter(this, array);

    list = (DragnDropListView) findViewById(R.id.list);
    list.setAdapter(adapter);

    chkSort = (CheckBox) findViewById(R.id.chkSort);
    chkSort.setOnClickListener(this);

    imgRemoveTile = (ImageView) findViewById(R.id.imgRemoveTile);
    list.setRemoveTile(imgRemoveTile);
  }

  @Override
  public void onClick(View v) {
    boolean sortable = chkSort.isChecked();
    list.setSortMode(sortable);
    if (sortable) {
      imgRemoveTile.setVisibility(View.VISIBLE);
    } else {
      imgRemoveTile.setVisibility(View.GONE);
    }
  }

  class StringArrayAdapter extends ArrayAdapter<String> implements SortableAdapter {

    private ArrayList<String> list;
    private LayoutInflater inflater;
    private int selectedPosition = -1;
    private int hoverPosition = -1;

    public StringArrayAdapter(Context context, ArrayList<String> list) {
      super(context, R.layout.row, list);
      this.list = list;
      inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public View getView(int position, View convertView, ViewGroup parent) {
      View view = null;
      ViewHolder holder = null;
      if (convertView == null) {
        view = inflater.inflate(R.layout.row, null);
        holder = new ViewHolder();
        holder.txtString = (TextView) view.findViewById(R.id.txtString);
        view.setTag(holder);
      } else {
        view = convertView;
        holder = (ViewHolder) view.getTag();
      }

      if (selectedPosition == hoverPosition) {
        if (position == selectedPosition) {
          view.setBackgroundColor(HOVER_BG_COLOR);
        } else {
          view.setBackgroundResource(android.R.drawable.list_selector_background);
        }
      } else {
        if (position == selectedPosition) {
          view.setBackgroundColor(SELECTED_BG_COLOR);
        } else if (position == hoverPosition) {
          view.setBackgroundColor(HOVER_BG_COLOR);
        } else {
          view.setBackgroundResource(android.R.drawable.list_selector_background);
        }
      }
      holder.txtString.setText(list.get(position));

      return view;
    }

    @Override
    public void setSelectedPosition(int position) {
      if (selectedPosition != position) {
        selectedPosition = position;
        notifyDataSetChanged();
      }
    }

    @Override
    public void setHoverPosition(int position) {
      if (hoverPosition != position) {
        hoverPosition = position;
        notifyDataSetChanged();
      }
    }
  }

  class ViewHolder {
    TextView txtString;
  }
}
DragnDropListView.java
package jp.tomorrowkey.android.dragondroplist;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;

public class DragnDropListView extends ListView {
  private static final String TAG = DragnDropListView.class.getSimpleName();
  private static final boolean DEBUG = false;

  private static final int SCROLL_SPEED_FAST = 25;
  private static final int SCROLL_SPEED_SLOW = 8;

  private static final int MOVING_ITEM_BG_COLOR = Color.argb(128, 0, 0, 0);
  private static final int HOVER_REMOVE_ITEM_BG_COLOR = Color.argb(200, 255, 0, 0);

  private boolean sortMode = false;
  private DragListener mDrag = new DragListenerImpl();
  private DropListener mDrop = new DropListenerImpl();
  private RemoveListener mRemove = new RemoveListenerImpl();

  public DragnDropListView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public DragnDropListView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  private SortableAdapter adapter;
  private Bitmap mDragBitmap = null;
  private ImageView mDragView = null;
  private WindowManager.LayoutParams mWindowParams = null;
  private int mFrom = -1;

  private View mRemoveTile = null;
  private Rect mRemoveHit = null;

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    if (!sortMode) {
      return super.onTouchEvent(event);
    }

    int index = -1;
    final int x = (int) event.getX();
    final int y = (int) event.getY();

    int action = event.getAction();

    if (action == MotionEvent.ACTION_DOWN) {
      index = pointToIndex(event);

      if (index < 0) {
        return false;
      }

      mFrom = index;
      startDrag();

      adapter.setSelectedPosition(index);

      return true;
    } else if (action == MotionEvent.ACTION_MOVE) {
      final int height = getHeight();
      final int fastBound = height / 9;
      final int slowBound = height / 4;
      final int center = height / 2;

      int speed = 0;
      if (event.getEventTime() - event.getDownTime() < 500) {
        // 500ミリ秒間はスクロールなし
      } 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;
      }

      if (DEBUG) {
        Log.d(TAG, "ACTION_MOVE y=" + y + ", height=" + height + ", fastBound=" + fastBound + ", slowBound=" + slowBound + ", center=" + center + ", speed=" + speed);
      }

      View v = null;
      if (speed != 0) {
        // 横方向はとりあえず考えない
        int centerPosition = pointToPosition(0, center);
        if (centerPosition == AdapterView.INVALID_POSITION) {
          centerPosition = pointToPosition(0, center + getDividerHeight() + 64);
        }
        v = getChildByIndex(centerPosition);
        if (v != null) {
          int pos = v.getTop();
          setSelectionFromTop(centerPosition, pos - speed);
        }
      }

      if (mDragView != null) {
        if (mDragView.getHeight() < 0) {
          mDragView.setVisibility(View.INVISIBLE);
        } else {
          mDragView.setVisibility(View.VISIBLE);
          if (this.isHitRemoveTile(x, y)) {
            mDragView.setBackgroundColor(HOVER_REMOVE_ITEM_BG_COLOR);
          } else {
            mDragView.setBackgroundColor(MOVING_ITEM_BG_COLOR);
          }
        }

        mWindowParams.x = getLeft();
        mWindowParams.y = getTop() + y;

        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        wm.updateViewLayout(mDragView, mWindowParams);

        if (mDrag != null) {
          index = pointToIndex(event);
          mDrag.drag(mFrom, index);
        }

        adapter.setHoverPosition(index);

        return true;
      }

    } else if (action == MotionEvent.ACTION_UP) {
      if (isHitRemoveTile(x, y)) {
        // 削除イメージにヒットしていた場合、削除する
        if (mRemove != null)
          mRemove.remove(mFrom);
      } else {
        // 削除イメージにヒットしていなければ、要素の入れ替えをする
        if (mDrop != null) {
          index = pointToIndex(event);
          mDrop.drop(mFrom, index);
        }
      }

      // ドラッグ中の項目の表示を削除する
      endDrag();

      adapter.setHoverPosition(-1);
      adapter.setSelectedPosition(-1);

      return true;
    } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_OUTSIDE) {
      // ドラッグ中の項目の表示を削除する
      endDrag();
      adapter.setHoverPosition(-1);
      adapter.setSelectedPosition(-1);
      return true;

    } else {
      Log.d(TAG, "Unknown event action=" + action);
    }

    return super.onTouchEvent(event);
  }

  /**
   * ドラッグを開始したときに呼び出される<br />
   * ドラッグ中の項目を表示する
   */
  private void startDrag() {
    WindowManager wm;
    View view = getChildByIndex(mFrom);

    final Bitmap.Config c = Bitmap.Config.ARGB_8888;
    mDragBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), c);
    Canvas canvas = new Canvas();
    canvas.setBitmap(mDragBitmap);
    view.draw(canvas);

    if (mWindowParams == null) {
      mWindowParams = new WindowManager.LayoutParams();
      mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;

      mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
      mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
      mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
      mWindowParams.format = PixelFormat.TRANSLUCENT;
      mWindowParams.windowAnimations = 0;
      mWindowParams.x = 0;
      mWindowParams.y = 0;
    }

    ImageView v = new ImageView(getContext());
    v.setBackgroundColor(MOVING_ITEM_BG_COLOR);
    v.setImageBitmap(mDragBitmap);

    wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    if (mDragView != null) {
      wm.removeView(mDragView);
    }
    wm.addView(v, mWindowParams);
    mDragView = v;
  }

  /**
   * ドラッグアンドドロップが終了したときに呼び出される<br />
   * ドラッグ中の項目の表示を削除する
   */
  private void endDrag() {
    if (mDragView == null) {
      return;
    }
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    wm.removeView(mDragView);
    mDragView = null;
    // リサイクルするとたまに死ぬけどタイミング分からない
    // Desireでおきる。ht-03aだと発生しない
    // mDragBitmap.recycle();
    mDragBitmap = null;
  }

  /**
   * 指定された座標が削除画像に被っているか返す
   * 
   * @param x
   * @param y
   * @return
   */
  private boolean isHitRemoveTile(int x, int y) {
    if (mRemoveTile != null && mRemoveTile.getVisibility() == View.VISIBLE) {
      if (mRemoveHit == null) {
        mRemoveHit = new Rect();
      }
      mRemoveTile.getHitRect(mRemoveHit);
      if (mRemoveHit.contains(x + getLeft(), y + getTop())) {
        return true;
      } else {
        return false;
      }
    } else {
      return false;
    }
  }

  /**
   * 指定された要素番号のViewを返す<br />
   * 要素番号はソースとなるListの要素番号を指定する
   * 
   * @param index
   * @return
   */
  private View getChildByIndex(int index) {
    return getChildAt(index - getFirstVisiblePosition());
  }

  /**
   * MotionEventから要素番号に変換する
   * 
   * @param ev
   * @return
   */
  private int pointToIndex(MotionEvent event) {
    return pointToIndex((int) event.getX(), (int) event.getY());
  }

  /**
   * 座標から要素番号に変換する
   * 
   * @param x
   * @param y
   * @return
   */
  private int pointToIndex(int x, int y) {
    return (int) pointToPosition(x, y);
  }

  /**
   * 削除画像の設定
   * 
   * @param v
   */
  public void setRemoveTile(View v) {
    mRemoveTile = v;
  }

  public void setOnDragListener(DragListener listener) {
    mDrag = listener;
  }

  public void setOnDropListener(DragListener listener) {
    mDrag = listener;
  }

  public void setOnRemoveListener(RemoveListener listener) {
    mRemove = listener;
  }

  public interface DragListener {
    public void drag(int from, int to);
  }

  public interface DropListener {
    public void drop(int from, int to);
  }

  public interface RemoveListener {
    public void remove(int which);
  }

  class DragListenerImpl implements DragListener {
    public void drag(int from, int to) {
      if (DEBUG) {
        Log.d(TAG, "DragListenerImpl drag event. from=" + from + ", to=" + to);
      }
    }
  }

  public class DropListenerImpl implements DropListener {
    @SuppressWarnings("unchecked")
    public void drop(int from, int to) {
      if (DEBUG) {
        Log.d(TAG, "DropListenerImpl drop event. from=" + from + ", to=" + to);
      }

      if (from == to || from < 0 || to < 0) {
        return;
      }

      Adapter adapter = getAdapter();
      if (adapter != null && adapter instanceof ArrayAdapter) {
        ArrayAdapter arrayAdapter = (ArrayAdapter) adapter;
        Object item = adapter.getItem(from);

        arrayAdapter.remove(item);
        arrayAdapter.insert(item, to);
      }
    }
  }

  public class RemoveListenerImpl implements RemoveListener {
    @SuppressWarnings("unchecked")
    public void remove(int which) {
      if (DEBUG) {
        Log.d(TAG, "RemoveListenerImpl remove event. which=" + which);
      }

      if (which < 0) {
        return;
      }

      Adapter adapter = getAdapter();
      if (adapter != null && adapter instanceof ArrayAdapter) {
        ArrayAdapter arrayAdapter = (ArrayAdapter) adapter;
        Object item = adapter.getItem(which);

        arrayAdapter.remove(item);
      }
    }
  }

  /**
   * 並び替えをするかを設定する
   * 
   * @param sortMode
   */
  public void setSortMode(boolean sortMode) {
    this.sortMode = sortMode;
  }

  /**
   * 並び替えをするかを返す
   * 
   * @return
   */
  public boolean isSortMode() {
    return sortMode;
  }

  /**
   * ListViewにAdapterを設定する<br />
   * Sortableを実装したAdapter以外を受け付けません
   * 
   * @param adapter
   */
  @Override
  public void setAdapter(ListAdapter adapter) {
    if (adapter instanceof SortableAdapter) {
      this.adapter = (SortableAdapter) adapter;
      super.setAdapter(adapter);
    } else {
      throw new RuntimeException("AdapterはSortableを実装する必要があります");
    }
  }

  interface SortableAdapter {
    void setSelectedPosition(int position);

    void setHoverPosition(int position);
  }
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <CheckBox
    android:text="sort toggle" 
    android:id="@+id/chkSort"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" />
  <ImageView 
    android:id="@+id/imgRemoveTile"
    android:src="@drawable/remove_tile"
    android:background="#80FF0000"
    android:scaleType="center"
    android:visibility="gone"
    android:layout_margin="2dip"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
  <jp.tomorrowkey.android.dragondroplist.DragnDropListView
    android:id="@+id/list" 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" />
</LinearLayout>
row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <TextView
    android:id="@+id/txtString"
    android:textSize="24dip"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" />
</LinearLayout>
remove_tile.png

質問とかあったら

twitterで答えるよ!!