ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

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);
    
    ...略
}

2015年1月26日月曜日

Galaxy NexusにAndroid 5 Lollipopをインストールする

Android 5 Lollipopに対応したアプリケーションの開発やテストを実機で行うために、Galaxy NexusにカスタムROMを突っ込んでみた。

Android 5搭載端末と言えば、Google - Nexus 6かLG - G3かエミュレータか…どれも若干高いし、やっぱり実機が欲しい。

ちなみにGalaxy Nexusは¥15,000〜¥17,000くらいで手に入る!

やること


前提として、作業するPCにAndroid SDKをインストールしておくこと。
adbコマンドとfastbookコマンドを利用するので、$SDK_HOME/platform-toolsにパスを通しておくと便利。
  • ブートローダーのUnlock
  • TWRPの導入
  • ROOT化
  • 全パーティションのバックアップ
  • ROMのバックアップ
  • カスタムROMの導入

とりあえず


PCと端末をUSBケーブルで接続し、adbコマンドが実行出来る状態にしておく。

ブートローダーのUnlock


※この作業を行うと端末のデータが初期化されるため、必要に応じて先にバックアップを取得する必要がある。

まず、PCのターミナルで下記のコマンドを実行する。もしくは、電源ボタンとボリューム+/-ボタンの両方を押しっぱなしにして電源を入れる。
./adb reboot bootloader
上記の画面が表示されたら、ターミナルで下記のコマンドを実行し、Unlockする。
./fastboot oem unlock
確認画面が表示されるので、ボリューム+ボタンで選択し、電源ボタンで決定する。
うまくいけば下図のように、LOCK STATE - UNLOCKEDとなる。

TWRPの導入


TWRP(Team Win Recovery Project)からTWRP for Galaxy Nexus GSM [maguro]を辿って、imgファイルをダウンロードしておく。ここでは「openrecovery-twrp-2.8.4.0-maguro.img」を利用。

ブートローダーを起動した状態で、ターミナルから下記のコマンドでTWRPを起動する。
./fastboot boot ~/Downloads/openrecovery-twrp-2.8.4.0-maguro.img
起動画面はこんな感じ

ROOT化


TWRPのRebootメニューからSystemを選択すると自動的に端末が再起動して、SuperSuのインストールを促されるので指示に従えばOK。

全パーティションのバックアップ


念のため、全パーティションのバックアップ(userdata除く)を行っておく。まずは、ターミナルで下記コマンドを実行し、パーティション一覧を取得する。
./adb shell
cd /dev/block/platform/omap/omap_hsmmc.0/by-name
ls -l
こんな感じで表示される。
lrwxrwxrwx root     root              2015-01-23 03:43 boot -> /dev/block/mmcblk0p7
lrwxrwxrwx root     root              2015-01-23 03:43 cache -> /dev/block/mmcblk0p11
lrwxrwxrwx root     root              2015-01-23 03:43 dgs -> /dev/block/mmcblk0p6
lrwxrwxrwx root     root              2015-01-23 03:43 efs -> /dev/block/mmcblk0p3
lrwxrwxrwx root     root              2015-01-23 03:43 metadata -> /dev/block/mmcblk0p13
lrwxrwxrwx root     root              2015-01-23 03:43 misc -> /dev/block/mmcblk0p5
lrwxrwxrwx root     root              2015-01-23 03:43 param -> /dev/block/mmcblk0p4
lrwxrwxrwx root     root              2015-01-23 03:43 radio -> /dev/block/mmcblk0p9
lrwxrwxrwx root     root              2015-01-23 03:43 recovery -> /dev/block/mmcblk0p8
lrwxrwxrwx root     root              2015-01-23 03:43 sbl -> /dev/block/mmcblk0p2
lrwxrwxrwx root     root              2015-01-23 03:43 system -> /dev/block/mmcblk0p10
lrwxrwxrwx root     root              2015-01-23 03:43 userdata -> /dev/block/mmcblk0p12
lrwxrwxrwx root     root              2015-01-23 03:43 xloader -> /dev/block/mmcblk0p1
さらに、下記コマンドを1つずつ実行して、バックアップを行う。※mkdirで作成しているディレクトリ名を変更する場合や、上で取得したパーティション一覧に相違がある場合は、適宜コマンドを変更すること。
cd /sdcard
mkdir partbk20150123

dd if=/dev/block/mmcblk0p7 of=/sdcard/partbk20150123/mmcblk0p7_boot bs=4096
dd if=/dev/block/mmcblk0p11 of=/sdcard/partbk20150123/mmcblk0p11_cache bs=4096
dd if=/dev/block/mmcblk0p6 of=/sdcard/partbk20150123/mmcblk0p6_dgs bs=4096
dd if=/dev/block/mmcblk0p3 of=/sdcard/partbk20150123/mmcblk0p3_efs bs=4096
dd if=/dev/block/mmcblk0p13 of=/sdcard/partbk20150123/mmcblk0p13_metadata bs=4096
dd if=/dev/block/mmcblk0p5 of=/sdcard/partbk20150123/mmcblk0p5_misc bs=4096
dd if=/dev/block/mmcblk0p4 of=/sdcard/partbk20150123/mmcblk0p4_param bs=4096
dd if=/dev/block/mmcblk0p9 of=/sdcard/partbk20150123/mmcblk0p9_radio bs=4096
dd if=/dev/block/mmcblk0p8 of=/sdcard/partbk20150123/mmcblk0p8_recovery bs=4096
dd if=/dev/block/mmcblk0p2 of=/sdcard/partbk20150123/mmcblk0p2_sbl bs=4096
dd if=/dev/block/mmcblk0p10 of=/sdcard/partbk20150123/mmcblk0p10_system bs=4096
dd if=/dev/block/mmcblk0p1 of=/sdcard/partbk20150123/mmcblk0p1_xloader bs=4096
大体20分くらいはかかるので気長に作業を行う。

ROMのバックアップ


TWRPを起動してメニューのBakcupを選択する。対象項目(System、Data、Boot)をチェックして「Swipe to Back Up」をスワイプする。10分くらいでバックアップが完了し、Backup Completeが表示される。

画面に表示されたパス(/data/media/0/TWRP/BACKUPS/01498B2D17012007/2015-01-22--21-20-46 JDQ39.SC04DOMMD4/といった場所)にバックアップが作成される。

カスタムROMの導入


今回は、FML(Fork My Life)のAndroid Lollipop版を焼くので、下記2つのファイルをダウンロードしておく。

※ファイルについての詳細は、[ROM][5.0.2/LRX22G][AOSP][LINARO/OPTIMIZED] FML: Fork My Life (2015/01/08) - xdadevelopersを参照されたし

ダウンロードしたファイルは、ターミナルから下記のようにして端末へ転送しておく。
./adb shell
cd /sdcard/
mkdir tmp
exit

./adb push ~/Downloads/FML-AOSP-5.0-20150108-maguro.zip /sdcard/tmp/
./adb push ~/Downloads/pa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zip /sdcard/tmp/
次に、現在のROMを消す。TWRPを起動し、Wipe->Advanced Wipeを辿って以下を選択し「Swipe to Back Up」をスワイプする。
  • /data
  • /system
  • /cache
  • dalvik cache
最後に、TWRPのメニューからInstallを選択し、/sdcard/tmpのFML-AOSP-5.0-2015018-maguro.zipを選択する。

Installが完了したら、TWRPのメニューに戻ってInstallを選択し、/sdcard/tmpのpa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zipを選択する。

完了後にSystem Rebootする。最初の1回は、アニメーション表示のまま固まって困ったけど、電池を抜いて再度電源を入れたら問題なく起動した。

2013年9月6日金曜日

DialogFragmentでのカスタムダイアログ実装方法

DialogFragmentによるカスタムダイアログ実装方法について、下記の点をまとめておく。お題としてパズドラ風のダイアログを実装してみる。
  • 基本
  • コンテンツ部分
  • スタイル
完成形

基本

DialogFragmentを継承したpublicクラスを作成する。注意点は下記の通り。

  • ファクトリーメソッド(下記例ではnewInstance())を用意する。
  • コンストラクタのオーバーロードを作らない、使わない
public class MyDialogFragment extends DialogFragment
{
  /**
   * ファクトリーメソッド
   */
  public static MyDialogFragment newInstance(String param)
  {
    MyDialogFragment instance = new MyDialogFragment();

    // ダイアログに渡すパラメータはBundleにまとめる
    Bundle arguments = new Bundle();
    arguments.putString("parameter", param);

    instance.setArguments(arguments);
    return instance;
  }
}

コンテンツ部分(お手軽パターン)

コンテンツ部分の実装は、onCreateDialog()やonCreateView()をオーバーライドして行う。 お手軽パターンでは、onCreateDialog()のみをオーバーライドして、必要な機能を有するDialogインスタンスを生成する。

/**
 * ダイアログコンテナを生成する。
 */
