読者です 読者をやめる 読者になる 読者になる

なら@はてなブログ

福岡で働くスマートフォンエンジニア(おっさん)のブログ。更新頻度がとにかく低いのが悩み。

画像ファイルを使わずにそこそこ見栄えのいいボタンを作ってみる

約3ヶ月ぶりの更新。
ちょっと仕事がバタバタしててというか、ストレスたまりすぎてて家に帰ってまでプログラムのことを考えたくなかったというか。


フロンティア(MHF)始めたのが原因かもしれませんが。
3鯖でのんびりやってますので、見かけたら声かけてください。喜びます。


で、仕事でAndroidアプリをいろいろと公開していますが、自分でもAndroid端末使いだして気づいたことは、iPhoneと比較してストレージ容量が非常に少ないこと。
なので、アプリのサイズは少しでも小さくしたほうがよいですね。microSDカードに移せますが、ユーザーが安物SDカード使ってると目に見えて処理が遅くなりますし。


で、これまではオリジナルボタンとか背景とかはデザイナーさんに画像ファイルを作ってもらってたんですが、できれば画像は使わない方向でいきたいなー、ということでちょっとだけやってみました。


とりあえず作ってみたサンプルを貼ります。

f:id:narazoro:20110710131843p:image

f:id:narazoro:20110710131844p:image


画像でかいなあw。


で、上から簡単に書いていきますが、「標準ボタン」ってのはそのままです。
何も指定せずにボタンを作るとそんな感じになります。これは端末依存で少し変わりますね。僕のはXperia arcです。

で、標準ボタンとそれ以外でボタンの大きさが違いますが、これlayoutファイルでは全部同じサイズ(width:120dp/height:48dp)で指定しています。
よーするに、標準ボタンは実際のViewの領域とボタン画像の領域が少し違うみたいですね。


で、どちらも標準以外はdrawableディレクトリの下にxmlを置いて、そこで指定しています。
いっこずつ説明するほどのことではないので、ざっくりと書くと、上の写真は単純なタグで作ってみました。下の写真はを使って複数のを重ねて表示してる感じですね。


ということで、細かいことは抜きにしてxmlのソースを貼ります。


ボタン1

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid
        android:color="#FFFFB7" />
</shape>

ボタン2

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid
        android:color="#FFFFB7" />
    <stroke
        android:color="#F95E00"
        android:width="2dp" />
    <corners 
        android:radius="8dp" />
</shape>

ボタン3

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#FFFFB7" 
        android:endColor="#FF9900"
        android:angle="270" />
    <stroke
        android:color="#F95E00"
        android:width="2dp" />
    <corners 
        android:radius="8dp" />
</shape>

ボタン4

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#FFFFB7"
        android:centerColor="#FF9900" 
        android:endColor="#FFFFB7"
        android:angle="270" />
    <stroke
        android:color="#F95E00"
        android:width="2dp" />
    <corners 
        android:radius="8dp" />
</shape>

ボタン5

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#FFFFB7"
        android:centerColor="#FFFFB7" 
        android:endColor="#FF9900"
        android:angle="270" />
    <stroke
        android:color="#F95E00"
        android:width="2dp" />
    <corners 
        android:radius="8dp" />
</shape>

ボタン6

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <solid
            android:color="#00000000" />
    </shape>
</item>
<item android:top="1dp" android:left="4dp" android:bottom="4dp" android:right="4dp">
    <shape android:shape="rectangle">
        <solid
            android:color="#D7EBFF" />
    </shape>
</item>
</layer-list>

ボタン7

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <solid
            android:color="#D7EBFF" />
    </shape>
</item>
<item android:top="24dp">
    <shape android:shape="rectangle">
        <solid
            android:color="#B7DBFF" />
    </shape>
</item>
</layer-list>

ボタン8

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <solid
            android:color="#D7EBFF" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
<item android:top="24dp">
    <shape android:shape="rectangle">
        <solid
            android:color="#B7DBFF" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
</layer-list>

ボタン9

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <gradient
            android:startColor="#D7EBFF"
            android:endColor="#59ACFF"
            android:angle="270" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
<item android:top="24dp">
    <shape android:shape="rectangle">
        <solid
            android:color="#28002F7B" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
</layer-list>

