2013/05/31

[書評]モバイルデザインパターン

モバイルデザインパターン― ユーザーインタフェースのためのパターン集
Theresa Neil
オライリージャパン
発売日:2012-09-24
ブクログでレビューを見る»
モバイルアプリデザインのベストプラクティスのカタログ本です。
実際のアプリのスクリーンショットを用いて
それぞれのデザインパターンについて説明しているので用途がイメージしやすいです。
アンチパターンについても書かれているので
開発したアプリがアンチパターンに陥っていないか確認するのに良いと思います。
基本的には良い本なんだけど私が嫌いなデザインの本にありがちな何点かの問題があるため☆5とは行かず。
  1. 説明とスクリーンショットがページをまたぐ。
  2. 説明の文末に対象の図番号を記述するので読み終わるまでどのSSについての説明なのかわからない。
  3. 実際の UI なのでその他のコンポーネントが多くてどの部分について説明しているのかわかりにくい。
  4. 実例を見ながらの説明なので説明文がなんとなくまとまっていない。
上記の4点のせいで何回も説明とスクリーンショットを見比べなければならないので頭に入って来にくかったけど
付録でデザインパターンと簡潔な説明がわかりやすく載っていたので良しとする。

デザインパターンの種類とスクリーンショットは以下のページにあります。
Mobile Design Pattern Gallery: UI Patterns for iOS, Android and More

他にもモバイルアプリデザインパターンを集めたサイトがたくさんあります。
Pttrns — Mobile User Interface Patterns
Mobile Patterns
MOBILE PATTERNS
Patterns of Designdiscovering the best in iOS app design
TapFancy – An iPhone app design showcase and gallery
TappGala: The Best in Mobile Interface Design
Android App Patterns
Tumblrにスクリーンショットがたくさんアップされているので参考フォローしておくのもいいかもしれません。
lovely ui
Well-Placed Pixels
Android niceties
以下のサイトは Android アプリの挙動のパターンを解説していて Android を使わない人に便利かな。
Android Interaction Design Patterns |

デザインパターンも大事だけどまずはデザインガイドラインを知ることから始めないといけませんね。
Design | Android Developers
Icon Design Guidelines | Android Developers
iOSヒューマンインターフェイスガイドライン(pdf)
iOS Human Interface Guidelines: Introduction
2013/05/26

[Android]Activityの画面遷移のアニメーションを無効化する

[Android]ActionBarSherlockの背景色をカスタマイズする | DevAchieve
[Android]ActionBarSherlockでActionBarを透過オーバーレイさせる | DevAchieve
書いたように Activity のアニメーションがあると透過 ActionBar で
起動時に一瞬灰色の表示になった後に透過されるので色々やっていたのだけど
どうも起動時のアニメーションが怪しい気がしたので
アニメーションを無効化してみたら灰色の表示が消えました。
というわけでActivityの画面遷移のアニメーションを無効化する方法について書きます。
と言ってもthrow Life - ActivityのOpenとCloseをアニメーションさせるを参考に
各アニメーションに @null を指定するだけ。
<style name="noActivityAnimation" parent="android:Animation.Activity">
    <!-- 呼び出される activity の Enter アニメーション -->
    <item name="android:activityOpenEnterAnimation">@null</item>
    <!-- 他の activity を呼び出す activity の Exit アニメーション -->
    <item name="android:activityOpenExitAnimation">@null</item>
    <!-- 他の activity を閉じる際に表示される activity の Enter アニメーション -->
    <item name="android:activityCloseEnterAnimation">@null</item>
    <!-- activity を閉じる際の Exit アニメーション -->
    <item name="android:activityCloseExitAnimation">@null</item>
</style>

あとは適用したい Activity のテーマで以下のように記述します。
<item name="android:windowAnimationStyle">@style/noActivityAnimation</item>

[Android]ActionBarSherlockでActionBarを透過オーバーレイさせる

[Android]ActionBarSherlockの背景色をカスタマイズする | DevAchieve
背景色をカスタマイズできるようになったので透過色を設定して
オーバーレイさせたいと思います。
まずオーバーレイはスタイルで android:windowActionBarOverlay
true を設定します。
ActionBarSherlock を利用しているので windowActionBarOverlay を true にします。
android:windowActionBarOverlay も一緒に設定しておいたほうが良いかもしれません。
<style name="Theme.ActionBar.Overlay" parent="@style/Theme.Sherlock.Light.DarkActionBar">
    <item name="actionBarStyle">@style/ActionBarCustomStyle</item>
    <item name="windowActionBarOverlay">true</item>
</style>
<style name="Theme.ActionBar.Overlay" parent="@android:style/Theme.Holo">
    <item name="android:actionBarStyle">@style/ActionBarCustomStyle</item>
    <item name="android:windowActionBarOverlay">true</item>
</style>
次はコードから ActionBar の背景色を変更します。
mActionBar = getSupportActionBar();
mActionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.actionBarTranslucent)));
ColorDrawable が int を受け取るのでリソースIDかと思ったら 0xAARRGGBB でした。
Activity のアニメーション次第では透過色の setBackgroundDrawable で一瞬灰色の表示が出てしまいました。
Activity のアニメーションを無効化しておくのが良いかもしれません。
2013/05/25

[Android]ActionBarSherlockの背景色をカスタマイズする

カメラアプリを作っているので QuickPic のようにActionBar を透過させて
オーバーレイさせて表示したいなぁと思ったのだけど
意外と ActionBarSherlockの背景色をカスタマイズするのが大変だったのでメモ。

ActionBar の背景色変更方法を適当にググると
以下のようにスタイルを変更する方法が見つかったりする。
Style.xmlを使用してThemeをカスタマイズする | Tech Booster
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="my_theme" parent="@android:style/Theme.Holo.Light">
        <item name="android:actionBarStyle">@style/my_actionbar_style</item>
    </style>
    <style name="my_actionbar_style" parent="@android:style/Widget.Holo.Light.ActionBar">
        <item name="android:background">#000000</item>
    </style>
</resources>
簡単だなぁと思って適用してみる…がっ…駄目っ…!
透過色を指定したら画面の高さいっぱいに ActionBar が表示されるハチャメチャな表示になってしまった。
色々調べて試してダメでというのを繰り返すこと2日くらい…。

神が現れた!
ニクログ: ActionBarSherlockでActionBar対応したメモ
以下のように書けば @color/actionBar の色が適用された ActionBarSherlock が使えます。
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
    <style name="ActionBarCustomStyle" parent="@style/Widget.Sherlock.Light.ActionBar.Solid">
        <item name="background">@color/actionBar</item>
        <item name="backgroundSplit">@color/actionBar</item>
        <item name="backgroundStacked">@color/actionBar</item>
        <item name="titleTextStyle">@style/ActionBarCustomTitleStyle</item>
    </style>
    <style name="ActionBarCustomTitleStyle" parent="@style/TextAppearance.Sherlock.Widget.ActionBar.Title">
        <item name="android:textColor">@color/abs__primary_text_holo_dark</item>
        <item name="android:textStyle">bold</item>
    </style>