@Override
public Dialog onCreateDialog(Bundle b)
{
  // ダイアログのコンテンツ部分
  LayoutInflater i
    = (LayoutInflater) getActivity()
        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  View content = i.inflate(R.layout.mydialog_content, null);

  AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  // タイトル
  builder.setTitle("My Custom Dialog");
  // コンテンツ
  builder.setView(content);
  // OK
  builder.setPositiveButton(android.R.string.ok, null);

  Dialog dialog = builder.create();

  // ダイアログ外タップで消えないように設定
  dialog.setCanceledOnTouchOutside(false);

  return dialog;
}
  • 「お手軽」ではあるが、埋め込み部品として再利用(レイアウトの一部として配置)することができないデメリットがある。
  • HoneyComb以降は、ダイアログ外タップで閉じるのがデフォルトなので注意。

コンテンツ部分(お上品パターン)

お上品パターンでは、onCreateDialog()でダイアログコンテナを生成し、onCreateView()でコンテンツを生成する。OKボタンなどはレイアウトで、ダイアログの見栄えは後述のスタイルで賄う。

/**
 * ダイアログコンテナを生成する。
 */
@Override
public Dialog onCreateDialog(Bundle b)
{
  Dialog dialog = super.onCreateDialog(b);

  // タイトル
  dialog.setTitle("My Custom Dialog");
  // ダイアログ外タップで消えないように設定
  dialog.setCanceledOnTouchOutside(false);

  return dialog;
}

/**
 * UIを生成する。
 */
@Override
public View onCreateView(LayoutInflater i, ViewGroup c, Bundle b)
{
  View content = i.inflate(R.layout.mydialog_content, null);
  return content;
}
  • onCreateDialog()をオーバーライドしつつ、onCreateView()でnull以外を返すとAndroidRuntimeExceptionが発生する…と読めるような情報もあるが、AlertDialog使用方法の問題でありDialogFragment固有の問題では無い (AlertDialogを使わなければ良い)

コンテンツのレイアウト

ここでは、前述のお上品パターンで利用するレイアウト(ボタンも含める)を想定し、下図のようなレイアウトを作成する。RelativeLayoutを利用することで、ダイアログをお好みの位置に表示する。(下記例では画面下部に配置)

このレイアウトのコードは以下の通り。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_margin="10dp"
    android:background="@drawable/dialog_bg"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingBottom="10dp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="20dp" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:background="#d595c2d7"
        android:minLines="7"
        android:text="以上の内容で送信してよろしいですか?"
        android:textColor="@android:color/black" />

    <Button
        android:id="@android:id/button1"
        android:layout_width="wrap_content"
        android:layout_height="35sp"
        android:background="@drawable/button_bg"
        android:minWidth="100dp"
        android:text="@android:string/ok"
        android:textColor="@android:color/white"
        android:textSize="22sp"
        android:textStyle="bold" />
    </LinearLayout>
</RelativeLayout>
さらに、ダイアログ背景をXMLで作成する。
drawable/dialog_bg_part1.xml(白枠だけの画像)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <stroke
    android:width="2dp"
    android:color="#ffffff" />
</shape>
drawable/dialog_bg_part2(黒枠にグラデーション塗りつぶしの画像)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <gradient
    android:angle="270"
    android:endColor="#f1114461"
    android:startColor="#f13c91ba" />
  <stroke
    android:width="1dp"
    android:color="#000000" />
</shape>
drawable/dialog_bg.xml(合体)
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:drawable="@drawable/dialog_bg_part1"/>
  <item
    android:bottom="2dp"
    android:drawable="@drawable/dialog_bg_part2"
    android:left="2dp"
    android:right="2dp"
    android:top="2dp"/>
</layer-list>
ボタンの背景画像も同様に作成する
drawable/button_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <gradient
    android:angle="270"
    android:endColor="#222656"
    android:startColor="#6a75c7" />
  <stroke
    android:width="1px"
    android:color="#0c2446" />
</shape>

スタイル

最後に、背景(前項キャプチャの黒いところ)やタイトル(前項キャプチャのvalueと書いてあるとこ)を消して、ダイアログの表示アニメーションを制御するためにスタイルを定義および適用する。

values/styles.xml
Androidのダイアログテーマを拡張し、タイトル指定・背景・アニメーションの設定を上書きする。

<!-- タイトル無し、背景透明、アニメーション指定 -->
<style name="Theme.MyDialog" parent="@android:style/Theme.Dialog">
  <item name="android:windowNoTitle">true</item>
  <item name="android:windowBackground">@android:color/transparent</item>
  <item name="android:windowAnimationStyle">@style/Animation.MyDialog</item>
</style>

ウィンドウアニメーションスタイルは下記の通り定義する。表示する時のアニメーション(windowEnterAnimation)と消える時のアニメーション(windowExitAnimation)をそれぞれ指定。

<style name="Animation.MyDialog" parent="android:Animation.Dialog">
  <item name="android:windowEnterAnimation">@anim/options_panel_enter</item>
  <item name="android:windowExitAnimation">@anim/options_panel_exit</item>
</style>

アニメーション定義

anim/options_panel_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@android:anim/decelerate_interpolator" >
  <translate
    android:duration="@android:integer/config_shortAnimTime"
    android:fromYDelta="25%"
    android:toYDelta="0" />
  <alpha
    android:duration="@android:integer/config_shortAnimTime"
    android:fromAlpha="0.0"
    android:toAlpha="1.0" />
</set>

anim/options_panel_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@android:anim/accelerate_interpolator" >
  <translate
    android:duration="@android:integer/config_shortAnimTime"
    android:fromYDelta="0"
    android:toYDelta="50%" />
  <alpha
    android:duration="@android:integer/config_shortAnimTime"
    android:fromAlpha="1.0"
    android:toAlpha="0.0" />
</set>

DialogFragmentのonCreate()をオーバーライドして、スタイルを適用するコードを記述する。

/**
 * フラグメント生成コールバックメソッド
 */
@Override
public void onCreate(Bundle b)
{
  super.onCreate(b);

  setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_MyDialog);
}

おしまい


参考サイト

2012年10月15日月曜日

AndroidでParse(Baas)を使ってPush通知する

巷で流行のBaaS(Backend as as Service)に、Parseというのがある。
色々なことができるけど、現状では「各種APIとデータベースを兼ね備えたサーバーアプリケーション」という風に認識され、使われていることが多いみたい。

今回はそんなParseの機能の一つであるNotification(Push 通知)を使って、サーバーサイドの開発無しにPush通知を実装してみる。

ちなみに100万リクエスト/月、1GBストレージまでは無料で使える。

ユーザー登録


www.parse.comへアクセスし、Try it freeボタンからサクっとユーザー登録を行う。

Quick Start

登録が済んだらログインする。自動生成とかスケルトンとかに抵抗がなければそのままQuick Start Guideへ進むと下記のページになる。



Choose your platformでAndroidを選択し、未作成であればCreate an Appあたりからアプリケーション作成を行う。
さらにBlank Android project with Parse SDK(.zip)というリンクから空のAndroidプロジェクトファイルをダウンロードする。Eclipseでダウンロードしたzipファイルを既存のプロジェクトとしてインポートする。

Quick Startの中身

自動生成とかスケルトンとかに抵抗がある場合、何を自動生成しているのか不安でいっぱいになるので、何が起きているのかを簡単にまとめておく。

  1. 空のAndroidプロジェクトを作成
  2. SDK(zipファイル)をlibsディレクトリへ設置
  3. プロジェクトのライブラリエクスポート設定に上記SDKを追加
  4. Applicationクラスを継承した新規クラスを作成し、onCreateメソッドをオーバーライド。さらにParse.initialize()メソッドの呼び出しを追加
  5. AndroidManifest.xmlのapplicationタグにandroid:name="作成したAppicationクラス名"を追記

といったことをやったのと同じ。


AndroidManifest.xmlの設定

AndroidManifest.xmlにサービスとブロードキャストレシーバの設定を追加する。

<!-- サービスを登録 -->
<service android:name="com.parse.PushService" />

<!-- ブロードキャストレシーバを登録 -->
<receiver android:name="com.parse.ParseBroadcastReceiver">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <action android:name="android.intent.action.USER_PRESENT" />
  </intent-filter>
</receiver>
パーミッションの利用設定を追加する。 ※必要最低限のみなので公式ドキュメントのチュートリアルとは少し異なる ※公式ドキュメントには書いていなかったけど、ACCESS_NETWORK_STATEも必要だった
<uses-permission
  android:name="android.permission.INTERNET" />
<uses-permission
  android:name="android.permission.ACCESS_NETWORK_STATE"/>

参考 : Push Notifications - Parse.com



Application IDとClient Keyの設定

Parseサイトにログインした状態で、アプリケーションのOverviewページを開くと、Application IDとClient Keyが表示されるのでこれをメモしておく。

ParseApplicationクラスのonCreateメソッドに記載されている、Parse.initializeの引数を上記の内容に書き換える。

Push通知を受け取るためのコードの追加

前項同様ParseApplicationクラスのonCreateメソッドに下記のコードを追加する。

PushService.subscribe(this, "", ParseStarterProjectActivity.class);

Push通知送信

Parseサイトにログインした状態で、アプリケーションのPush Notificationsページを開くと簡単なPush通知送信管理画面が開くので、適当なメッセージを記入してボタンを押すだけ。


REST APIでPush通知を行う

ParseサイトでのPush通知は、REST APIで以下のリクエストを送信しているっぽい。
送信する内容と受け側のアプリをカスタマイズすれば「Push通知をステータスバーに表示する」というデフォルト動作を変更することができる。