ボタン10

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <gradient
            android:startColor="#D7EBFF"
            android:centerColor="#59ACFF"
            android:endColor="#D7EBFF"
            android:angle="270" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
<item android:top="24dp">
    <shape android:shape="rectangle">
        <gradient
            android:startColor="#28002F7B"
            android:centerColor="#50002F7B"
            android:endColor="#36002F7B"
            android:angle="270" />
        <corners
            android:radius="6dp" />
    </shape>
</item>
</layer-list>

ボタン11

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <solid 
            android:color="#B30078"/>
    </shape>
</item>
<item android:top="2dp" android:left="2dp" android:right="2dp" android:bottom="2dp">
    <shape android:shape="rectangle">
        <solid
            android:color="#FFFFFF" />
    </shape>
</item>
<item android:top="3dp" android:left="3dp" android:right="3dp" android:bottom="3dp">
    <shape android:shape="rectangle">
        <gradient 
            android:startColor="#FFE3F7"
            android:endColor="#FF35BD"
            android:angle="270"/>
    </shape>
</item>
<item android:top="24dp" android:left="3dp" android:right="3dp" android:bottom="3dp">
    <shape android:shape="rectangle">
        <gradient 
            android:startColor="#28FD00AB"
            android:endColor="#28FF8EDB"
            android:angle="270"/>
    </shape>
</item>
</layer-list>

ボタン12

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
    <shape android:shape="rectangle">
        <gradient 
            android:startColor="#FFF2B7"
            android:centerColor="#FFC959"
            android:endColor="#FFF2B7" />
        <corners 
            android:radius="10dp" />
        <stroke
            android:width="2dp"
            android:color="#F95E00" />
    </shape>
</item>
<item android:top="34dp" android:left="2dp" android:bottom="10dp" android:right="2dp">
    <shape android:shape="rectangle">
        <gradient
            android:startColor="#FFE1A4"
            android:endColor="#F95E00" />
    </shape>
</item>
<item android:top="2dp" android:left="98dp" android:bottom="2dp" android:right="18dp">
    <shape android:shape="rectangle">
        <gradient
            android:startColor="#FFE1A4"
            android:endColor="#F95E00" 
            android:angle="270" />
    </shape>
</item>
</layer-list>


公式リファレンスで書いてること以上のものはやってないです。
あ、いっこ気づいた点として、タグって4隅それぞれ別の数値を設定できるのですが、で重ねているの中で4隅別の指定をしたらエラーが出てlayoutのほうで読み込んでくれませんでした。残念。


久々に書いた割には内容うすいわー。うっすいわー。

ダイアログ表示中のsearchキー

ほひー。こんな更新頻度でblogと呼んでいいんじゃろか?
というわけで、例によって久々の更新でございます。


タイトルそんまんまです。
Androidではいろいろダイアログ使うと思います。エラーダイアログとかは普通に表示させるだけなんでアレですが、YES/NOのついた確認ダイアログとかで、必ずダイアログで準備したボタンのリスナー処理を走らせたい場合はよくありますね。
あとは、非同期処理中に表示させるProgressDialogなんかも、途中でダイアログ消されると困る場合が多いです。

そういった場合、通常は

Dialog.setCancelable(false)

を設定することで、ダイアログ表示をキャンセルさせない設定ができます。

ただ、これはあくまで端末のbackキーを無効にするだけで、端末のsearchキーを押された場合はダイアログが消えてしまいます。
困りました。

で、その対応としては、普通にsearchキーのイベントを無視すればいいんですが、ダイアログの表示中は呼び出し元のActivityではなく、前面に出ているダイアログがイベントを拾うので、実装場所の注意が必要です。
具体的には、表示するDialog自身がキーイベントを拾って無効にする、という処理を書く必要があります。


実装例

Dialog dialog = new Dialog(this);
dialog.setCancelable(false);
dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
    @override
    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
        // searchキーの場合はイベントを消化したことにして返す
        if (keyCode == KeyEvent.KEYCODE_SEARCH) {
            return true;
        }
        // それ以外はイベント未消化で返す
        return false;
    }
});
// 以下、ダイアログ構築の処理を続ける

って感じですか。

サンプルはしかさんに作ってもらったんですがw。

Android Market で特定のキャリアだけに公開する

はい、また2ヶ月放置しておりました!
・・・いや、読者そんなにおらんから別にええねんけどな・・・。