</resources>
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
    <style name="ActionBarCustomStyle" parent="@style/Widget.Sherlock.Light.ActionBar.Solid">
        <item name="android:background">@color/actionBar</item>
        <item name="android:backgroundSplit">@color/actionBar</item>
        <item name="android:backgroundStacked">@color/actionBar</item>
        <item name="android:titleTextStyle">@style/ActionBarCustomTitleStyle</item>
    </style>
    <style name="ActionBarCustomTitleStyle" parent="@style/TextAppearance.Sherlock.Widget.ActionBar.Title">
        <item name="android:textColor">@color/abs__primary_text_holo_dark</item>
        <item name="android:textStyle">bold</item>
    </style>
</resources>
透過色を指定すると一瞬灰色っぽいのがちらつきましたが
Activity のアニメーションを無効化したら出なくなりました。
2013/05/19

[Android]独自クラッシュレポート送信機能を実装する レポート送信編

[Android]独自クラッシュレポート送信機能を実装する レポート作成編 | DevAchieve
クラッシュレポートをテキストファイルに書き出すところまでは行ったので
後はファイルが有るかどうかチェックして送信するだけです。
私は自分のサーバを持っていないのでメールで送ることにしました。

いくつかのメソッドについては説明は省略しますが、動きはメソッド名通りです。
アプリ内ストレージに保存したレポートファイルを SD に移して
他のアプリで添付できるようにします。
直接 SD に保存しても良いのだけれど
startActivity した直後 delete してるとメール送信時には消えてしまうので注意です。
File file = getFileStreamPath(this, CrashExceptionHandler.FILE_NAME);
if(file.exists() && checkSdCardStatus()){
    String attachmentFilePath = Environment.getExternalStorageDirectory().getPath() + File.separator + getString(R.string.appName) + File.separator + CrashExceptionHandler.FILE_NAME;
    File attachmentFile = new File(attachmentFilePath);
    if(!attachmentFile.getParentFile().exists()){
        attachmentFile.getParentFile().mkdirs();
    }
    file.renameTo(attachmentFile);
    Intent intent = createSendMailIntent(CrashExceptionHandler.MAILTO, getString(R.string.reportMailTitle), getString(R.string.reportMailBody));
    intent = addFile(intent, attachmentFile);
    Intent gmailIntent = createGmailIntent(intent);
    if(canIntent(this, gmailIntent)){
        startActivity(gmailIntent);
    }else if(canIntent(this, intent)){
        startActivity(Intent.createChooser(intent, getString(R.string.sendCrashReport)));
    }else{
        showToast(this, R.string.mailerNotFound);
    }
    file.delete();
}

[Android]独自クラッシュレポート送信機能を実装する レポート作成編

throw Life - Androidアプリのバグ報告システムを考えるとか
コジオニルク - Android - 独自のバグレポート機能とかを参考に
独自クラッシュレポート送信機能を実装してみた。

[Android]Logクラスをもっと使いやすく!クラス名、メソッド名、行数を表示するLogUtilクラス | DevAchieveのクラスにスタックトレースからクラス名、メソッド名、行数を取得する処理を依存しています。
/**
 * キャッチされなかったExceptionが発生した場合にログを記録する
 * 
 * @author wada
 * 
 */
public final class CrashExceptionHandler implements UncaughtExceptionHandler {

    public static final String             FILE_NAME = "report.txt";
    public static final String             MAILTO    = "my.mail@address.com";

    private final Context                  mContext;
    private final PackageInfo              mPackageInfo;
    private final UncaughtExceptionHandler mHandler;

    public CrashExceptionHandler(Context context) {
        mContext = context;
        try{
            mPackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        }catch(NameNotFoundException e){
            throw new RuntimeException(e);
        }
        mHandler = Thread.getDefaultUncaughtExceptionHandler();
    }

    /**
     * キャッチされなかった例外発生時に各種情報をJSONでテキストファイルに書き出す
     */
    @Override
    public void uncaughtException(Thread thread, Throwable throwable){
        PrintWriter writer = null;
        try{
            FileOutputStream outputStream = mContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
            writer = new PrintWriter(outputStream);
            JSONObject json = new JSONObject();
            json.put("Build", getBuildInfo());
            json.put("PackageInfo", getPackageInfo());
            json.put("Exception", getExceptionInfo(throwable));
            json.put("SharedPreferences", getPreferencesInfo());
            writer.print(json.toString());
        }catch(FileNotFoundException e){
            e.printStackTrace();
        }catch(JSONException e){
            e.printStackTrace();
        }finally{
            if(writer != null){
                writer.close();
            }
        }
        mHandler.uncaughtException(thread, throwable);
    }

    /**
     * ビルド情報をJSONで返す
     * 
     * @return
     * @throws JSONException
     */
    private JSONObject getBuildInfo() throws JSONException{
        JSONObject buildJson = new JSONObject();
        buildJson.put("BRAND", Build.BRAND); // キャリア、メーカー名など(docomo)
        buildJson.put("MODEL", Build.MODEL); // ユーザーに表示するモデル名(SO-01C)
        buildJson.put("DEVICE", Build.DEVICE); // デバイス名(SO-01C)
        buildJson.put("MANUFACTURER", Build.MANUFACTURER); // 製造者名(Sony Ericsson)
        buildJson.put("VERSION.SDK_INT", Build.VERSION.SDK_INT); // フレームワークのバージョン情報(10)
        buildJson.put("VERSION.RELEASE", Build.VERSION.RELEASE); // ユーザーに表示するバージョン番号(2.3.4)
        return buildJson;
    }

    /**
     * パッケージ情報を返す
     * 
     * @return
     * @throws JSONException
     */
    private JSONObject getPackageInfo() throws JSONException{
        JSONObject packageInfoJson = new JSONObject();
        packageInfoJson.put("packageName", mPackageInfo.packageName);
        packageInfoJson.put("versionCode", mPackageInfo.versionCode);
        packageInfoJson.put("versionName", mPackageInfo.versionName);
        return packageInfoJson;
    }

    /**
     * 例外情報を返す
     * 
     * @param throwable
     * @return
     * @throws JSONException
     */
    private JSONObject getExceptionInfo(Throwable throwable) throws JSONException{
        JSONObject exceptionJson = new JSONObject();
        exceptionJson.put("name", throwable.getClass().getName());
        exceptionJson.put("cause", throwable.getCause());
        exceptionJson.put("message", throwable.getMessage());
        // StackTrace
        JSONArray stackTrace = new JSONArray();
        for(StackTraceElement element : throwable.getStackTrace()){
            stackTrace.put("at " + LogUtil.getMetaInfo(element));
        }
        exceptionJson.put("stacktrace", stackTrace);
        return exceptionJson;
    }