Header

項目値
URLhttps://api.parse.com/1/push
MethodPOST
X-Parse-Application-IdアプリケーションID
X-Parse-REST-API-KeyREST APIキー
Content-Typeapplication/json

Body

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "alert": "This is test message."
  }
}

Push通知で独自の動作を実装する



配信側

REST APIへ送信するJSONデータのalertをactionに変更する。(以下例)

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "action": "jp.blogspot.tomokey.action.SAMPLE",
    "msg": "This is test message."
  }
}

受信側

ブロードキャストレシーバを作成し、インテントフィルタのactionに上記のjp.blogspot.tomokey.action.SAMPLEを指定する。ブロードキャストレシーバの実装は、以下のようにcom.parse.DataをキーとしてJSONデータを取得すれば、あとはお好みの処理を。

public class MyReceiver extends BroadcastReceiver
{
  public void onReceive(Context ctx, Intent intent)
  {
    try
    {
      // データを取得
      Bundle extra = intent.getExtras();
      String data = extra.getString("com.parse.Data");
      
      // jsonオブジェクトへパース
      JSONObject json = new JSONObject(data);
      
      String msg = json.getString("msg");
      Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show();
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
}

念のためブロードキャストレシーバの定義も。

<receiver android:name="MyReceiver">
  <intent-filter>
    <action android:name="jp.blogspot.tomokey.action.SAMPLE"/>
  </intent-filter>
</receiver>

2011年7月5日火曜日

Honeycomb(Android 3.0)まとめ(その2)

前回に引き続き、7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。

今回はプログラミングTipsについて。

互換性

parallel activities pattern

Honeycomb(Android OS 3.0)以上であるか、タブレット端末であるか、従来のスマートフォン(Android OS 2.x)であるかによってActivityを切り替えるTips

// OSバージョンを判定する方法
boolean isHoneycomb = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;

// さらに画面サイズを判定する方法
int layoutSize
  = context.getResources().getConfiguration().screenLayout
  & Configuration.SCREENLAYOUT_SIZE_MASK;

boolean isTablet = layoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE;


Interfaceでセンサーを隠蔽

例として画面の傾きを取得するための手段を提供するinterfaceを定義し、その実装としてデバイスの搭載センサーに応じてジャイロスコープセンサーと加速度センサーを切り替えるといった方法

// 傾きを取得するための手段を提供するinterface
interface IOrientationSensorListener
{
  public String getValue();
}

ジャイロスコープセンサーを利用する実装

// ジャイロスコープセンサーを利用する実装
class Gyro implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Gyro(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_GYROSCOPE)
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}

加速度センサーを利用する実装

// 加速度センサーを利用する実装
class Accelerometer implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Accelerometer(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), // ★
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) // ★
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}
※飽くまでコンセプト。上記くらいの差異なら実装は一つにして、初期化パラメータでジャイロと加速度センサーを振り分けたほうが見通しが良さそう。

サポートしているフィーチャーの確認方法

一応。
PackageManager pm = getPackageManager();

// 加速度センサーのサポート状況
boolean supported
  = pm.hasSystemFeature(
      PackageManager.FEATURE_SENSOR_ACCELEROMETER);


画面の向きはPortraitが標準ではない

Tipsとは少し違うけど、タブレットで顕著になった画面の向きという要素に気をつける。Android主要端末の画面サイズ(small, normal, large, xlarge)でも触れたように、layoutのxmlを格納するディレクトリをport用およびland用に作成して対応すべき。



トラッキング

ユニークなインストール数の取得や、特定ユーザーの挙動をトラッキングする目的で使用できる識別子は2種類ある。また、前提として…

  • 工場出荷初期化を行っても変化しない値は使用すべきではない
  • それなりに高い精度でのトラッキングが目的であり、厳密に個人を識別する値と考えてはいけない


その1

工場出荷初期化を行うとクリアされる値であり、Root権限を取得している端末では書き換え可能な値なので個人を識別するものとして利用してはいけない。似ているけどSettings.System.ANDROID_IDではないので注意。

Settings.Secure.ANDROID_ID;


その2

数学的に重複する可能性が低いとみなすことのできるユニークな値。

UUID.randomUUID().toString();


Location Based Service

位置情報サービスを利用する際には常に電力消費の大きさが問題になる。また、位置情報を取得するまでの時間的なイニシャルコストも同様である。これらの問題を解決するためのTips

  • 前回取得した位置情報を活用する
  • 位置情報取得を1度だけ行う
  • 適切な頻度で位置情報をトレースする
  • 位置情報プロバイダの生死をハンドリングする
  • バックグラウンドで位置情報を取得しておく


前回取得した位置情報を活用する

利用可能な位置情報プロバイダが保有している位置情報を検査し、一定の条件を満たす有効なデータがあればこれを利用する。

List<String> providers = locationManager.getAllProviders();
for (String provider: providers)
{
  Location location
    = locationManager.getLastKnownLocation(provider);
  
  // locationの時刻を一定の閾値でテストして有効なものを利用する
  ...
}


位置情報取得を1度だけ行う

位置情報を追いかけ続ける必要性がなければ、可能なかぎり素早く現在の位置情報を取得するといった方法を採る。※但し、criteria(現在のデバイス状況など)に従って決定されるため、位置情報プロバイダは必ずしも素早いとは限らない。

locationManager.requestSingleUpdate(criteria, singleUpatePI);

また、上記はGingerbreadより前のAndroid OSでは利用できないため代替手段などを使う必要がある。



適切な頻度で位置情報をトレースする

例えば、頻度は毎15分、75m以上の移動があった場合など。

locationManager.requestLocationUpdates(
  AlarmManager.INTERVAL_FIFTEEN_MINUTES,
  75,
  new Criteria(),
  pendingIntent);


位置情報プロバイダの生死をハンドリングする

使用中のプロバイダが使えなくなった場合や、より良いプロバイダが利用可能になった場合についてブロードキャストレシーバを登録してハンドリングする。

// 位置情報プロバイダが利用できなくなったよーブロードキャストを
IntentFilter intentFilter
  = new IntentFilter(
      PlacesConstants.ACTIVE_LOCATION_UPDATE_PROVIDER_DISABLED);

// ハンドリングするレシーバを登録
registerReceiver(
  locProviderDisabledReceiver,
  intentFilter);


バックグラウンドで位置情報を取得しておく

UIの起動と同時に位置情報に依存した挙動を行う必要性がある場合には、サービスにおいて位置情報の取得を行っておく。但し、電力消費を考慮しFroyo(Android 2.2)以降では、Passive Providerを利用する。

locationManager.requestLocationUpdates(
  LocationManager.PASSIVE_PROVIDER,
  minTime,
  minDistabce,
  pendingIntent);

Honeycomb(Android 3.0)まとめ(その1)

7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。ちなみに、内容は前日に行われたGoogle Android Developer Lab Tokyo 2011と同様らしい。

概要

  • SDKバージョンは11
  • Fragmentsなどの機能についての互換性は静的ライブラリの提供で保つ(Android Compatibility package)
  • Honeycombで追加したUIや機能は、スマートフォン版(2.x系)へ適用していく(Icecream Sandwich)

New Features

Honeycombで追加された新要素達は、こんな感じ。

  • Hardware Acceleration
  • Drag & Drop
  • Multi choice List
  • Bluetooth APIS
  • System wide clipboard
  • RenderSript
  • Loaders
  • App Widgets

ユーザーインターフェース特性

システムバー

Honeycombにおいて、画面最下部に常時表示されているインターフェースをシステムバーと呼称する。
ハードウェア的なボタンが存在しないタブレット端末では、ソフトウェア的にこのシステムバーを描画することにより、2.xと酷似したインターフェースを提供する。

システムバーの表示制御は下記のAPIで行うことができるが、描画領域を消す手段は提供されていない。ユーザーが混乱する(ホーム画面に戻る手段がなくなる等)ため必要な場合以外は常に表示することが推奨される。

View#SetSystemUiVisibility( STATUS_BAR_VISIBLE or STATUS_BAR_HIDDEN )

システムバーの右端には通知アイコンが表示される。通知アイコンをタップすることにより表示されるインターフェースが大幅に強化されているため、画像にあるようなメディアプレイヤー機能を通知領域のみで実現するといったことが可能になっている。



アクションバー

Honeycombにおいて、画面最上部に表示されているインターフェースをアクションバーと呼称する。
Android 2.xまでは、各デベロッパーが画面上部に配置するインターフェースを独自に実装してきた。

Honeycombでは多様なニーズに答えつつ統一されたユーザー体験を提供できるようにプラットフォームで画面上部のインターフェース(=アクションバー)を提供する。



Drag & Drop

大きな画面を有するタブレット端末ならではのインターフェースとして、Drag & Dropを標準APIとしてサポートする。



設計

Fragments

機能の集合体をFragmentと呼称し、この集合体の組み合わせによって様々なデバイスへの対応が可能になる。



見た目のデザイン

RenderScript

RenderScriptを利用するとハイパフォーマンスな3D描画処理をJNI無しで実装可能。



ハードウェアアクセラレーション

LiveWallpaperなどで利用されているGPUを使用した描画高速化を、通常のGUI部品にも適用できるようになった。

