2015年2月19日木曜日

Androidホームアプリ風のドラッグアンドドロップを実装する方法

目指すもの

グリッド表示されたアプリ一覧から、アイコン長押しでドラッグ&ドロップし、任意の場所へアプリを移動するUI

動画をとってみた


アプリの表示

まずは、アプリアイコンの表示を実装する。アプリアイコンは、TextViewへsetCompoundDrawablesWithIntrinsicBounds()メソッドでアイコン画像を指定することで実現する。

layout/appicon.xml

    
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingTop="4dp"
    android:textSize="12dp"
    android:singleLine="true"
    android:ellipsize="end"
    android:textColor="@android:color/white"
    android:gravity="center_horizontal|center_vertical" />

Javaコード

アプリアイコンを作るところ

// アイコン画像をロード(とりあえず電話っぽいアイコンにしとく)
Drawable icon = getResources().getDrawable(android.R.drawable.ic_menu_call);

// TextViewをXMLから生成
TextView appIcon
    = (TextView) getLayoutInflater().inflate(R.layout.appicon, null);
// アプリ名称を設定
appIcon.setText("でんわ");
// アイコンを設定(left, top, right, bottomの順で指定)
appIcon.setCompoundDrawables(null, icon, null, null);

アプリアイコンをコンテナ(ここではRelativeLayout)へ追加する

// コンテナへ追加
RelativeLayout c = (RelativeLayout) findViewById(R.id.container);
c.addView(appIcon);

Viewをドラッグ可能にする

次に、アプリアイコンをドラッグできるようにする。TextViewのstartDrag()メソッドを呼び出せば良く、長押しでドラッグ開始とするため、OnLongClickListenerを実装してアプリアイコンへセットする。

特筆すべき点として、ドロップ先へデータを渡すためstartDrag()の第3引数にViewそのものをセットしておく。

/**
 * Viewの長押しイベントをハンドリングするリスナークラス
 */
private static class MyLongClickListener implements View.OnLongClickListener
{
    /**
     * Viewが長押しされた際にシステムから呼び出される。
     */
    @Override
    public boolean onLongClick(View v)
    {
        // ドラッグ&ドロップで受け渡しするデータ(使わないのでダミー)
        ClipData tmpData = ClipData.newPlainText("dummy", "dummy");
        // ドラッグ中に表示するイメージのビルダー
        View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);

        // ドラッグを開始
        v.startDrag(tmpData, shadow, v, 0);
        
        return true;
    }
}

アプリアイコンへリスナーをセットする

MyLongClickListener listener = new MyLongClickListener();
appIcon.setOnLongClickListener(listener);

以上でドラッグできるようになっている、が、実行しても見た感じだと出来てるのか分からない。次項の「ドラッグ中に表示するイメージの制御」が必要。

ドラッグ中に表示するイメージの制御

基本的に、ドラッグ中に表示するイメージはDragShadowBuilderで制御しており、デフォルト実装(View#draw(Canvas)を呼び出す)で問題ない。

しかし、TextViewをドラッグ対象とした場合には意図した動作とならない。DragShadowBuilderクラスを継承して、TextViewのキャプチャを表示するようにカスタマイズする。

Javaコード

/**
 * ドラッグ&ドロップ中に表示するイメージを制御するクラス
 */
private static class MyDragShadowBuilder extends DragShadowBuilder
{
    /**
     * コンストラクタ
     */
    public MyDragShadowBuilder(View v) {
        super(v);
    }
    
    /**
     * ドラッグ中のイメージを描画する際にシステムが呼び出すメソッド
     */
    @Override
    public void onDrawShadow(Canvas canvas)
    {
        // ドラッグ対象View
        View view = getView();

        // Viewのキャプチャを取得する準備
        view.setDrawingCacheEnabled(true);
        view.destroyDrawingCache();

        // キャプチャを取得し、キャンバスへ描画する
        Bitmap bitmap = view.getDrawingCache();
        canvas.drawBitmap(bitmap, 0f, 0f, null);
    }
}

前述のOnLongClickListenerで実装していたイメージビルダーを作成したイメージビルダーへ置き換える。

...前略...
// これを
View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);
↓
// こうする
View.DragShadowBuilder shadow = new MyDragShadowBuilder(v);
...後略...