    /**
     * Preferencesを返す
     * 
     * @return
     * @throws JSONException
     */
    private JSONObject getPreferencesInfo() throws JSONException{
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        JSONObject preferencesJson = new JSONObject();
        Map<String, ?> map = preferences.getAll();
        for(Entry<String, ?> entry : map.entrySet()){
            preferencesJson.put(entry.getKey(), entry.getValue());
        }
        return preferencesJson;
    }
}
呼び出しは1アプリにつき1回で良いらしいです。
Thread.setDefaultUncaughtExceptionHandler(new CrashExceptionHandler(getApplicationContext()));

[Android]独自クラッシュレポート送信機能を実装する レポート送信編 | DevAchieveに続く。

[Android]SharedPreferencesの全ての値を文字列やJSONで取得する

デバッグ用とかクラッシュレポート用に SharedPreferences の全ての値を
文字列やJSONで取得できるとすごく便利です。

PreferenceManager#getSharedPreferencesは ApplicationContext を渡さないと
メモリリークの恐れがあるらしいです。
そのためラッパーを用意してラッパー経由で SharedPreferences を取得します。

public static SharedPreferences getDefaultSharedPreferences(Context context){
    return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
}

public static String toString(Context context){
    return getDefaultSharedPreferences(context).getAll().toString();
}
上記のメソッドで以下のように SharedPreferences の全ての値を文字列として取得することができます。
{key1=value1, key2=value2}
public static String toJsonString(Context context) throws JSONException{
    SharedPreferences preferences = getDefaultSharedPreferences(mContext);
    JSONObject preferencesJson = new JSONObject();
    Map<String, ?> map = preferences.getAll();
    try{
        for(Entry<String, ?> entry : map.entrySet()){
            preferencesJson.put(entry.getKey(), entry.getValue());
        }
    }catch(JSONException e){
        e.printStackTrace();
    }
    return preferencesJson.toString();
}
上記のメソッドで以下のように SharedPreferences の全ての値を JSON として取得することができます。
{"key1": "value1", "key2": "value2"}
#toString の引数にインデントスペース数を渡すとインデントされた JSON が返ってきます。
僕は コマンドラインJSONプロセッサ jq をインスールする | DevAchieve で jq をインストールしているので
インデントせずに JSON 化しますが、インデントされた JSON も取得できるのは便利ですね。
2013/05/15

[Android]添付ファイル付きメール送信Intentを作成する

添付ファイルの MIME Type を返す関数も
[Android]拡張子からMIME Typeを返す関数 | DevAchieveで作ったし、
クラッシュレポートを JSON で送信出来れば
コマンドラインJSONプロセッサ jq をインスールする でフォーマットして見れるし、
データがいっぱい集まったら MongoDB に JSON をインポートして見れたら楽しそう!

このようにクラッシュレポート送信機能を作り始めたがどうにもファイルが添付できない。
Intent#ACTION_SEND | Android Developersによると
setType で添付ファイルの MIME Type を指定する必要があるようなので application/json を指定するも
メーラー側が text/plain と image/* にしか(?)対応していなくて添付できない。

諦めてテキストファイルを送信することにしました。
Intent intent = createSendMailIntent(mailto, subject, body);
intent = addFile(intent, attachmentFile);
Utilクラスに以下のコードをコピペしておくと呼び出しが上記だけで済む。
/**
 * メールを送信するIntentを作成する
 * 
 * @param mailto
 * @param subject
 * @param body
 */
public static Intent createSendMailIntent(String mailto, String subject, String body){
    return createSendMailIntent(new String[]{ mailto }, new String[]{}, new String[]{}, subject, body);
}

/**
 * メールを送信するIntentを作成する
 * 
 * @param mailto
 * @param subject
 * @param body
 */
public static Intent createSendMailIntent(String[] mailto, String subject, String body){
    return createSendMailIntent(mailto, new String[]{}, new String[]{}, subject, body);
}

/**
 * メールを送信するIntentを作成する
 * 
 * @param mailto
 * @param cc
 * @param bcc
 * @param subject
 * @param body
 */
public static Intent createSendMailIntent(String[] mailto, String[] cc, String[] bcc, String subject, String body){
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType(HTTP.PLAIN_TEXT_TYPE);
    intent.putExtra(Intent.EXTRA_EMAIL, mailto);
    intent.putExtra(Intent.EXTRA_CC, cc);
    intent.putExtra(Intent.EXTRA_BCC, bcc);
    intent.putExtra(Intent.EXTRA_SUBJECT, subject);
    intent.putExtra(Intent.EXTRA_TEXT, body);
    return intent;
}

/**
 * メールIntentにファイルを添付する
 * 
 * @param intent
 * @param filePath
 * @return
 */
public static Intent addFile(Intent intent, String filePath){
    return addFile(intent, new File(filePath));
}

/**
 * メールIntentにファイルを添付する
 * 
 * @param intent
 * @param file
 * @return
 */
public static Intent addFile(Intent intent, File file){
    return addFile(intent, Uri.fromFile(file), getMimeType(file));
}

/**
 * メールIntentにファイルを添付する
 * 
 * @param intent
 * @param uri
 * @param mimeType
 * @return
 */
public static Intent addFile(Intent intent, Uri uri, String mimeType){
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_STREAM, uri);
    return intent;
}
2013/05/14

[Android]拡張子からMIME Typeを返す関数

Intent でメールにファイルを添付する場合は MIME Type を指定する必要がある。
いちいち MIME Type まで定義するのは面倒なので拡張子から判断して貰いたい、
と思っていたらちょうどいい関数があったのでお手軽に使えるようにしておいた。

URLEncoder#encode | Android Developersでエンコードすれば
日本語名ファイルでも正しく取得することができる。
/**
 * ファイルパスの拡張子から MIME Type を取得する
 * 
 * @param filePath
 * @return
 */
public static String getMimeType(String filePath){
    String mimeType = null;
    String extension = null;
    try{
        extension = MimeTypeMap.getFileExtensionFromUrl(URLEncoder.encode(filePath, "UTF-8"));
    }catch(UnsupportedEncodingException e){
        e.printStackTrace();
    }
    if(extension != null){
        MimeTypeMap mime = MimeTypeMap.getSingleton();
        mimeType = mime.getMimeTypeFromExtension(extension);
    }
    return mimeType;
}
しかし、これでは半角スペースが + になってしまうので半角スペース混じりのファイル名からは
MimeTypeMap#getFileExtensionFromUrlが拡張子を取得できない。
そのため、普通に文字列処理で拡張子を取得する方法に変更した。
/**
 * ファイルパスの拡張子から MIME Type を取得する
 * 
 * @param filePath
 * @return
 */