android:hardwareAccelerated


アニメーション

少量のコードでViewのプロパティを用いたアニメーションを行うことができる。

ObjectAnimator.ofFloat(myView, "alpha", 0f).start();

2011年6月26日日曜日

Android主要端末の画面サイズ(small, normal, large, xlarge)

「drawable-mdpi」や「layout-normal」とか、時間が無い開発業務の中では、気にしたら負けかなと思う。


…なんてこともあるかもしれないけど、こういったところを気にしないと、アプリが安っぽく見えたり、機種によって使いやすさが違いすぎるといったことが発生する。

先に挙げたのは、普段から何気なく使っているリソースディレクトリのことであり、何となく名前から想像がつく通り、画面サイズや解像度に応じて別々のリソース(画像やら画面レイアウトやら)を格納しておくことができる。細かいことはあんざい先生の本に任せて、今回はざっくりした説明といくつかの主要端末についての表を主題とする。

ざっくり

レイアウトファイルを格納するディレクトリは、以下を使い分ければ大概OK(例:layout-normal-long-port)

  • small, normal, large, xlarge(Androidで独自に定めた基準)
  • long, no-long(横長か否か)
  • port, land(画面が縦の時、横の時)

画像ファイルを格納するディレクトリは、以下を使い分ければOK

  • ldpi, mdpi, hdpi, xhdpi

主要端末の画面サイズ表

手元に情報がある端末について、計算でそれぞれのAndroid OSにおけるサイズ情報を算出し表にしてみた。

モデルサイズ解像度
SO-01B(Xperia)normalhdpi
SO-01C(Xperia arc)normalhdpi
SO-02C(Xperia acro)normalhdpi
IS03normalxhdpi
IS04normalhdpi
SHI05(IS05)normalhdpi
SC-02B(Galaxy S)normalhdpi
SC-02C(Galaxy S II)normalhdpi
N-04C(MEDIAS)normalhdpi
N-06C(MEDIAS WP)normalhdpi
L-07C(Optimus bright)normalhdpi
X06HT(Desire)normalhdpi
HTC Legendnormalmdpi
MZ604(Xoom wifi)xlargemdpi
L-06C(Optimus Pad)xlargemdpi

但し、注意点として同じnormalであっても、微妙にサイズが異なるので注意が必要。

320x569(単位はdip)

  • SO-01B(Xperia)
  • SO-01C(Xperia arc)
  • SO-02C(Xperia acro)
  • IS04
  • SHI05(IS05)
  • N-06C(MEDIAS WP)

320x533(単位はdip)

  • SC-02B
  • SC-02C
  • X06HT
  • L-07C

320x480(単位はdip)

  • IS03
  • HTC Legend

※解像度からしてもIS03は、日本で発売されているAndroid端末の中では特殊な機体と言える。

スペック情報から該当するサイズ情報を割り出す

簡単に端末スペックから該当するサイズ情報を算出するフォームを作ってみた。

入力

Width:px
Height:px
Density:(ex 1.0~2.0)
Density Dpi:

結果

Width:dip
Height:dip
画面サイズ:
解像度:

2011年5月22日日曜日

Androidの基本 ListViewを使う

今回は、基本に戻ってListViewを使った画面の作成について書いてみる。がしかし、それだけでは面白くないので一切の画像ファイルを使わずに実現してみる。出来上がりのイメージはこんな感じ。


やること

  • 画面のレイアウト定義
  • 1行分のレイアウト定義
  • リストアダプタの作成
  • アクティビティクラスの作成

画面のレイアウト定義

画面のレイアウトはシンプルに、ListViewのみを配置した形に定義する。同じことを実現するためにListActivityというのが用意されているが、使用するメリットが薄い上にこれ系のクラス(TabActivityとか)には問題があったりもするので使わない。

<?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">
    <ListView
      android:id="@+id/listview"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
</LinearLayout>

1行分のレイアウト定義

ListViewは、各行のレイアウトを自由にカスタマイズすることができる。単純に文字だけを表示するならAndroid標準で用意されているレイアウトを使うことも可能。

今回は、右端に三角形のマークを表示したいので、下記のようにLinearLayoutを定義、子要素としてTextViewとカスタムビューを配置する。TextViewにはlayout_weight="1"のオマジナイがしてあるのでTextViewの領域は限界まで広がり、残りの部分に三角形マークが配置される。

<?xml version="1.0" encoding="UTF-8"?>  
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"  
  xmlns:tomokey="http://schemas.android.com/apk/res/com.tomokey"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:orientation="horizontal"
    android:gravity="center_vertical" >
    
    <TextView 
      android:id="@+id/row_text" 
      android:layout_height="wrap_content"
      android:layout_width="wrap_content"  
      android:layout_weight="1"
      android:textColor="@android:color/white"
      android:textSize="20dip"
      android:minHeight="50dip"
      android:gravity="center_vertical"/>
  
    <com.tomokey.custom.view.Triangle
      android:id="@+id/tri"
      android:layout_width="7dip"
      android:layout_height="15dip"
      tomokey:color="@android:color/white"
      tomokey:direction="right"
      />
    
</LinearLayout>
※画像を使わない縛りがあるために、com.tomokey.custom.view.Triangleというカスタムビューを利用している。俺は画像を使っちゃうぜ!って場合はImageViewとかにしておけばOK。

リストアダプタの作成

リストアダプタ(ListAdapter)はその名の通り、データとListViewを繋ぐためのもの。今回は配列データとListViewを繋ぐためのArrayAdapterを使って下記のようにリストアダプタを作成する。

ArrayAdapterのコンストラクタには、1行分のレイアウト定義とTextViewを指し示すIDを指定する。ArrayAdapterをそのまま利用する場合には、1行分のレイアウト定義にTextViewが含まれていなければならないし、そのTextViewにはIDが設定されていなければならない。

ListAdapter adapter
  = new ArrayAdapter<String>(
    this,
    R.layout.row,
    R.id.row_text,
    DATA)

上記のコードで利用しているDATAはこんな感じ。

private static final List<String> DATA
  = Arrays.asList(new String[]{
          "Google",
          "Apple",
          "Microsoft",
          "Research In Motion",
          "Palm"
    });

アクティビティクラスの作成

お決まりのコード達に加えて、ListViewウィジェットへリストアダプタを設定するコード(setAdapter)を追加する。

@Override
public void onCreate(Bundle savedInstanceState)
{
  super.onCreate(savedInstanceState);

  // 画面のレイアウト定義を設定
  setContentView(R.layout.main);
  
  // ListViewウィジェットを取得
  mListView = (ListView) findViewById(R.id.listview);

  // この辺りで「リストアダプタの作成」をしておく

  // リストアダプタを設定
  mListView.setAdapter(adapter);
}

三角形マークは画像で行くぜ!って場合はここまでで終わり。ここからは、三角形をプログラムのみで実現する方法。

三角形のためにやること

  • 三角形のプロパティ設計
  • カスタムビュークラスの作成

三角形のプロパティ設計

設計などと仰々しく書いてみたけど、単純に三角形を使うとき何が変えられたら便利かを考えてXMLを書くだけ。今回は以下のように定義してみた。

変えられたら便利なとこ

三角形の色 三角形の方向

XML - res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="Triangle">
    <attr name="color" format="color" />
    <attr name="direction">
      <enum name="right" value="0"/>
      <enum name="left" value="1"/>
      <enum name="top" value="2"/>
      <enum name="bottom" value="3"/>
  </attr>
  </declare-styleable>
</resources>

Triangleという名前のカスタムビューについて、colorとdirectionという名前のプロパティを定義している。また、directionについては「right」「left」「top」「bottom」のいずれかが設定できて、それぞれ数字の0~3の値を持っている。

カスタムビュークラスの作成

カスタムビューとは、Androidアプリの各ウィジェットの基礎になっているViewクラスを独自に拡張して作成した、世界でたった一つの私だけのビュー・・・だ。以下、簡潔に。

定義
package com.tomokey.custom.view;

public class Triangle extends View
定数
// 三角形の方向
public final static int DIRECTION_RIGHT = 0;
public final static int DIRECTION_LEFT = 1;
public final static int DIRECTION_TOP= 2;
public final static int DIRECTION_BOTTOM = 3;
インスタンス変数
// 描画オブジェクト
private Paint mPaint;

// 三角形の色
private int mColor;

