2015年2月19日木曜日

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

目指すもの

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

動画をとってみた


アプリの表示

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

layout/appicon.xml

  1.       
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="wrap_content"  
  4.     android:layout_height="wrap_content"  
  5.     android:paddingTop="4dp"  
  6.     android:textSize="12dp"  
  7.     android:singleLine="true"  
  8.     android:ellipsize="end"  
  9.     android:textColor="@android:color/white"  
  10.     android:gravity="center_horizontal|center_vertical" />  

Javaコード

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

  1. // アイコン画像をロード(とりあえず電話っぽいアイコンにしとく)  
  2. Drawable icon = getResources().getDrawable(android.R.drawable.ic_menu_call);  
  3.   
  4. // TextViewをXMLから生成  
  5. TextView appIcon  
  6.     = (TextView) getLayoutInflater().inflate(R.layout.appicon, null);  
  7. // アプリ名称を設定  
  8. appIcon.setText("でんわ");  
  9. // アイコンを設定(left, top, right, bottomの順で指定)  
  10. appIcon.setCompoundDrawables(null, icon, nullnull);  

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

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

Viewをドラッグ可能にする

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

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

  1. /** 
  2.  * Viewの長押しイベントをハンドリングするリスナークラス 
  3.  */  
  4. private static class MyLongClickListener implements View.OnLongClickListener  
  5. {  
  6.     /** 
  7.      * Viewが長押しされた際にシステムから呼び出される。 
  8.      */  
  9.     @Override  
  10.     public boolean onLongClick(View v)  
  11.     {  
  12.         // ドラッグ&ドロップで受け渡しするデータ(使わないのでダミー)  
  13.         ClipData tmpData = ClipData.newPlainText("dummy""dummy");  
  14.         // ドラッグ中に表示するイメージのビルダー  
  15.         View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);  
  16.   
  17.         // ドラッグを開始  
  18.         v.startDrag(tmpData, shadow, v, 0);  
  19.           
  20.         return true;  
  21.     }  
  22. }  

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

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

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

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

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

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

Javaコード

  1. /** 
  2.  * ドラッグ&ドロップ中に表示するイメージを制御するクラス 
  3.  */  
  4. private static class MyDragShadowBuilder extends DragShadowBuilder  
  5. {  
  6.     /** 
  7.      * コンストラクタ 
  8.      */  
  9.     public MyDragShadowBuilder(View v) {  
  10.         super(v);  
  11.     }  
  12.       
  13.     /** 
  14.      * ドラッグ中のイメージを描画する際にシステムが呼び出すメソッド 
  15.      */  
  16.     @Override  
  17.     public void onDrawShadow(Canvas canvas)  
  18.     {  
  19.         // ドラッグ対象View  
  20.         View view = getView();  
  21.   
  22.         // Viewのキャプチャを取得する準備  
  23.         view.setDrawingCacheEnabled(true);  
  24.         view.destroyDrawingCache();  
  25.   
  26.         // キャプチャを取得し、キャンバスへ描画する  
  27.         Bitmap bitmap = view.getDrawingCache();  
  28.         canvas.drawBitmap(bitmap, 0f, 0f, null);  
  29.     }  
  30. }  

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

  1. ...前略...  
  2. // これを  
  3. View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);  
  4. ↓  
  5. // こうする  
  6. View.DragShadowBuilder shadow = new MyDragShadowBuilder(v);  
  7. ...後略...  

ドロップ先を用意する

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

OnDragListenerの実装は以下の通り

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

Javaコード

  1. /** 
  2.  * ドラッグイベントをハンドリングするリスナークラス 
  3.  */  
  4. private static class MyDragListener implements View.OnDragListener  
  5. {  
  6.     private LayoutInflater mInflater;  
  7.   
  8.     /** 
  9.      * コンストラクタ 
  10.      */  
  11.     public MyDragListener(Context ctx)  
  12.     {  
  13.         mInflater = LayoutInflater.from(ctx);  
  14.     }  
  15.   
  16.     /** 
  17.      * ドラッグイベントが発生した際に、システムから呼び出されるメソッド 
  18.      */  
  19.     @Override  
  20.     public boolean onDrag(View v, DragEvent event)  
  21.     {  
  22.         switch (event.getAction())  
  23.         {  
  24.             case DragEvent.ACTION_DRAG_STARTED:  
  25.             case DragEvent.ACTION_DRAG_ENTERED:  
  26.             case DragEvent.ACTION_DRAG_LOCATION:  
  27.             case DragEvent.ACTION_DRAG_EXITED:  
  28.                 return true;  
  29.   
  30.             case DragEvent.ACTION_DROP:  
  31.                 //  
  32.                 // ドラッグ元の情報を取得  
  33.                 //  
  34.                 // startDrag()の第3引数で渡したデータを取得  
  35.                 TextView src = (TextView) event.getLocalState();  
  36.                 // アプリ名  
  37.                 CharSequence name = src.getText();  
  38.                 // 画像  
  39.                 Drawable[] imgs = src.getCompoundDrawables();  
  40.   
  41.                 //  
  42.                 // 新しくドロップ先に設置するViewを生成  
  43.                 //  
  44.                 // アプリアイコンを新規作成(TextViewをXMLから生成)  
  45.                 TextView appIcon  
  46.                     = (TextView) mInflater.inflate(R.layout.appicon, null);  
  47.                 // アプリ名称を設定  
  48.                 appIcon.setText(name);  
  49.                 // 画像を設定  
  50.                 appIcon.setCompoundDrawables(  
  51.                     imgs[0], imgs[1], imgs[2], imgs[3]);  
  52.   
  53.                 // ViewGroupへ追加  
  54.                 ViewGroup c = (ViewGroup) v;  
  55.                 c.addView(appIcon);  
  56.   
  57.                 return true;  
  58.   
  59.             default:  
  60.                 break;  
  61.         }  
  62.   
  63.         return false;  
  64.     }  
  65. }  

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

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

ドロップ先の背景

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

drawable/drop_place_background.xml

  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   
  3.     <item android:state_drag_hovered="true">  
  4.         <shape>  
  5.             <solid android:color="#0affffff"></solid>  
  6.             <stroke  
  7.                 android:color="#ffd700"  
  8.                 android:dashgap="10dp"  
  9.                 android:dashwidth="15dp"  
  10.                 android:width="3dp">  
  11.         </stroke></shape>  
  12.     </item>  
  13.       
  14.     <item>  
  15.         <shape>  
  16.             <solid android:color="#0affffff"></solid>  
  17.         </shape>  
  18.     </item>  
  19.       
  20. </selector>  

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

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

  1. PackageManager manager = ctx.getPackageManager();  
  2.   
  3. // アプリ一覧を取得する条件  
  4. Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);  
  5. mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);  
  6.   
  7. // アプリ一覧を取得  
  8. final List<ResolveInfo> apps = manager.queryIntentActivities(mainIntent, 0);  
  9. Collections.sort(apps, new ResolveInfo.DisplayNameComparator(manager));  
  10.   
  11. // アプリ情報一覧を作成  
  12. for (ResolveInfo each : apps)  
  13. {  
  14.     // アプリ名を取得  
  15.     CharSequence label = each.loadLable(manager);  
  16.     // アイコンを取得  
  17.     Drawable icon = each.activityInfo.loadIcon(manager);  
  18.       
  19.     ...略  
  20. }  

0 件のコメント:

コメントを投稿