public static String getMimeType(String filePath){
    String mimeType = null;
    String extension = null;
    int index = filePath.lastIndexOf(".");
    if(index > 0){
        extension = filePath.substring(index + 1);
    }
    MimeTypeMap mime = MimeTypeMap.getSingleton();
    if(extension != null){
        mimeType = mime.getMimeTypeFromExtension(extension);
    }
    return mimeType;
}
だが、そんな試行錯誤も虚しく英数字以外の文字列のファイル名はメールに添付できないようだった。

参考
Android: should I use MimeTypeMap.getFileExtensionFromUrl()? [bugs] - Stack Overflow
2013/05/13

コマンドラインJSONプロセッサ jq をインスールする

JSON をフォーマットしてくれたり抽出したりしてくれるすごいやつです。

インストール方法は Download jq にあるように
brew install jqとすればできるようなのだけどエラーで失敗するので
バイナリの方をダウンロードしてきてchmod +x jqで実行権限を付与して
パスの通ったところに置けば使えます。

簡単に使い方を説明すると以下のような JSON あったとして、
{"Exception":{"stacktrace":["at [SplashActivity#onCreate:46]","at [Instrumentation#callActivityOnCreate:1047]","at [ActivityThread#performLaunchActivity:1623]","at [ActivityThread#handleLaunchActivity:1675]","at [ActivityThread#access$1500:121]","at [ActivityThread$H#handleMessage:943]","at [Handler#dispatchMessage:99]","at [Looper#loop:130]","at [ActivityThread#main:3701]","at [Method#invokeNative:-2]","at [Method#invoke:507]","at [ZygoteInit$MethodAndArgsCaller#run:866]","at [ZygoteInit#main:624]","at [NativeStart#main:-2]"],"name":"java.lang.OutOfMemoryError"},"PackageInfo":{"packageName":"com.wada811.devcamera","versionCode":1,"versionName":"1.0"},"SharedPreferences":{"isLatestversion":true},"Build":{"VERSION.RELEASE":"2.3.4","DEVICE":"SO-01C","MODEL":"SO-01C","MANUFACTURER":"Sony Ericsson","BRAND":"docomo","VERSION.SDK_INT":10}}
これをjq . report.jsonで以下のように表示することができます。
{
  "Build": {
    "VERSION.SDK_INT": 10,
    "BRAND": "docomo",
    "MANUFACTURER": "Sony Ericsson",
    "MODEL": "SO-01C",
    "DEVICE": "SO-01C",
    "VERSION.RELEASE": "2.3.4"
  },
  "SharedPreferences": {
    "isLatestversion": true
  },
  "PackageInfo": {
    "versionName": "1.0",
    "versionCode": 1,
    "packageName": "com.wada811.devcamera"
  },
  "Exception": {
    "name": "java.lang.OutOfMemoryError",
    "stacktrace": [
      "at [SplashActivity#onCreate:46]",
      "at [Instrumentation#callActivityOnCreate:1047]",
      "at [ActivityThread#performLaunchActivity:1623]",
      "at [ActivityThread#handleLaunchActivity:1675]",
      "at [ActivityThread#access$1500:121]",
      "at [ActivityThread$H#handleMessage:943]",
      "at [Handler#dispatchMessage:99]",
      "at [Looper#loop:130]",
      "at [ActivityThread#main:3701]",
      "at [Method#invokeNative:-2]",
      "at [Method#invoke:507]",
      "at [ZygoteInit$MethodAndArgsCaller#run:866]",
      "at [ZygoteInit#main:624]",
      "at [NativeStart#main:-2]"
    ]
  }
}
すごい!
詳しい使い方は jq Manual を見てください。
2013/05/12

figlet コンソールで文字列をAA化できるコマンド

[小ネタ]コンソールの文字をAA化して表示するツール-Figlet | Developers.IOを見て
HTML ソースとかにこういうのが入っていたら遊び心があって面白いなーとか思うので
入れてみた。
Terminal から HomeBrew で以下のようにインストールするだけで使える。
brew install figlet

figlet "DevAchieve"
 ____              _        _     _                
|  _ \  _____   __/ \   ___| |__ (_) _____   _____ 
| | | |/ _ \ \ / / _ \ / __| '_ \| |/ _ \ \ / / _ \
| |_| |  __/\ V / ___ \ (__| | | | |  __/\ V /  __/
|____/ \___| \_/_/   \_\___|_| |_|_|\___| \_/ \___|
等幅フォントじゃないとズレるし、 line-height が小さめじゃないと少し微妙かも。
2013/05/10

[zsh]Terminalでhistoryからのコマンド補完

Terminal で history からのコマンド補完ができるよ!
という記事を読んだので設定してみたが動かなかったので
漢のzsh から該当部分を見つけ出して設定した。
【コラム】漢のzsh (4) コマンド履歴の検索~EmacsとVi、どっちも設定できるぜzsh
漢のzsh 読まなきゃなぁ。

vi .zshrc で以下を記述して
autoload history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^P" history-beginning-search-backward-end
bindkey "^N" history-beginning-search-forward-end
source .zshrc で設定を読み込んだら Ctrl + p で遡ったりできる。便利。
2013/05/09

[Android]TextViewでURLをリンクにする

前の記事の方法1で bold, italic, underline はエスケープなしで使えることがわかったので
簡単な HTML だったらコレと今回のURLなどをリンクにする設定で十分な気がしてきた。

レイアウトから設定する場合は android:autoLink を指定する。
android:autoLink | TextView | Android Developers
指定できるのは none, web, email, phone, map, all の5つ。
例えば URL をリンクにするのは以下のように指定する。
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:autoLink="web"
    android:text="私のブログのURLは http://wada811.blogspot.com/ です。" />

コードから設定する場合は TextView#setAutoLinkMask(int) を使用する。
TextView#setAutoLinkMask(int) | Android Developers
String text = "私のブログのURLは http://wada811.blogspot.com/ です。";
textView.setAutoLinkMask(Linkify.WEB_URLS);
textView.setText(text);
2013/05/08

[Android]stringリソースにHTMLを入れる

TextView で簡単な HTML 表現をしたかったのでどうやってやろうかと思ったら
いくつか方法があるみたい。

1. bold, italic, underline タグはそのまま使える

Styling with HTML markup | String Resources | Android Developers
実は <b>, <i>, <u>タグはそのまま string リソースに入れても問題ないらしい。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="welcome">Welcome to <b>Android</b>!</string>
</resources>
まじか。この記事を書くにあたって初めて知った。しかしこれでは表現力が低い。そんな時は次の方法で。