えーと、遊んでいたわけではなく、むしろ生活リズム破壊しながら仕事してました!
インフルエンザもかかりました!


で、そんなこんなで、去る2月25日、ならさんの開発したアプリがとあるAndroid端末のプリインアプリとして採用され、無事、店頭に並んでおります。
昨日はヨドバシでホットモックさわってニヨニヨしてましたが。



で、ここ数カ月でいろいろTipsがたまってきたので、時間をみつけて少しずつアップしていこうと思うのですが、とりあえず今はモンハンやりたい。
あと、アプリはできたけど周辺が固まってないので、これから連携部分とか開発しなきゃダメなのです。なので、ひょっとしたらしばらくはサーバサイド作ってるかも。


ということで本題。
意外と知られていない(と思われる)ことですが、Android Market、いつのまにかキャリア指定でアプリ公開指定ができるようになってたのね。
ひょっとして知らなかったの僕だけ? というオチかもしれませんが、僕と同じようにご存知ない方いるかもしれないので書いておきます。


おなじみのAndroid Marketデベロッパーコンソールです。
f:id:narazoro:20110227092909p:image


で、「すべての国/地域」のチェックを外すと、国を選択できますよね。
f:id:narazoro:20110227092910p:image


ここで、なぜかアンカーが張られてる国名をクリックしてみます。
f:id:narazoro:20110227092911p:image


選べるんかーい!!!

LinearLayoutでジェスチャーイベントを拾う

いやー、いろいろ忙しくて1ヶ月以上も放置しておりました。
相変わらずAndroidアプリ開発にどっぷりで、地元のイベントにも行けないことが多くなりました。
 
さてさて、今回の記事ですが、表題の通りです。
普通にコーディングしてできそうな気がしますが、多くのViewGroupではジェスチャーイベントが取得できないようです。
 
私は画面全体を占めるLinearLayoutでフリックイベントを検知するために、GestureDetectorを使ってみたのですがうまくいかず。
 
うまくいかないコード(SomeActivity.java

// GestureDetectorインスタンスを保持しておく
private GestureDetector mGestureDetector;
 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    // GestureDetectorを生成(リスナーを設定する)
    mGestureDetector = new GestureDetector(this, new OnFlingListener());
    
    // LinearLayoutにonTouchListenerを設定して、そこからジェスチャーイベントを
    // 拾うようにする
    findViewById(R.id.LinearLayout01).setOnTouchListener(new OnTouchListener {
        public boolean onTouch(View v, MotionEvent event) {
            return mGestureDetector.onTouchEvent(event);
        }
    });
}
 
// GestureDetector.OnGestureListenerの実装
class OnFlingListener extends GestureDetector.SimpleOnGestureListener {
    // onFlingをオーバーライド
    onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        // do something
        Log.d("FLING_TEST", "onFling was called.");
    }
}


さて、どこが間違っているのかというと、そもそも、LinearLayoutのデフォルトの動きとして、連続したイベント(要するに、画面をタップしてから動かして離すまでの一連の動作)を取得していないようです。
実際にログを出してみるとわかりますが、タップした瞬間にイベントメソッドが呼ばれていますが、その後で指をいくら動かしても何も起こりません。
ということは、一連の動きを判定してイベントを判定するジェスチャー系の処理は、そのままでは使えないということですね。
 
で、こういうときは、とりあえず公式ドキュメントを読みます。
ViewGroup | Android Developers

ということで、こういう場合はViewGroup.onInterceptTouchEventをオーバーライドした独自Viewを作成するのがスタンダードなやり方っぽいです。
ただし、上記リンク先の注意事項にある通り、このメソッドをオーバーライドしただけではまだジェスチャーイベントは拾ってくれません。
 
詳しくは上記のドキュメントに書いてありますが、LinierLayoutでジェスチャーを取得したい場合は、まずonTouchEventメソッドがtrueを返す必要があるようです。ここでfalseを返してしまうと、以降の連続したイベント発生時にonTouchEventメソッドが呼ばれません。
それとは逆に、onInterceptTouchEventのほうでは、falseを返している限りは連続したイベントが取得できて、trueを返すとそこでイベント取得が終わってしまうようです。
 
ということで、イベント発生条件なんかを考慮しなくてよい場合のもっとも簡単な実装は、
 