// 三角形の方向
private int mDirection;
コンストラクタ
public Triangle(Context context, AttributeSet attrs)
{
  super(context, attrs);

  // 描画オブジェクトを初期化
  mPaint = new Paint();
  mPaint.setAntiAlias(true);
  
  // XMLで定義されたプロパティ値を取得
  TypedArray a =
    context.obtainStyledAttributes(
        attrs,
        R.styleable.Triangle);
  
  // 三角形の色を取得(デフォ:白)
  mColor 
    = a.getColor(
        R.styleable.Triangle_color,
        0xFFFFFFFF);
  
  // 三角形の方向を取得(デフォ:右)
  mDirection
    = a.getInt(
        R.styleable.Triangle_direction,
        DIRECTION_RIGHT);
}
プログラムから色を設定するメソッド
public void setColor(String color)
{
  mColor = Color.parseColor(color);
  requestLayout();
  invalidate();
}
ビューの描画メソッド
@Override
protected void onDraw(Canvas canvas)
{
  super.onDraw(canvas);
  
  // 三角形の色を設定
  mPaint.setColor(mColor);
  
  // 三角形の方向に応じてパスを生成
  Path triangle = new Path();
  switch (mDirection)
  {
  case DIRECTION_RIGHT:
    // 左上から
    triangle.moveTo(0, 0);
    // まっすぐ左下へ
    triangle.lineTo(0, getMeasuredHeight());
    // んで、右端の高さは真ん中へ
    triangle.lineTo(getMeasuredWidth(), (getMeasuredHeight() / 2));
    break;
    
  case DIRECTION_LEFT:
    triangle.moveTo(getMeasuredWidth(), 0);
    triangle.lineTo(getMeasuredWidth(), getMeasuredHeight());
    triangle.lineTo(0, (getMeasuredHeight() / 2));
    break;
    
  case DIRECTION_TOP:
    triangle.moveTo(0, getMeasuredHeight());
    triangle.lineTo(getMeasuredWidth(), getMeasuredHeight());
    triangle.lineTo((getMeasuredWidth() / 2), 0);
    break;
    
  case DIRECTION_BOTTOM:
    triangle.moveTo(0, 0);
    triangle.lineTo(getMeasuredWidth(), 0);
    triangle.lineTo((getMeasuredWidth() / 2), getMeasuredHeight());
    break;
  }
  
  // パスを塗りっと
  canvas.drawPath(triangle, mPaint);
}

2011年5月17日火曜日

AndroidでFeliCa対応端末であるかを判定する方法

アプリ開発者にとって、FeliCa対応Android端末(通称:おサイフケータイ対応Android携帯電話)であるか否かを知るのは、時と場合によっては大事なこと。

今回は、FeliCa Networksが公開しているモバイルFeliCaクライアント for Androidを使ってお手元の端末がおサイフケータイなのか否かを判別する。



必要な作業

  • AndroidManifest.xmlへの設定追加
  • FeliCaサービス(com.felicanetworks.mfc.Felica)と接続するインターフェースを実装
  • FeliCaサービスとの接続(bindService)
  • FeliCaをサポートしているかチェック(FeliCaチップの利用開始処理を試行)

AndroidManifest.xmlへの設定追加


AndroidManifestには、FeliCaチップにアクセスするためのパーミッション設定を下記のように追加する。
<uses-permission
  android:name="com.felicanetworks.mfc.permission.MFC_ACCESS"/>
さらに、FeliCaサービスを使用するため、下記のようにサービス宣言を追加する。
<service
  android:name="com.felicanetworks.mfc.Felica"
  android:exported="false"/>

サービス接続インターフェース実装


サービス接続インターフェース(ServiceConnection)には、サービスの状態をモニタリングするメソッドが定義されている。 今回は、下記2つの役割を担うクラスとして実装する。
  • FeliCaサービスとの接続が確立された時点でFeliCaチップにアクセスするためのオブジェクトを得る
  • FeliCaチップの利用開始処理を試行し、成功=FeliCa搭載端末と判定する
public class FelicaConnection implements ServiceConnection
{
  private Felica mFelica;
  
  @Override
  public void onServiceConnected(ComponentName name, IBinder service)
  {
    // FeliCaチップアクセスオブジェクト取得
    mFelica
      = ((Felica.LocalBinder)service).getInstance();
  }

  @Override
  public void onServiceDisconnected(ComponentName name)
  {
    try {
      mFelica.inactivateFelica();
    }
    catch (Exception ignore) {}
  }
  
  /**
   * FeliCaをサポートしているかどうかチェックする。
   * @return true:サポートしている
   *         false:サポートしていない
   */
  public boolean isSupported()
  {
    try
    {
      // FeliCaチップ利用開始
      mFelica.activateFelica(null, new FelicaEventAdapter());
      return true;
    }
    catch (Exception e)
    {
      // 失敗するとここに来る
      return false;
    }
    finally
    {
      try
      {
        mFelica.inactivateFelica();
      }
      catch (Exception ignore) {}
    }
  }
}
さらっと出てきたFelicaEventAdapterは、FelicaEventListenerインターフェースを空実装(処理無し)したものなのでここでは割愛する。

FeliCaサービスとの接続


FeliCaサービスとの接続は、通常のサービス接続と何も変わらず、下記のような感じに実装する。
Context ctx = getApplicationContext();

// サービスを識別するインテント
Intent intent
  = new Intent(ctx, Felica.class);

// サービス接続インターフェース
FelicaConnection conn
  = new FelicaConnection();

// 接続
boolean result = ctx.bindService(
  intent,
  conn,
  Context.BIND_AUTO_CREATE);

FeliCa搭載チェック


最後に、用意しておいたFeliCaサポートチェックメソッドを呼び出して、サービス接続を切ったら終了。 コードは上の続き。
boolean supported = false;

// 接続に成功した場合のみ
if (result)
{
  supported = conn.isSupported();
  
  // サービス切断
  getApplicationContext().unbindService(conn);
}

// FeliCa搭載してるか否か!?
return supported;

注意点
  • FeliCaのサイトからダウンロードしたMFCUtility_1.0.1.jarを静的にリンクしておく必要がある(やってないとコンパイルすら通らないので気付くと思うけど:D)
  • FeliCaチップ利用開始処理(activateFelica())を行うと、利用終了処理(inactivateFelica())を行うまでは他のアプリケーションがFeliCaチップを利用できなくなる。

2011年5月3日火曜日

Androidのソースコードレビュー(メモリリーク)

有限であるモバイル端末のメモリを無駄に消費するアプリは悪だ。そんなアプリのソースコードレビュー担当者の目は節穴だ。

と言われないためか否かは知らないけど、Android Developers - Avoiding Memory Leaksでは、Androidアプリを作成する上で、メモリリークが発生するパターンとその対処法を色々と紹介している。(英語)

要点のみを自分が見直し易いようにまとめつつ、説明の足りてない「非staticな内部クラスが駄目な件」について書いてみる。

そもそもメモリリークするん?

Java実行環境下では、ガベージコレクタによるメモリ管理が行われているため大抵の場合は意識せずともメモリをいい感じに回収してくれる。だが、ガベージコレクタの動作を妨げるようなコードを書いてしまうと、いつまで経っても解放されるべきメモリが解放されなくなる。これが今回のお題であるJavaにおけるメモリリークの定義である。




問題 1

@Override
protected void onCreate(Bundle state)
{
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}

何が問題か

labelのコンストラクタでthis(=Activityインスタンス)を渡しているため、labelがActivityの参照を保持している。このためActivityを破棄しても上手いことGCの対象となってくれない。(labelによる参照が残っている)


対処法 1

そもそも各Viewオブジェクトのコンストラクタで渡しているコンテキスト(上記ではthis(=Activityインスタンス))を、アプリケーションコンテキストに変更する。その場合、コードは下記のようになるはず。

@Override
protected void onCreate(Bundle state)
{
  super.onCreate(state);
  
  TextView label = new TextView(getApplicationContext());
  label.setText("Leaks are bad");
  
  setContentView(label);
}

対処法 2

レイアウトはXMLで定義する。その場合、コードは下記のようになるはず。

@Override
protected void onCreate(Bundle state)
{
  super.onCreate(state);
  setContentView(R.layout.main);
}



問題 2

private static Drawable sBackground;
  