2. stringリソースにエスケープした HTML を入れる

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="welcome">Welcome to &lt;b&gt;Android&lt;/b&gt;!</string>
</resources>
サンプルは同じだけどエスケープすればもっと色んなタグを使用できる。
:Tips  TextView を使いこなそう ~ 表示編 ~  その2 - - Google Android -  雑記帳
How to display HTML in android TextView | JavatechIG
TextView で使用出来るタグと属性一覧
<a href="...">
<b>, <big>, <blockquote>, <br>, <cite>, <dfn>
<div align="...">, <em>, <font size="..." color="..." face="...">
<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
<i>, <img src="...">, <p>, <small>
<strike>, <strong>, <sub>, <sup>, <tt>, <u>

3. XML の CDATA セクションを使う

CDATA セクションとは のこと。これを使えば以下のように書ける。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="welcome"><![CDATA[Welcome to Android!]]></string>
</resources>
android - Set TextView text from html-formatted string resource in XML - Stack Overflow
string リソースに HTML を入れる方法ではコレが一番可読性が良くて保守しやすい。
res/raw フォルダに html ファイルを直接持って読み込む方式の方が一番いいかもしれないけど
res/raw フォルダから文字列を読み込む処理を書かなければいけない。
2013/05/07

[Eclipse]コンテンツアシストのトリガーを変更して爆速コーディング

Eclipse > Preference > XML > XML Files > Editor > Content Assist から
Auto Activation の Auto Activation delay (ms) (※補完プロンプト表示ディレイ時間)と
Prompt when these characters are inserted (※補完プロンプト表示トリガー) を
変更します。
ディレイ時間を短くすればするほど爆速ですが思考が付いていかない気がします。
ネタ帳 A.B.C: Android Layout XMLのコーディングを快適にする方法を参考に
トリガーを以下のように設定しました。
<=:._@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ


Java でも Eclipse > Preference > Java > Editor > Content Assist から
Auto Activation の項目を同様に変更することで設定できます。
参考: Eclipseの補完設定をカスタマイズして爆速コーディング - ser1zw's blog
しかしプロンプトが出てからスペースを押すと補完を入力してスペースが入ってしまうので微妙かもしれません。
2013/05/06

[Android]2回の変身を遂げて更に良くなったAlertDialogFragment

[Android]DialogFragmentの汎用性を高めたAlertDialogFragment | DevAchieve
書いたら@noxi515先生が数時間でもっと良い物を書いて公開してくれました。
Androidあれこれ: AlertDialogFragmentつくった
DialogFragment like AlertDialog.Builder | noxi515 / AlertDialogFragment.java

変身後のパワーアップ感ヤバイです。
Android やりながら適当に Java を習得してきた僕が書いたコードがゴミのようだ。
最初 Generic の部分が分からなくてちゃんと Java を勉強しなきゃなぁと思いました。

とりあえず以下の変更/追加のためにフォークしました。
wada811 / AlertDialogFragment.java
以下、変更点。
  • コメント追加。
  • AlertDialog.Builder#setCustomTitle を使えるように変更。
  • AlertDialog#setCanceledOnTouchOutside を呼べるように変更。
  • boolean を使用しない意図がわからなかったので boolean に変更。
  • 呼び出し順に並べ替え。
  • AlertDialogFragment.Builder#setTitleView(int) を追加。
  • AlertDialogFragment.Builder#setView(int) を追加。
  • その他微修正。

Android2.3 で見るダイアログのタイトルってアイコンとタイトル文字の vertical-align が合ってないから
それを簡単に修正したレイアウトを指定したかったので AlertDialog.Builder#setCustomTitle を使えるように。
あと、レイアウトを指定するだけでいいカジュアルなカスタマイズ方法として
AlertDialogFragment.Builder#setTitleView(int) と AlertDialogFragment.Builder#setView(int) も追加。
Honeycomb 以降でダイアログの外部タッチで閉じる、がデフォルトになるらしいので
AlertDialog#setCanceledOnTouchOutside を設定できるようにした。
Y.A.M の 雑記帳: Android Honeycomb 以降ではダイアログの外部タッチで閉じる、がデフォルトになっている

boolean を使用しない理由ってなんでしょう?わたし、気になります!
未指定はデフォルト false みたいな動きが多かったので勝手に変数に初期値 false を持たせました。

Android2.3 で見るダイアログだけか知らないけど AlertDialog.Builder#setView すると
上下に謎のスペースが出来るので内部で AlertDialog#setView でパディング消しています。
他のバージョンだとこれはどうなんでしょう?わたし、気になります!

ところでこのソースコードのライセンスはどうなるの?わたし、気になります!
Gist にライセンス指定があればいいのではないかとか思ったり。

@noxi515先生の次回作にご期待ください!
2013/05/05

[Android]バージョンコードとバージョン名を取得する

アプリ起動時にバージョンチェックして更新を強制させるとか
アプリにバージョン名を表示させるとかしたいときに使えるかもしれない。

#getPackageInfoの第二引数のフラグが何を指定すればいいのかわからなくて
他の人のコードを見るもみんな 0 だったり#GET_ACTIVITIESを指定していて
わりとなんでも動く感じなので気持ち悪いなぁと思いつつ 0 を指定した。

/**
 * バージョンコードを取得する
 * 
 * @param context
 * @return
 */
public static int getVersionCode(Context context){
    PackageManager pm = context.getPackageManager();
    int versionCode = 0;
    try{
        PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
        versionCode = packageInfo.versionCode;
    }catch(NameNotFoundException e){
        e.printStackTrace();
    }
    return versionCode;
}

/**
 * バージョン名を取得する
 * 
 * @param context
 * @return
 */
public static String getVersionName(Context context){
    PackageManager pm = context.getPackageManager();
    String versionName = "";
    try{
        PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
        versionName = packageInfo.versionName;
    }catch(NameNotFoundException e){
        e.printStackTrace();
    }
    return versionName;
}
2013/05/04

[Android]パーミッションの改竄を検出する

アプリケーションの改竄でたまに聞くのが広告を消すために
INTERNET パーミッションを削るというもの。
広告が出るのはうざったいだろうけど削られてたらメシが食えないのでチェックします。
後は怪しいパーミッションを削って使うという用途もあるらしいけど
僕は怪しいパーミッションを取らないので関係ない話。
怪しいパーミッション入りのアプリなんか削る前に使うのやめた方が良いような。

参考
visible true: Androidアプリケーションのパーミッション改竄を検知するスニペット
/**
 * パーミッションが改竄されていないかチェックする
 * 
 * @param context
 * @param permissions
 *        想定するパーミッション
 * @return
 */
public static List<String> diffPermissions(Context context, String[] permissions){
    return diffPermissions(context, new ArrayList<String>(Arrays.asList(permissions)));
}

/**
 * パーミッションが改竄されていないかチェックする
 * 
 * @param context
 * @param permissions
 *        想定するパーミッション
 * @return
 */