onInterceptTouchEvent(MotionEvent)でfalseを返す
onTouchEvent(MotionEvent)でtrueを返す
 
ということになるでしょうか?
例によって英文読解がてきとうなので、どこか間違ってるかもしれませんが、LinearLayoutの代わりに上記の実装をしたサブクラスを使用すると、LinearLayoutでもジェスチャーを取得できました。
 
 
あー、やっぱ時間がないと文章も適当ですね。
ほんとはもうちょっときちんとソースとか説明を書きたいんですが。

ProgressDialogをカスタマイズする

引き続きAndroid。たぶん年内はAndroidどっぷりになりそうなので、記事もそっちに偏りそうです。
 
さて、Androidに限ったことではなく、時間がかかりそうな処理をするときは処理中のお待ちくださいダイアログを出しますね。
Tim Brayも言ってましたが、レスポンスタイムが200msを超えるとユーザはストレスを感じるそうです。昔のWebサービスだと「8秒ルール」とかあったんですが、それに比べるとざっと1/40です。
 
前置きはさておき。
そのProgressDialogですが、デフォルトだといかにもAndroidっぽくて味気ないデザインです。
いちおう処理中画像(ぐるぐる回るやつ、または進捗バー)を自前の画像に差し替えることができますが、基本それだけですね。
f:id:narazoro:20101107011020p:image
 
というわけで、自分でレイアウトを作ってしまうのが一番いいのではないかと。
実用系アプリだとデフォルトで全然問題ないと思うのですが、通常レイアウトをきちんと作りこんでいる場合、ダイアログだけデフォルトで味気ないという状況は避けたいですね。
 
で、一番簡単なやり方と思われるものを。
まずはレイアウトを作ります。
/res/layout/custom_progress_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
<ProgressBar android:id="@+id/ProgressBar01" 
    android:layout_height="wrap_content" android:layout_width="wrap_content" 
    android:indeterminate="true" android:indeterminateDrawable="@drawable/progress_bar" 
    style="?android:attr/progressBarStyle"  />
</LinearLayout>

 
上記ProgressBarのandroid:indeterminateDrawable要素に、自作パラパラアニメを設定しています。
で、そのパラパラアニメの定義例は以下。
/res/drawable/progress_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
    <item android:drawable="@drawable/b_1" android:duration="200" />
    <item android:drawable="@drawable/b_2" android:duration="200" />
    <item android:drawable="@drawable/b_3" android:duration="200" />
    <item android:drawable="@drawable/b_4" android:duration="200" />
    <item android:drawable="@drawable/b_3" android:duration="200" />
    <item android:drawable="@drawable/b_2" android:duration="200" />
</animation-list>

 
で、プログラム上でどう書くかというと、単にDialog作ってContentViewを設定してやるだけです。
MainActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    dialog = new Dialog(this);
    dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    dialog.setContentView(R.layout.custom_progress_dialog);
    dialog.show();
}

 
で、こうなる。
f:id:narazoro:20101107012000p:image
 
この画像だと全然わかりませんが、いちおう色調がうねうね変わってます。わからんっつーに。
デフォルトより味気ないダイアログになりましたが、今回は「ProgressDialogのレイアウトを自作する」のが目的なのでクオリティは気にしない。
 
と、ここまで書きましたが、これだけだとたぶん不完全です。
なぜかというと、これは普通のダイアログなので、「戻るボタン」でダイアログがキャンセルされるんですね。
なので、そのへんの制御を追加してやるか、キャンセルされた時の処理を入れるかが必要ですね。Dialog.setCancelable(false)あたりですかね。
 
そうそう、意外と明記されてるところが少ないのですが、ProgressBarに指定するdrawableのサイズ、基本は48*48(dp換算)です。で、大きめ画像のスタイル(progressBarStyleLarge)にすると76*76みたいですね。ご参考までに。

SQLiteに大量の初期データを登録したい

Androidアプリのお話。
軽いアプリを作るだけならあんまり気にしなくていい話ですが、本格的というか大量のデータを使うアプリを作ろうと思ったら、やはりデータベースは避けては通れないわけで。
で、開発者の方ならご存知の通り、AndroidにはSQLiteが組み込まれています。