@Override
protected void onCreate(Bundle state)
{
  super.onCreate(state);
  
  TextView label = new TextView(getApplicationContext());
  label.setText("Leaks are bad");
  
  if (sBackground == null)
  {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
  
  setContentView(label);
}

何が問題か

Drawableは、Viewにアタッチされた際(上記で言うところの label.setBackgroundDrawable(sBackground))に、アタッチ先Viewの参照をcallbackとして保持する。このため、アタッチ先のView(もしくはそのViewが参照している全て)は、当該Drawableインスタンスが破棄されない限りGCの対象となってくれない。
上記コードの場合、Drawableはstaticな変数であるため最悪な感じ。


対処法

Drawableインスタンスがcallbackとして保持している参照を消す。その場合、コードは下記のようになるはず。

private static Drawable sBackground;
  
@Override
protected void onCreate(Bundle state)
{
  super.onCreate(state);
  
  TextView label = new TextView(getApplicationContext());
  label.setText("Leaks are bad");
  
  if (sBackground == null)
  {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  // callback参照を削除
  sBackground.setCallback(null);
  
  setContentView(label);
}



問題 3

class MyActivity extends Activity
{
  class InnnerClass
  {
    private Activity parent;
    
    InnerClass(Activity aParent)
    {
      parent = aParent;
    }
  }
}

何が問題か

この短いコードに2つも問題点が含まれているが、いずれも親クラスへの参照を保持しているのが問題である。

  • 非staticな内部クラスである(参考:Effective Java)
  • 親クラス(MyActivity)への強参照を持っている

対処法

対処法はお決まりの方法であり「非staticな内部クラスは作らない」「ライフサイクルの異なるインスタンス参照は弱参照とする」である。

class MyActivity extends Activity
{
  // 内部クラスは原則としてstaticとすべき
  static class InnnerClass
  {
    private WeakReference<Activity> parent;
    
    InnerClass(Activity aParent)
    {
      // 弱参照とする
      parent = new WeakReference<Activity>(aParent);
    }
  }
}

参考

2011年4月28日木曜日

Androidアプリをリバースエンジニアリングする方法

結構前の記事でapkファイルを解凍して、中身のAndroidManifest.xmlをデコンパイルする方法を調べてみたが、前回の方法では何が書かれているのかを知ることはできても、元のXMLファイルを復元することはできなかった。

今回は、いくつかのツールを利用することでAndroidManifest.xmlに限らず、各種のバイナリ形式となっているXMLファイルを復元したり、dex形式になっているJavaのバイナリコードをJavaソースコードへ復元する方法をまとめる。

まずは解凍

対象のAPKファイルをzipファイルとして解凍し、下記のファイルおよびディレクトリを得る。

  • META-INF
  • res
  • AndroidManifest.xml
  • classes.dex
  • resources.arsc

バイナリXMLをテキストXMLへコンバート

AndroidManifest.xmlや、res/layout/*.xmlなどは、AXMLPrinter2.jar(Google Code : android4me)というツールを使用することで元の人間に読むことのできるテキスト形式のXMLへ変換することができる。

このツールはJavaで作成されているため、Java実行環境が必要。コマンドラインより、下記のようにして使用する。
java -jar AXMLPrinter2.jar AndroidManifest.xml
各種のリソースを表現するXMLであっても同様。
java -jar AXMLPrinter2.jar res/layout/main.xml

バイナリコード(dex)をバイナリコード(class)へコンバート

Javaのバイナリコードをソースコードへ変換するには、2つのステップが必要。1つ目はdexファイルをclassファイルへ変換(ツールの出力はjarファイル)であり、dex2jar(Google Code : dex2jar)というツールを使用。

dexからclassへの変換は、windows用およびlinux用の実行ファイル(dex2jar.bat / dex2jar.sh)が用意されているので、これを使う。出力ファイルはclasses.dex.dex2jar.jar。
dex2jar.bat classes.dex
2つ目のclassからjavaへの変換は、JD(Java Decompiler)のGUIを使うのが便利。windows版であればjd-gui.exeを起動し、メニューから上記で出力されたclasses.dex.dex2jar.jarを指定する。

すると、下図のようにJavaソースコードとしてclassファイルの中身を閲覧することができるようになる。

参考

2011年4月20日水曜日

Ubuntu 10.04とXoomを繋ぐ方法

Motorola XOOMとPC(今回はUbuntu 10.04)を繋ぐ方法をまとめてみる。XOOMはAndroid OS 3.0搭載のタブレット端末であり、次代のAndroidにおけるUIを早めに研究するのに最適と言える(気がする)。

今回は端末とPCを繋いで、以下のような画面キャプチャを取得することを目的としてみる。

基本手順

  • 51-android.rulesというファイルを作成
  • udevを再起動
  • adb daemonを再起動

51-android.rulesというファイルを作成

root権限で、/etc/udev/rules.d/51-android.rulesというファイルを作成する。内容は以下の通り。
SUBSYSTEM=="usb", SYSFS{idVendor}=="22b8", MODE="0666"
ちなみに、古いバージョンのUbuntuを利用している場合は、以下のようにする必要があるかも。
SUBSYSTEM=="usb_device", SYSFS{idVendor}=="22b8", MODE="0666"
ファイルを保存したら、ファイルの閲覧権限を設定する。
chmod a+r /etc/udev/rules.d/51-android.rules

udevを再起動

この手順は必要性ないかもしれないけど、一応やっとく。ちなみにudev(userspace device management)とは何ぞやと調べてみると・・・プラグアンドプレイを実現するための機構みたいな感じかな?
sudo restart udev

adb daemonを再起動

下のようなエラーになって困る。そんな時に必要な手順。
$sudo ./adb devices

List of devices attached
???????????? no permissions
サクっと。
sudo ./adb kill-server
sudo ./adb start-server
以上がうまく設定できていれば、こうなる
$sudo ./adb devices

List of devices attached
0288414743ffd297 device

これで、準備完了。あとはアプリ作ったりキャプチャしたりとやりたい放題。

参考

2011年3月25日金曜日

Androidアプリをインストール時に起動させる

Androidで常駐アプリケーションを作成する場合、サービス(Service)を利用する。サービスの実装方法は…他のサイトを見るとして、以下の点について実装方法を調べてみた。
  • 端末を再起動後、自動的に常駐する
  • アプリケーションが停止されても、自動的に再常駐する
  • インストール直後から自動的に常駐する

端末を再起動後、自動的に常駐する


これは、Androidシステムが発行するブロードキャスト(ACTION_BOOT_COMPLETED)を受け取った際に、サービスを起動させることで実現できる。具体的に必要となる作業は以下の3つ。
  • サービスクラスの定義
  • ブロードキャストレシーバクラスの定義
  • レシーバの定義

サービスクラスを定義
Androidに常駐して行いたい作業を記述するクラス。下記の例では具体的な処理内容は定義していない。
public class MyService extends Service
{
  @Override
  public void onCreate()
  {
    super.onCreate();
    // 初期化
  }

  @Override
  public void onStart(Intent intent, int startId)
  {
    super.onStart(intent, startId);
    // 常駐作業
  }

  @Override
  public IBinder onBind(Intent arg0)
  {
    return null;
  }
}

ブロードキャストを受け取るレシーバクラスを定義
システムの起動完了通知(ACTION_BOOT_COMPLETED)を受け取り、サービス(MyService.class)を起動する。
public class MyReceiver extends BroadcastReceiver
{
  @Override
  public void onReceive(Context context, Intent i)
  {
    if (Intent.ACTION_BOOT_COMPLETED.equals(i.getAction()))
    {
      context.startService(new Intent(context, MyService.class));
    }
  }
}

レシーバの定義
AndroidManifext.xmlの<application>タグ内に、下記のレシーバの定義を追加する。
<receiver
  android:exported="true" 
  android:name=".MyReceiver">

  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  
</receiver>
また、<manifest>タグ内に、下記のパーミッション定義を追加擦る必要もある。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

アプリケーションが停止されても、自動的に再常駐する


これは、あえて実装する必要はない。Androidリファレンスの記述では、Context.stopService()もしくはstopSelf()が呼び出されるまで、サービスは起動し続けるし、iMoniの中の人によれば色々な理由でサービスが殺されることはあるものの勝手に再起動するという仕様らしい。

サービスの実装面では考慮すべきことが沢山あるが、落ちても再起動させるという観点からすれば、特にやることはない。


インストール直後から自動的に常駐する


これも、Androidシステムが発行するブロードキャストをきっかけにしてサービス起動を行うことで実現できる。受け取るブロードキャストはINSTALL_REFERRERというもの。具体的に必要となる作業は以下の3つ。
  • サービスクラスの定義
  • ブロードキャストレシーバクラスの定義
  • レシーバの定義

サービスクラスを定義
前述と同様なので省略

ブロードキャストレシーバクラスの定義
アプリケーションのインストール完了通知を受け取り、サービス(MyService.class)を起動する。
public class MyReceiver extends BroadcastReceiver
{
  @Override
  public void onReceive(Context context, Intent i)
  {
    context.startService(new Intent(context, MyService.class));
  }
}

レシーバの定義
Androidマーケットからのインストールを検知するレシーバを定義する。
<receiver
android:exported="true" 
android:name=".MyReceiver">

  <intent-filter>
    <action android:name="com.android.vending.INSTALL_REFERRER"/>
  </intent-filter>
  
</receiver>

注意点
  • 動くかどうか試してない:D
  • Androidマーケットからインストールしないと検知できないらしい


P.S


動くかどうか試していないのは、かなりイケてない。。。近いうちに、サクっとDeveloper登録してお試ししてみよう。

2011年3月10日木曜日

Androidで格好いいプログレスダイアログを表示する

完成形


通常のプログレスダイアログ


ニュートラルな状態


Androidアプリケーションを作っていると、通信などでユーザーを待たせる場面がでてくる。こんな時にはプログレスダイアログを表示してあげると良いだろう。しかし、いつでもどこでも通常のプログレスダイアログ(上図)を表示しているとちょっとダサい。特にメッセージとして「通信中」と表示しているだけなら、もうメッセージいらないんじゃね?

若干無理がある導入はここまでにして、、、以下が今回必要な定義。
  • レイアウト
  • テーマ
  • ダイアログクラス

レイアウトの定義


構成はこんな感じ
  • FrameLayout
    • ProgressBar

res/layout/custom_progress_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content">
  
  <ProgressBar
    android:id="@android:id/progress"
    style="@android:style/Widget.ProgressBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:max="10000"
    android:layout_marginRight="12dip" />

</FrameLayout>


テーマの定義


タイトルを削除し、背景を透明に設定したダイアログテーマを定義する

res/values/themes_custom_progress_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style
    name="Theme.CustomProgressDialog"
    parent="android:style/Theme.Dialog">
    
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
  
  </style>
</resources>


ダイアログクラスの定義


定義したテーマとレイアウトでダイアログを初期化するクラスを定義
public class CustomProgressDialog extends Dialog
{
  /**
   * コンストラクタ
   * @param context
   */
  public CustomProgressDialog(Context context)
  {
    super(context, R.style.Theme_CustomProgressDialog);

    // レイアウトを決定
    setContentView(R.layout.custom_progress_dialog);
  }
}


使い方


使い方は簡単

但し、実際に利用する場合は、dialog.setCancelable(true)とか、AsyncTaskなどを使って処理完了後にdialog.dismiss()とかが必要になるはず。
CustomProgressDialog dialog = new CustomProgressDialog(MyActivicty.this);
dialog.show();

2011年3月4日金曜日

Androidで格好いいダイアログを表示する

完成形


このダイアログは一切の画像を使わずに実現可能なところが格好いい!必要なのは以下の定義。
  • レイアウト
  • 色
  • ボタンの背景
  • テーマ
  • ダイアログクラス

ダイアログのレイアウト

構成はこんな感じ
  • LinearLayout
    • TextView ( @+id/custom_dialog_title )
    • ScrollView
      • TextView ( @+id/custom_dialog_text )
    • Button ( @+id/custom_dialog_ok )

res/layout/custom_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="250dip" 
  android:layout_height="wrap_content" 
  android:layout_margin="50dip"
  android:orientation="vertical"
  android:gravity="center_horizontal"
  android:background="@color/custom_dialog_background">
  
  <TextView
    android:id="@+id/custom_dialog_title"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:paddingTop="10dip"
    android:paddingBottom="10dip"
    android:paddingLeft="5dip"
    android:paddingRight="5dip"
    android:gravity="center"
    android:background="@color/custom_dialog_title_background"
    android:textColor="@color/custom_dialog_title_foreground"/>
  
  <ScrollView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_weight="1">
    
    <TextView
      android:id="@+id/custom_dialog_text"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="5dip"
      android:textColor="@color/custom_dialog_text_foreground"
      android:background="@color/custom_dialog_text_background"/>
    
  </ScrollView>
  
  <Button
    android:id="@+id/custom_dialog_ok"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="5dip"
    android:minWidth="80dip"
    android:paddingTop="10dip"
    android:paddingBottom="10dip"
    android:textColor="@color/custom_dialog_button_foreground"
    android:background="@drawable/custom_dialog_button_background"
    android:text="OK"/>
  
</LinearLayout>

色の定義


こんな感じに定義して、変更しやすくしておく
名前ID色
背景custom_dialog_background薄い茶色
タイトル部背景custom_dialog_title_background濃い茶色
タイトル部前景custom_dialog_title_foreground白
ボタン前景custom_dialog_button_foreground白
テキスト部背景custom_dialog_text_background白
テキスト部前景custom_dialog_text_foreground黒

res/values/colors_custom_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="custom_dialog_background">#e4dccc</color>
  <color name="custom_dialog_title_background">#34211d</color>
  <color name="custom_dialog_title_foreground">#ffffff</color>
  <color name="custom_dialog_button_foreground">#ffffff</color>
  <color name="custom_dialog_text_background">#ffffff</color>
  <color name="custom_dialog_text_foreground">#000000</color>
</resources>

ボタンの背景


画像を使わずに格好いい背景を定義する(概要は下記の通り)
selector
 + item ( 押下時の定義:緑色の角丸四角を定義 )
    + layer-list
       + item
          + shape ( 四角形を定義 )
             + gradient ( 縦にグラデーションで色を塗る )
             + corners ( 角丸 )
             + stroke ( 縁取り )
 
 + item ( 通常時の定義:茶色の角丸四角を定義 )
    + layer-list
       + item
          + shape ( 四角形を定義 )
             + gradient ( 縦にグラデーションで色を塗る )
             + corners ( 角丸 )
             + stroke ( 縁取り )

res/drawable/custom_dialog_button_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector
  xmlns:android="http://schemas.android.com/apk/res/android">

<item android:state_pressed="true">
<layer-list>

  <item>
    <shape 
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:shape="rectangle">
        
      <gradient
        android:angle="270"
        android:startColor="#b7ed36" 
        android:endColor="#649016"
        android:type="linear" />
      <corners 
        android:radius="5dip" />
        <stroke 
          android:width="2dip" 
          android:color="#649016"/>

    </shape>
  </item>

</layer-list>
</item>

<item android:state_checked="false" android:state_pressed="false">
<layer-list>

  <item>
    <shape 
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:shape="rectangle">
        
      <gradient 
        android:angle="270"
        android:startColor="#978d8b" 
        android:endColor="#382622"
        android:type="linear" />
        <corners 
          android:radius="5dip" />
        <stroke 
          android:width="2dip" 
          android:color="#34211d"/>
        
    </shape>
  </item>

</layer-list>
</item>

</selector>

テーマの定義


タイトル定義を削除したダイアログテーマを定義する

res/values/themes_custom_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style
    name="Theme.CustomDialog"
    parent="android:style/Theme.Dialog">
        <item
          name="android:windowNoTitle">true</item>
  </style>
</resources>

ダイアログクラスの定義


定義したテーマとレイアウトでダイアログを初期化するクラスを定義
public class CustomDialog extends Dialog
{
  /** タイトルビュー */
  private TextView iTitle;
  /** テキストビュー */
  private TextView iText;
  /** ボタンビュー */
  private Button iButton;
  
  /**
   * コンストラクタ
   * @param context
   */
  public CustomDialog(Context context, String title, String text)
  {
    super(context, R.style.Theme_CustomDialog);
    
    // レイアウトを決定
    setContentView(R.layout.custom_dialog);
    
    // タイトルビューを取得
    iTitle = (TextView)findViewById(R.id.custom_dialog_title);
    // テキストビューを取得
    iText = (TextView)findViewById(R.id.custom_dialog_text);
    // OKボタンビューを取得
    iButton = (Button)findViewById(R.id.custom_dialog_ok);
    
    // タイトルを設定
    iTitle.setText(title);
    // テキストを設定
    iText.setText(text);
    
    // ボタンビューにOnClickListenerを登録
    iButton.setOnClickListener(new View.OnClickListener()
    {
      public void onClick(View v)
      {
        dismiss();
      }
    });
  }
}

使い方


使い方は簡単
new CustomDialog(MyActivicty.this, "タイトル", "本文").show();

2011年3月2日水曜日

Androidでメールを送信する方法

Androidアプリケーションにおいて、メール送信機能を実現するもっともスタンダードな方法はIntentを発行して、メール送信アプリケーションにその処理を移譲してしまうという方法だ。

お作法に従った場合


メールを送信するには、下記のようにIntentを発行するのが正しい。(と思われる)
// Intentインスタンスを生成
Intent intent = new Intent();

// アクションを指定(ACTION_SENDTOではないところがミソ)
intent.setAction(Intent.ACTION_SEND);
// データタイプを指定
intent.setType("message/rfc822");

// 宛先を指定
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"foo@example.com"});
// CCを指定
intent.putExtra(Intent.EXTRA_CC, new String[]{"cc@example.com"});
// BCCを指定
intent.putExtra(Intent.EXTRA_BCC, new String[]{"bcc@example.com"});
// 件名を指定
intent.putExtra(Intent.EXTRA_SUBJECT, "件名");
// 本文を指定
intent.putExtra(Intent.EXTRA_TEXT, "本文の内容");

// Intentを発行
startActivity(intent);
この方法で正常な動作を確認できたのは、下記のメールアプリ
  • GMail
  • K-9Mail
  • AUの標準メーラー(IS03で確認)

逆に下記のメールアプリでは動作しない(Intentに反応しない)
  • Softbankメールアプリ(003SH, 005SHはアプリが微妙に違うので不明)
  • HTC標準メーラー(Desire X06HTで確認)
  • spモードメール(T01-Cで確認)

HTMLでのAタグにmailto:を指定した場合(と同じ動作)


HTMLの場合
メール送信

Javaの場合
// Intentインスタンスを生成
Intent intent = new Intent();

// アクションを指定(ACTION_SENDTOではないところがミソ)
intent.setAction(Intent.ACTION_SENDTO);
// データを指定
intent.setData(Uri.parse("mailto:foo@example.com?subject=件名&body=本文&cc=cc@example.com&bcc=bcc@example.com"));

// Intentを発行
startActivity(intent);
この方法で正常な動作を確認できたのは、下記のメールアプリ
  • GMail
  • K-9Mail
  • AUの標準メーラー(IS03で確認)

下記のメールアプリでは動作しない(CC,BCCが正しく設定されない)
  • HTC標準メーラー(Desire X06HTで確認)

下記のメールアプリでは動作しない(起動はするもののまともではない)
  • Softbankメールアプリ(003SH, 005SHはアプリが微妙に違うので不明)
  • spモードメール(T01-Cで確認)

ACTION_SENDTOとEXTRA_を指定

Intent intent = new Intent();

// アクションを指定
intent.setAction(Intent.ACTION_SENDTO);
// データを指定
intent.setData(Uri.parse("mailto:foo@example.com"));

// CCを指定
intent.putExtra(Intent.EXTRA_CC, new String[]{"cc@example.com"});
// BCCを指定
intent.putExtra(Intent.EXTRA_BCC, new String[]{"bcc@example.com"});
// 件名を指定
intent.putExtra(Intent.EXTRA_SUBJECT, "件名");
// 本文を指定
intent.putExtra(Intent.EXTRA_TEXT, "本文の内容");