private static List<String> diffPermissions(Context context, List<String> permissions){
    if(context == null || permissions == null){
        return new ArrayList<String>();
    }
    try{
        PackageManager pm = context.getPackageManager();
        PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
        String[] requestedPermissions = packageInfo.requestedPermissions;
        if(requestedPermissions != null){
            for(String permission : requestedPermissions){
                if(permissions.contains(permission)){
                    permissions.remove(permission);
                }else{
                    permissions.add(permission);
                }
            }
        }
        return permissions;
    }catch(NameNotFoundException e){
        e.printStackTrace();
    }
    return new ArrayList<String>();
}
メソッド名を diffPermissions に変更。
AndroidManifest.xml と呼び出しの差分をとるように変更。
null を返している部分で空の ArrayList を返すように変更。
String[] permissions = new String[]{ permission.INTERNET, permission.WRITE_EXTERNAL_STORAGE };
if(!diffPermissions(this, permissions).isEmpty()){
    // コードと AndroidManifest.xml に差分がある
}
nullチェックをなくすのと差分を取って将来のパーミッション追加による差分の検知をするように変更。
後から INTERNET パーミッションを追加してコード側に変更しないでリリースして、
そこから INTERNET パーミッションを削られたら検知できないので差分を検知できたほうが便利なはず。
add して差分を検出するのは主に開発者の変更漏れ防止のためなので別になくてもいいけど。
2013/05/03

[Android]アプリケーション(.apk)の自己署名を検証する

簡単なリパッケージ対策のコード。
参考
Android アプリケーション(.apk)の自己署名を検証する方法 | Tech Booster

どの程度効果があるのかは知らないけど
スキルの低い人には効果があるかもしれない。
気休め程度に導入してみた。
/**
 * 正しく署名されているかチェックする
 * 
 * @param context
 * @return
 */
public static boolean checkSigneture(Context context){
    PackageManager pm = context.getPackageManager();
    try{
        PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
        // 通常[0]のみ
        for(int i = 0; i < packageInfo.signatures.length; i++){
            Signature signature = packageInfo.signatures[i];
            if(BuildConfig.DEBUG){
                if(DEBUG_KEY.equals(signature.toCharsString())){
                    return true;
                }
            }else{
                if(RELEASE_KEY.equals(signature.toCharsString())){
                    return true;
                }
            }
        }
    }catch(NameNotFoundException e){
        e.printStackTrace();
    }
    return false;
}
2013/05/02

[Android]DialogFragmentの汎用性を高めたAlertDialogFragment

[Android]DialogFragmentの罠を回避するAlertDialogFragment | DevAchieve
なかなか良いと思ったがそれぞれのダイアログで微妙に表示したい形式が違うと
それぞれクラスを用意しなければならず面倒だったので頑張って汎用的にしてみた。

AlertDailog.Builder でできることの一部を実装済みだけど
設定できるのは他にも色々あって使わないのはよくわからないので
Pull request してくれたら嬉しいです。

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;