データベースを作って、マスタデータ的なものを登録しておきたいところなのですが、Androidアプリのお作法だと、だいたいSQLiteOpenHelperでデータベースを作成したり定義更新したり、ってのが一般的ですね。
それだと、初回起動時にデータベースを作成して、初期データをえんえんINSERTし続けるという処理が必要になってきます。そしてSQLiteのINSERTは遅いです。しかも、登録元のデータをWebサーバから落としたりする仕様だと、ネットワークとデータベースのコストがそのままユーザのストレスにつながります。


さて、どうしよう?


いくつか案があるのですが、まずは通信コストをなくす方向。
登録元データをアプリに同梱します。たぶんassetsあたりに入れるのが正しいのかなー? これが第1案として。


第2案。SQLiteデータベースのファイルをあらかじめ作っておいて(データベース作成してINSERTまでやっておく)、それを同じようにassetsにおいて端末なりSDカードなりにコピーする方法。


で、双方ともメリット・デメリットがあります。
前提として、assetsに置いたファイルはアプリ起動後に更新できません。削除もできません。なので、初期データ登録後はゴミファイルが残ることになります。


第1案のメリットは圧縮効率。たとえば、元データをCSVファイルにしてzip圧縮すれば、かなりサイズは小さくなります。
カラム数とかサイズも関係しますが、一例として、20万レコードくらいのデータがCSVで約4MB、zip圧縮したら700KBくらい。
デメリットは、結局はデータベース作ってINSERTせなあかんので、初期登録のレスポンスが悪い。上記の例だと2分くらい。


第2案のメリット・デメリットは、そのまま第1案の逆。
圧縮効率は悪いです。上記と同じデータだと、SQLiteデータベースファイルの状態で約6.5MB。CSVより大きいですね。で、圧縮したら2MB。圧縮効率もテキストのCSVに劣ります。
メリットは初期データ登録が速いこと。zip圧縮したファイルを端末に解凍して配置するだけなので、ほんの数秒です。
あ、デメリットもういっこあった。SQLiteOpenHelperが使いづらい(使えないわけではない)。普通にやったら使えないと思います。SQLiteDatabase.openOrCreateDatabase()とかを使うことに。


試していませんが、おそらく事前にデータベースファイルを作るとき、Androidアプリ内でSQLiteOpenHelper使って作っておけば、データベースのバージョン管理とかもできそうな気がします。
ちなみに、別パッケージで作ったSQLiteファイルでも、使いたいアプリに同梱して展開すれば普通に読み込めます。Androidアプリって、各種パーミッションはOSのファイルパーミッションでやってるので、そのファイル自体を自分のアプリで作成してしまえば問題なくアクセスできるかと。



いや、データそんなに多くないのであれば、普通にサーバから元データ落としてくるのが一番簡単だと思いますけどね。



うわ、今回文字ばっかや。

Android Night in Fukuoka 2010

イベント行ってきましたー。
例によって、「予定ある」って前もって言ってる日に限って会議入れられる罠。
 
40分ほど遅れて参加したら、AIP Cafe人大杉
ちょうどヌーラボ縣さんのプレゼンが終わったところで、しかもAndroidのUI作成についてかなり興味深い内容だったらしく(後からしかさんに教えてもらった)。
 
日本Androidの会のイベントは初めてだったので、なるべく顔覚えてもらおうと思って2次会まで参加。
しょうき赤坂店?だったかな。2,500円で飲み放題付きでもつ鍋で料理も多くて、いや、安いわ。ローテーションに入れておこう。
 
 
肝心の内容。
 
上海Androidの会の中尾さんのお話がメインだったんですが、KDDIの方がIS03/IS06を持ってきてくださったのでそれらを触りつつ。
中国、本家Android Marketよりサードパーティのマーケットの方が主流、というか本家は国内産端末からはつながらないらしい。ふむー。
KDDIの方は、ひょっとしたらこれから仕事で会うことになるかも知れない。なんかうちの営業がアポとってる人と名前が同じだったような?
 
覚えた言葉:Great Fire Wall
 
IS03。かなりヌルヌル動く。IS06は触るタイミングがなかった。残念。
f:id:narazoro:20101102194530j:image
 
じゃんけん大会でDroid君ストラップもらった。本当はDroid Robot君らしい。
f:id:narazoro:20101102213956j:image
 
また行こう。


日本Androidの会

日本Androidの会