// Intentを発行
startActivity(intent);
この方法で正常な動作を確認できたのは、下記のメールアプリ
  • GMail
  • AUの標準メーラー(IS03で確認)
  • spモードメール(T01-Cで確認)

下記のメールアプリでは動作しない(CC,BCCが正しく設定されない)
  • HTC標準メーラー(Desire X06HTで確認)

下記のメールアプリでは動作しない(CC,BCC,件名,本文が正しく設定されない)
  • K-9Mail
  • Softbankメールアプリ(003SH, 005SHはアプリが微妙に違うので不明)

ACTION_SENDTOとEXTRA_の関係


件名、本文が指定できないという現象について、K-9mailでは仕様となっている。ソースコードのコメントには、下記のように記載されている。
Android Open Source Projectのドキュメントに、ACTION_VIEWとACTION_SENDTOでは「EXTRA_」という接頭辞の付いたパラメータを受け取らないように規定されているので無視する。
開発者の皆さんもこれに従うべきです!もしくはAndroid Open Source Projectのドキュメントを変更しましょ。

その他


やっぱり実際問題としては、全てのケースで動作するGMailが最優秀。他にもACTION_SENDとtext/plainの組み合わせでSMSが起動したりと色々あるみたい。現状では挙動が違い過ぎて全てのメーラーで共通に動作するのは宛先と件名を指定したアプリ起動くらいかもしれない。

頑張れキャリアの中の人。

2011年2月4日金曜日

GoogleのBloggerアプリを使ってみる

GoogleがBloggerアプリをリリースしたらしいので、この機会にAndroidから投稿できるアプリを導入してみようと思う。

候補は2つあるので、両方ためしてみた。

Blogger (by Google.Inc)
うん、微妙。

素のHTMLを書くことは少ないかもしれないけど、HTML編集モード的なのはあってもいいんじゃないかと思う。
とりあえず現状では、ただの文字列を投稿するために利用って感じ。

Blogger Droid (by ANDERS HEDSTRÖM)
デザインが無機的な感じだけど、割といい感じ。
  • ローカル画像、YouTubeのデータ、Picasaのデータを投稿できるっぽい
  • HTML編集ができる
  • 故に、画像の投稿位置も制御可能
  • Draft保存ができる
  • 画像はクオリティやサイズを変更できる
  • 画像やYoutubeの投稿テンプレートを作成可能
  • 投稿履歴閲覧(アプリを使って投稿したもののみ?)
▼GoogleのBloggerアプリでの書き込みはここから▼
リストをHtmlで書いてみる。
<ul>
<li>タグを書くのは面倒だな
<li>そもそもちゃんとアプロードできるのか?
</ul>

コードを書いてみる。
<pre name=“code“ class=“java“>
public static void main(String[] args)
{
  System.out.println(“Hello World“);
}
</pre>

画像は、どんな感じになるのかな。

あとは、タグと位置情報もつけてみる!
 
▲GoogleのBloggerアプリでの書き込みはここまで▲

▼ANDERSのBloggerアプリでの書き込みはここから▼

かなり自由に書き込める。
public static void main(String[] args)
{
  System.out.println(“Hello World“);
}

画像は、こんな感じになるのかな。

タグは付けられるけど、位置情報は無理かな。いらない気もするけど!
Published with Blogger-droid v1.6.7

▲ANDERSのBloggerアプリでの書き込みはここまで▲

2011年1月4日火曜日

ScrollViewを格好よくスクロールする

iPhoneとAndroidを並べて使っていると、スクロールする画面で感じるストレスに差を感じることがある。

2つを比べてみると、iPhoneでは最上部までスクロールしてもまだスクロールすることができるのに対し、Androidではコンテンツの最上部までスクロールしてしまうと、それ以上スクロールを試みてもうんともすんとも言わないといった違いがあり、これがストレスの原因の一つと思われる。

そこで、Androidでもびろーんってのを実装してみようと考えたわけだが、Androidでは簡単に実現する方法が見当たらない。※Android 2.3 (Gingerbread)ではOverscrollを使う簡単な方法があるらしい。

なので今回は、OverscrollをAndroid 2.2以下でも簡単に実装することができる方法を考えてみた。そんでもってついでに公開してみた。Google Codeにサンプルプロジェクトごと公開してみたので、興味があればご参照あれ。

まず、画面を構成する要素はScrollViewとその子要素(今回はLinearLayoutとした)である。ScrollViewは子要素を一つしか設定できない。子要素の中身には適当なViewを配置してみる。
<ScrollView
    android:id="@+id/scroll_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
  
    <LinearLayout
      android:id="@+id/content"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:orientation="vertical"
      android:paddingTop="200dip"
      android:paddingBottom="200dip">
      
      <TextView
        android:layout_width="fill_parent"
        android:layout_height="300dip"
        android:text="First Content."
        android:textSize="50dip"/>
      
      <TextView
        android:layout_width="fill_parent"
        android:layout_height="300dip"
        android:text="Second Content."
        android:textSize="50dip"/>
      
      <TextView
        android:layout_width="fill_parent"
        android:layout_height="300dip"
        android:text="Third Content."
        android:textSize="50dip"/>

    </LinearLayout>

</ScrollView>

ScrollViewの子要素であるLinearLayoutには、OverScrollするための余白としてpaddingTopおよびpaddingBottomに200dipを設定しておく。

次にTouchEventListenerを作成し、ScrollViewへ登録する。
scrollView.setOnTouchListener(new MyOnTouchListener(scrollView, contentView));

TouchEventListenerではACTION_UPをフックし、ScrollViewのgetScrollY()メソッドで現在のスクロール位置を取得する。現在のスクロール位置がpadding領域であった場合は、画面の端にpadding領域を除いたContent部分がぴったり表示されるようにスクロールする。といった感じ。

多分、コードのほうがわかり易い。
@Override
public boolean onTouch(View v, MotionEvent event)
{
    scroller.forceFinished(true);
    mScrollView.removeCallbacks(task);
    
    if (event.getAction() == MotionEvent.ACTION_UP)
    {
        int displayHeight = mScrollView.getHeight();
        int contentTop = mContentView.getPaddingTop();
        int contentBottom = mContentView.getHeight() - mContentView.getPaddingBottom();
        int lastPageTop = contentBottom - displayHeight;
        
        int currScrollY = mScrollView.getScrollY();
        
        scroller.startScroll(0, currScrollY, 0, lastPageTop - currScrollY, 500);
        
        mScrollView.post(task);
        
        return true;
    }
    
    return false;
}

ちなみに、scrollerはアニメーション付きのスクロール量計算をカプセル化したクラスで、下記のようなコードでインスタンスを生成しておく。
scroller = new Scroller(mScrollView.getContext(), new OvershootInterpolator());

また、taskの実体はRunnableであり、scrollerの計算結果取得とScrollView#scrollTo()メソッドの呼び出しを行っている。
task = new Runnable()
{
    @Override
    public void run()
    {
        scroller.computeScrollOffset();
        mScrollView.scrollTo(0, scroller.getCurrY());
        
        if (!scroller.isFinished())
        {
            mScrollView.post(this);
        }
    }
};

結果はこれ。

画像だと伝わりにくいが、タッチして下へぐぐっと引っ張ることができる。

指をはなすとびろーんアニメーション(Overshoot)でもとに戻る。

2010年12月16日木曜日

AndroidのAPKのManifest.xmlの中身の秘密の知りたいの

Androidのアプリケーションはapkという拡張子がついたzipファイルとして配布される。
zipファイルなので当然、解凍したくなる。

欲望のままに解凍するとDalvik VM用にコンパイルされたdexファイルや各種のリソースファイルが展開される。

展開されたファイルは、Android SDKに含まれる各種のツールでデコンパイルすることができるらしい。

  • dexdump
  • aidl
  • aapt
上記のツールは、%Android SDK Home%\platforms\android-4\toolsとかその辺りに入ってる。
今回は、aaptというツールを使って、AndroidManifest.xmlファイルをデコンパイルしてみる。

まず、そもそもapkを解凍して得られるAndroidManifest.xmlファイルはバイナリファイルなので、テキストエディタで開いても意味不明な感じになる。

そこで、aaptを使ってxmlファイル・・・ではなく、apkファイルの中身をサクっと見てみると、AndroidManifest.xmlの内容を見ることができてしまう。
そのコマンドがこれだ・・・

aapt l -a アプリケーション.apk

ずらずらーっと(中略)

Android manifest:
N: android=http://schemas.android.com/apk/res/android
  E: manifest (line=2)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
    A: package="com.example.listview.tap" (Raw: "com.example.listview.tap")
    E: application (line=6)
      A: android:label(0x01010001)=@0x7f040001
      A: android:icon(0x01010002)=@0x7f020000
      E: activity (line=7)
        A: android:label(0x01010001)=@0x7f040001
        A: android:name(0x01010003)=".Main" (Raw: ".Main")
        E: intent-filter (line=9)
          E: action (line=10)
            A: android:name(0x01010003)
                ="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=11)
            A: android:name(0x01010003)
               ="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")

(※便宜上、一部改行してます)

EはおそらくElement、AはAttribute、NはNamespaceの略かと思われる。