public class AlertDialogFragment extends DialogFragment implements OnClickListener {

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // getter/setter用キーの定義
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    private static final String KEY_DIALOG_ICON                 = "dialogIcon";
    private static final String KEY_DIALOG_TITLE                = "dialogTitle";
    private static final String KEY_DIALOG_MESSAGE              = "dialogText";
    private static final String KEY_DIALOG_TITLE_VIEW           = "dialogTitleView";
    private static final String KEY_DIALOG_CONTENT_VIEW         = "dialogContentView";
    private static final String KEY_DIALOG_POSITIVE_BUTTON_TEXT = "dialogPositiveButtonText";
    private static final String KEY_DIALOG_NEUTRAL_BUTTON_TEXT  = "dialogNeutralButtonText";
    private static final String KEY_DIALOG_NEGATIVE_BUTTON_TEXT = "dialogNegativeButtonText";

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // getString系のonAttachまでの一時保持用変数
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    private static final int    DEFAULT_INT_VALUE               = 0;
    private int                 mTitleId                        = DEFAULT_INT_VALUE;
    private int                 mMessageId                      = DEFAULT_INT_VALUE;
    private int                 mPositiveButtonTextId           = DEFAULT_INT_VALUE;
    private int                 mNeutralButtonTextId            = DEFAULT_INT_VALUE;
    private int                 mNegativeButtonTextId           = DEFAULT_INT_VALUE;

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // インスタンス生成とsetArguments(※setArgumentsしないとgetArgumentsでNPE)
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    public static AlertDialogFragment newInstance(){
        AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
        Bundle args = new Bundle();
        alertDialogFragment.setArguments(args);
        return alertDialogFragment;
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // activityにアタッチされていなければ使えないgetString系とリスナーのセット
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);
        if(mTitleId != DEFAULT_INT_VALUE){
            setTitle(getString(mTitleId));
        }
        if(mMessageId != DEFAULT_INT_VALUE){
            setMessage(getString(mMessageId));
        }
        if(mPositiveButtonTextId != DEFAULT_INT_VALUE){
            setPositiveButtonText(getString(mPositiveButtonTextId));
        }
        if(mNeutralButtonTextId != DEFAULT_INT_VALUE){
            setNeutralButtonText(getString(mNeutralButtonTextId));
        }
        if(mNegativeButtonTextId != DEFAULT_INT_VALUE){
            setNegativeButtonText(getString(mNegativeButtonTextId));
        }
        if(activity instanceof DialogListener == false){
            throw new ClassCastException("Activity not implements DialogListener.");
        }
        mListener = (DialogListener)activity;
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // AlertDialog.Builder
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState){
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setIcon(getIcon());
        builder.setTitle(getTitle());
        builder.setMessage(getMessage());
        int titleViewId = getTitleViewId();
        if(titleViewId != DEFAULT_INT_VALUE){
            LayoutInflater inflater = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            builder.setCustomTitle(inflater.inflate(titleViewId, null));
        }
        int contentViewId = getContentViewId();
        if(contentViewId != DEFAULT_INT_VALUE){
            LayoutInflater inflater = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            builder.setView(inflater.inflate(contentViewId, null));
        }
        String positiveButtonText = getPositiveButtonText();
        if(positiveButtonText != null){
            builder.setPositiveButton(positiveButtonText, this);
        }
        String neutralButtonText = getNeutralButtonText();
        if(neutralButtonText != null){
            builder.setNeutralButton(neutralButtonText, this);
        }
        String negativeButtonText = getNegativeButtonText();
        if(negativeButtonText != null){
            builder.setNegativeButton(negativeButtonText, this);
        }
        return builder.create();
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // DialogListener
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    private DialogListener mListener = null;

    public interface DialogListener {

        public void onPositiveButtonClick(String tag);

        public void onNeutralButtonClick(String tag);

        public void onNegativeButtonClick(String tag);

        public void onDialogCancel(String tag);

        public void onDialogDismiss(String tag);

    }

    @Override
    public void onClick(DialogInterface dialog, int which){
        switch(which){
            case DialogInterface.BUTTON_POSITIVE:
                if(mListener != null){
                    mListener.onPositiveButtonClick(getTag());
                }
                break;
            case DialogInterface.BUTTON_NEUTRAL:
                if(mListener != null){
                    mListener.onNeutralButtonClick(getTag());
                }
                break;
            case DialogInterface.BUTTON_NEGATIVE:
                if(mListener != null){
                    mListener.onNegativeButtonClick(getTag());
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onCancel(DialogInterface dialog){
        super.onCancel(dialog);
        if(mListener != null){
            mListener.onDialogCancel(getTag());
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog){
        super.onDismiss(dialog);
        if(mListener != null){
            mListener.onDialogDismiss(getTag());
        }
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // getter/setter
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    private int getIcon(){
        return getArguments().getInt(KEY_DIALOG_ICON, DEFAULT_INT_VALUE);
    }

    public void setIcon(int resId){
        getArguments().putInt(KEY_DIALOG_ICON, resId);
    }

    private String getTitle(){
        return getArguments().getString(KEY_DIALOG_TITLE);
    }

    public void setTitle(int resId){
        mTitleId = resId;
    }

    public void setTitle(String title){
        getArguments().putString(KEY_DIALOG_TITLE, title);
    }

    private String getMessage(){
        return getArguments().getString(KEY_DIALOG_MESSAGE);
    }

    public void setMessage(int resId){
        mMessageId = resId;
    }

    public void setMessage(String text){
        getArguments().putString(KEY_DIALOG_MESSAGE, text);
    }

    private int getTitleViewId(){
        return getArguments().getInt(KEY_DIALOG_TITLE_VIEW, DEFAULT_INT_VALUE);
    }

    public void setTitleViewId(int resId){
        getArguments().putInt(KEY_DIALOG_TITLE_VIEW, resId);
    }

    private int getContentViewId(){
        return getArguments().getInt(KEY_DIALOG_CONTENT_VIEW, DEFAULT_INT_VALUE);
    }

    public void setContentViewId(int resId){
        getArguments().putInt(KEY_DIALOG_CONTENT_VIEW, resId);
    }

    private String getPositiveButtonText(){
        return getArguments().getString(KEY_DIALOG_POSITIVE_BUTTON_TEXT);
    }

    public void setPositiveButtonText(int resId){
        mPositiveButtonTextId = resId;
    }

    public void setPositiveButtonText(String text){
        getArguments().putString(KEY_DIALOG_POSITIVE_BUTTON_TEXT, text);
    }

    private String getNeutralButtonText(){
        return getArguments().getString(KEY_DIALOG_NEUTRAL_BUTTON_TEXT);
    }

    public void setNeutralButtonText(int resId){
        mNeutralButtonTextId = resId;
    }

    public void setNeutralButtonText(String text){
        getArguments().putString(KEY_DIALOG_NEUTRAL_BUTTON_TEXT, text);
    }

    private String getNegativeButtonText(){
        return getArguments().getString(KEY_DIALOG_NEGATIVE_BUTTON_TEXT);
    }

    public void setNegativeButtonText(int resId){
        mNegativeButtonTextId = resId;
    }

    public void setNegativeButtonText(String text){
        getArguments().putString(KEY_DIALOG_NEGATIVE_BUTTON_TEXT, text);
    }
}
getter/setter で Arguments を参照/変更すれば画面回転で破棄されて再生成しても問題ない。
呼び出しは以下のようにする。
Fragment prevDialogFragment = getSupportFragmentManager().findFragmentByTag(TAG_DIALOG_INFO);
if(prevDialogFragment == null){
    AlertDialogFragment dialogFragment = AlertDialogFragment.newInstance();
    dialogFragment.setTitleViewId(R.layout.dialog_info_title);
    dialogFragment.setContentViewId(R.layout.dialog_info_view);
    dialogFragment.setPositiveButtonText(R.string.dialogClose);
    dialogFragment.show(getSupportFragmentManager(), TAG_DIALOG_INFO);
}
タグでリスナーのコールバックでどのダイアログに対するイベントなのか判別できる。
前回と呼び出しが違うのは DialogFragment が dismiss した際に内部で BackStack を pop しているので
いちいち remove する必要がないということがわかったため。
前回のコードはすでにあったら破棄して再表示だったので
重いダイアログは2回表示されるのが見えていたけど、
今回は prevDialogFragment があったら(ダイアログ表示ボタン連打などで消す前に表示していたら)
何もしないで現在表示中のものを表示しておくという処理になっている。

[Android]DialogFragmentの罠を回避するAlertDialogFragment

DialogFragmentの罠

  1. setArgumentsしないと画面回転で値が保持されない
  2. setterなどで追加設定しても画面回転で値が保持されない
  3. ボタンクリックのコールバックリスナーが画面回転で保持されない

setArgumentsしないと画面回転で値が保持されない

public static AlertDialogFragment newInstance(int iconId, int titleId, int textId){
    AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
    Bundle args = new Bundle();
    args.putInt(KEY_DIALOG_ICON, iconId);
    args.putInt(KEY_DIALOG_TITLE, titleId);
    args.putInt(KEY_DIALOG_TEXT, textId);
    args.putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
    alertDialogFragment.setArguments(args);
    return alertDialogFragment;
}
Fragment 関係で newInstance メソッドをよく見る理由は
Fragment が再生成される際にデフォルトコンストラクタが呼び出されるので
newInstance メソッドに引数渡してその中で setArguments の使用を強制させるためらしい。
ということで上記のように記述すれば問題ない。

setterなどで追加設定しても画面回転で値が保持されない

public void setPositiveButtonText(int positiveButtonTextId){
    mPositiveButtonTextId = positiveButtonTextId;
    getArguments().putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
}
通常の setter で追加設定しても画面回転などで DialogFragment が再生成されると
新しいインスタンスを生成するっぽいのでメンバ変数に保持しても意味が無い。
arguments から復元しているっぽいので後から追加しておけば良いんじゃねーのとやってみたら保持された。

ボタンクリックのコールバックリスナーが画面回転で保持されない

public static interface DialogListener {
    /**
     * Positive ボタンが押されたイベントを通知する
     */
    public void onPositiveButtonClick();
}

@Override
public void onAttach(Activity activity){
    super.onAttach(activity);
    if(activity instanceof DialogListener == false){
        throw new ClassCastException("Activity not implements DialogListener.");
    }
    mListener = (DialogListener)activity;
}

@Override
public void onClick(DialogInterface dialog, int which){
    if(mListener != null){
        mListener.onPositiveButtonClick();
    }
}
onAttach でメンバ変数にセットしなおしてやれば問題ない。
呼び出し側の Fragment で DialogListener を implements してやれば良い。

参考

Y.A.M の 雑記帳: Android Fragment で setArguments() してるサンプルが多いのはなぜ?
ネタ帳 A.B.C: DialogFragmentメモ(2)
夜でもアッサム: DialogFragmentの落とし穴にはまらないための方法
コジオニルク - Android - DialogFragment

ソースコード

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;

public class AlertDialogFragment extends DialogFragment {

    public static interface DialogListener {
        /**
         * Positive ボタンが押されたイベントを通知する
         */
        public void onPositiveButtonClick();
    }

    /** 引数受け渡し用のキー */
    private static final String KEY_DIALOG_ICON                 = "dialogIcon";
    private static final String KEY_DIALOG_TITLE                = "dialogTitle";
    private static final String KEY_DIALOG_TEXT                 = "dialogText";
    private static final String KEY_DIALOG_POSITIVE_BUTTON_TEXT = "dialogPositiveButtonText";

    /** Positive ボタンのボタンテキストのリソースID */
    private static int          mPositiveButtonTextId           = android.R.string.ok;

    /** ボタンのクリックを通知するリスナー */
    private DialogListener      mListener                       = null;

    public static AlertDialogFragment newInstance(int iconId, int titleId, int textId){
        AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
        Bundle args = new Bundle();
        args.putInt(KEY_DIALOG_ICON, iconId);
        args.putInt(KEY_DIALOG_TITLE, titleId);
        args.putInt(KEY_DIALOG_TEXT, textId);
        args.putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
        alertDialogFragment.setArguments(args);
        return alertDialogFragment;
    }

    public void setPositiveButtonText(int positiveButtonTextId){
        mPositiveButtonTextId = positiveButtonTextId;
        getArguments().putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
    }

    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);
        if(activity instanceof DialogListener == false){
            throw new ClassCastException("Activity not implements DialogListener.");
        }
        mListener = (DialogListener)activity;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState){
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setIcon(getArguments().getInt(KEY_DIALOG_ICON));
        builder.setTitle(getArguments().getInt(KEY_DIALOG_TITLE));
        builder.setMessage(getArguments().getInt(KEY_DIALOG_TEXT));
        builder.setPositiveButton(mPositiveButtonTextId, new OnClickListener(){
            @Override
            public void onClick(DialogInterface dialog, int which){
                if(mListener != null){
                    mListener.onPositiveButtonClick();
                }
            }
        });
        return builder.create();
    }
    
}

おまけ: 公式の罠?

DialogFragment | Android Developers で紹介されているサンプルで
prev を remove する FragmentTransaction が commit されていないので 何も起こらない?
連打すると普通に2つくらいダイアログが表示されるが、commit すると1つしか表示されないようになる。
private void showDailog(){
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    Fragment prevDialogFragment = getSupportFragmentManager().findFragmentByTag(TAG_DIALOG_INFO);
    if(prevDialogFragment != null){
        fragmentTransaction.remove(prevDialogFragment);
    }
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();
    AlertDialogFragment dialogFragment = AlertDialogFragment.newInstance(android.R.drawable.ic_dialog_info, R.string.dialogTitleInfo, R.string.dialogTextInfo);
    dialogFragment.setPositiveButtonText(R.string.dialogClose);
    dialogFragment.show(getSupportFragmentManager(), TAG_DIALOG_INFO);
}

※追記(2013/05/02)

AlertDialogFragment をもっと汎用的にしました。
[Android]DialogFragmentの汎用性を高めたAlertDialogFragment | DevAchieve

[Android]portraitとlandscapeでthemeを変える

Androidのレイアウトは layout-port/layout-land で portrait と landscape の
レイアウトをそれぞれ指定することができることは知っていたけど
theme についても同様のことができるとは知らなかった。
まぁProviding Resources | Android Developersに書いてあるので
ドキュメントを読もう!という話なんだけど。

てことで res/values-port/style.xml と res/values-land/style.xml を用意すれば良い。
2013/05/01

[Android]タイトルバーの高さを変更する

[Android]タイトルバーにカスタムレイアウトを表示する | DevAchieve
方法1で ImageButton 入りのタイトルバーを作成したが、
通常のテーマでは TextView の高さ分しかなく切れてしまうので
タイトルバーの高さを変更する必要がある。
ちなみに方法2は擬似タイトルバーなので高さは
レイアウトで組んだ通りになるので問題はない。

以下のように記述して AndroidManifest.xml で Theme.Light.iOS を指定すれば良い。
<resources xmlns:android="http://schemas.android.com/apk/res/android">

    <style name="Theme.Light.iOS" parent="@android:style/Theme.Light">
        <item name="android:windowTitleSize">44dp</item>
    </style>

</resources>

parent 属性を指定することで全てのスタイルを指定しなくても parent のスタイルを継承できるので楽ちん。

[Android]タイトルバーにカスタムレイアウトを表示する

iOSみたいにタイトルバーにボタンを入れたかったのでカスタマイズしてみた。
ついでにタイトルも中央揃えにして iOS っぽさを増してみた。

一応2つの方法があるので両方書いておく。
  1. requestWindowFeature(Window.FEATURE_CUSTOM_TITLE) を使う方法
  2. layout を include して擬似タイトルバーを表示する方法

1. requestWindowFeature(Window.FEATURE_CUSTOM_TITLE) を使う方法

@Override
public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
    setContentView(R.layout.activity_main);
    getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.activity_main_title);
}
<?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="44dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        android:gravity="center_vertical"
        android:text="@string/appName"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:textStyle="bold" />

    <ImageButton
        android:id="@+id/infoButton"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:background="@android:color/transparent"
        android:contentDescription="@string/infoButton"
        android:src="@android:drawable/ic_menu_info_details" />

</RelativeLayout>
こちらが正統派なタイトルバーのカスタマイズ。
ImageButton を使用していてデフォルトの高さでは残念なコトになるので高さを変更する必要がある。
[Android]タイトルバーの高さを変更する | DevAchieve

2. layout を include して擬似タイトルバーを表示する方法

@Override
public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
<?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="44dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        android:gravity="center_vertical"
        android:text="@string/appName"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:textStyle="bold" />

    <ImageButton
        android:id="@+id/infoButton"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:background="@android:color/transparent"
        android:contentDescription="@string/infoButton"
        android:src="@android:drawable/ic_menu_info_details" />

</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <include layout="@layout/activity_main_title" />

    <!-- コンテンツのレイアウト -->

</LinearLayout>
こちらは擬似タイトルバーなので NoTitleBar 入りのテーマを AndroidManifest.xml で指定する必要がある。

タグ(RSS)

ブログ アーカイブ