ドロップ先を用意する

最後に、ドロップ先としてViewGroup(LinearLayoutとか)を用意しておく。ViewGroupには、ドラッグイベントをハンドリングするためのOnDragListenerをセットすればOK。

OnDragListenerの実装は以下の通り

  • ACTION_DRAG_STARTEDを受けたらtrueを返す
  • ACTION_DROPを受けたらアプリアイコンを作ってViewGroupに追加する

Javaコード

/**
 * ドラッグイベントをハンドリングするリスナークラス
 */
private static class MyDragListener implements View.OnDragListener
{
    private LayoutInflater mInflater;

    /**
     * コンストラクタ
     */
    public MyDragListener(Context ctx)
    {
        mInflater = LayoutInflater.from(ctx);
    }

    /**
     * ドラッグイベントが発生した際に、システムから呼び出されるメソッド
     */
    @Override
    public boolean onDrag(View v, DragEvent event)
    {
        switch (event.getAction())
        {
            case DragEvent.ACTION_DRAG_STARTED:
            case DragEvent.ACTION_DRAG_ENTERED:
            case DragEvent.ACTION_DRAG_LOCATION:
            case DragEvent.ACTION_DRAG_EXITED:
                return true;

            case DragEvent.ACTION_DROP:
                //
                // ドラッグ元の情報を取得
                //
                // startDrag()の第3引数で渡したデータを取得
                TextView src = (TextView) event.getLocalState();
                // アプリ名
                CharSequence name = src.getText();
                // 画像
                Drawable[] imgs = src.getCompoundDrawables();

                //
                // 新しくドロップ先に設置するViewを生成
                //
                // アプリアイコンを新規作成(TextViewをXMLから生成)
                TextView appIcon
                    = (TextView) mInflater.inflate(R.layout.appicon, null);
                // アプリ名称を設定
                appIcon.setText(name);
                // 画像を設定
                appIcon.setCompoundDrawables(
                    imgs[0], imgs[1], imgs[2], imgs[3]);

                // ViewGroupへ追加
                ViewGroup c = (ViewGroup) v;
                c.addView(appIcon);

                return true;

            default:
                break;
        }

        return false;
    }
}

ドロップ先(ここではLinearLayout)へOnDragListenerをセットする

MyDragListener listener = new MyDragListener(this);
LinearLayout dropPlace = (LinearLayout) findViewById(R.id.drop_place);
dropPlace.setOnDragListener(mDragListener);

ドロップ先の背景

ドラッグしている際に、ドロップ先の背景が変わるようにしておく。以下のようにXMLで背景を作成し、ドロップ先に設定してあげるだけでOK

drawable/drop_place_background.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_drag_hovered="true">
        <shape>
            <solid android:color="#0affffff"></solid>
            <stroke
                android:color="#ffd700"
                android:dashgap="10dp"
                android:dashwidth="15dp"
                android:width="3dp">
        </stroke></shape>
    </item>
    
    <item>
        <shape>
            <solid android:color="#0affffff"></solid>
        </shape>
    </item>
    
</selector>

おまけ:アプリ一覧の取得

アプリ一覧を取得するコード例は以下の通り。

PackageManager manager = ctx.getPackageManager();

// アプリ一覧を取得する条件
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);

// アプリ一覧を取得
final List<ResolveInfo> apps = manager.queryIntentActivities(mainIntent, 0);
Collections.sort(apps, new ResolveInfo.DisplayNameComparator(manager));

// アプリ情報一覧を作成
for (ResolveInfo each : apps)
{
    // アプリ名を取得
    CharSequence label = each.loadLable(manager);
    // アイコンを取得
    Drawable icon = each.activityInfo.loadIcon(manager);
    
    ...略
}

4 件のコメント: