すごいよlibandrotranslation

libandrotranslationというAndroidアプリの翻訳を助けるライブラリがあります。

libandrotranslation - Android user translation library. - Google Project Hosting 
http://code.google.com/p/libandrotranslation/

導入方法についてはこちらをどうぞ

Description - libandrotranslation - LibAndroTranslationの説明 - Android user translation library. - Google Project Hosting 
http://code.google.com/p/libandrotranslation/wiki/Description

ユーザが翻訳をして、翻訳したstring.xmlファイルをデベロッパにメールで送られてくる仕組みです。
導入はすごく簡単でした。
こんな感じで送られてきます。


私はこれを使ってスクリーンキャプチャショートカットの翻訳を募集していました。
成果はというと、1ヶ月間で65件のメールが来ました。

韓国語が多いのはアプリの特性でGalaxySのみ対応しているからです。
これを使ってスクリーンキャプチャーショートカットのアップデートをしました。

Version 1.2.0 - Androidアプリ スクリーンキャプチャーショートカット 
https://sites.google.com/site/screencaptureshortcut/what-s-new/version120

機械翻訳ではなく人力翻訳を1ヶ月でこれだけ集まったのはすごいです!

端末の振りを検知する

加速度を使って端末の振りを検知します。
簡単そうだけど、考えてみると難しいでした。
端末が振られた時に、加速度の平均値と、加速度の差が大きくなる事を利用して振りを検知しています。

ソースコード

package jp.tomorrowkey.android.shakelistener;

import java.util.Arrays;
import java.util.LinkedList;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

/**
 * 端末が振られたことを検知するクラス
 * 
 * @author tomorrowkey at gmail.com
 */
public class ShakeListener implements SensorEventListener {

    public static final String LOG_TAG = ShakeListener.class.getSimpleName();

    /**
     * X方向に振られた時に{@link ShakeListener.OnShakeListener#onShaked(int)}でこのフラグが立ちます
     */
    public static final int DIRECTION_X = 0x0001;

    /**
     * Y方向に振られた時に{@link ShakeListener.OnShakeListener#onShaked(int)}でこのフラグが立ちます
     */
    public static final int DIRECTION_Y = 0x0010;

    /**
     * Z方向に振られた時に{@link ShakeListener.OnShakeListener#onShaked(int)}でこのフラグが立ちます
     */
    public static final int DIRECTION_Z = 0x0100;

    /**
     * 加速度の判定をするために最大いくつのデータを保持するか設定します
     */
    private static final int SAMPLING_SIZE = 50;

    /**
     * 端末が振られた事を検知するコールバックインターフェイスです
     */
    public interface OnShakeListener {
        /**
         * 端末が振られた時に呼ばれるメソッドです
         * 
         * @param direction 端末が振られた方向を表します
         * @see ShakeListener#DIRECTION_X
         * @see ShakeListener#DIRECTION_Y
         * @see ShakeListener#DIRECTION_Z
         */
        void onShaked(int direction);
    }

    /**
     * コールバッククラス
     */
    private OnShakeListener mOnShakeListener = null;

    /**
     * 振りが検知されなくてもコールバックを返すかを保持します
     */
    private boolean mIsCallbackAlways = false;

    /**
     * 加速度の平均値と、絶対値の差がどれだけ差があると振られた事にするか保持します
     */
    private int mDifferenceThreshold = 500;

    /**
     * 加速度の値を保持しておくリスト
     */
    private LinkedList<float[]> mAccelerometerList;

    /**
     * 加速度の値の合計<br>
     * サイズはx,y,z分で3つ<br>
     * 加速度センサーの反応の度に計算するとコストが高いのでメモリに保持しておく
     */
    private float[] mAccelerometerSamplingSums;

    /**
     * コンストラクタ
     */
    public ShakeListener() {
        mAccelerometerList = new LinkedList<float[]>();
        mAccelerometerSamplingSums = new float[] {
                0f, 0f, 0f
        };
    }

    /**
     * センサーを登録します<br>
     * センサー検知の頻度は{@link SensorManager#SENSOR_DELAY_FASTEST } が設定されます。
     * 振りを検知した時のみにコールバックメソッドが呼び出されます。
     * 
     * @param sensorManager
     * @param l コールバックリスナ
     */
    public void registerListener(SensorManager sensorManager, OnShakeListener l) {
        registerListener(sensorManager, l, SensorManager.SENSOR_DELAY_FASTEST, false);
    }

    /**
     * センサーを登録します<br>
     * センサー検知の頻度は{@link SensorManager#SENSOR_DELAY_FASTEST } が設定されます。
     * 
     * @param isCallbackAlways true を設定した場合、振りを検知しない場合でもコールバックメソッドを呼び出します。false
     *            を設定した場合、振りを検知した場合のみコールバックメソッドを呼び出します。
     * @param sensorManager
     * @param l コールバックリスナ
     */
    public void registerListener(SensorManager sensorManager, OnShakeListener l,
            boolean isCallbackAlways) {
        registerListener(sensorManager, l, SensorManager.SENSOR_DELAY_FASTEST, isCallbackAlways);
    }

    /**
     * センサーを登録します<br>
     * 振りを検知した時のみにコールバックメソッドが呼び出されます。
     * 
     * @param sensorManager
     * @param l コールバックリスナ
     * @param rate センサー検知の頻度を設定します。{@link SensorManager#SENSOR_DELAY_NORMAL}
     *            {@link SensorManager#SENSOR_DELAY_UI}
     *            {@link SensorManager#SENSOR_DELAY_GAME}
     *            {@link SensorManager#SENSOR_DELAY_FASTEST}
     */
    public void registerListener(SensorManager sensorManager, OnShakeListener l, int rate) {
        registerListener(sensorManager, l, rate, false);
    }

    /**
     * センサーを登録します。
     * 
     * @param sensorManager
     * @param l コールバックリスナ
     * @param rate センサー検知の頻度を設定します。{@link SensorManager#SENSOR_DELAY_NORMAL}
     *            {@link SensorManager#SENSOR_DELAY_UI}
     *            {@link SensorManager#SENSOR_DELAY_GAME}
     *            {@link SensorManager#SENSOR_DELAY_FASTEST}
     * @param isCallbackAlways true を設定した場合、振りを検知しない場合でもコールバックメソッドを呼び出します。 false
     *            を設定した場合、振りを検知した場合のみコールバックメソッドを呼び出します。
     */
    public void registerListener(SensorManager sensorManager, OnShakeListener l, int rate,
            boolean isCallbackAlways) {
        if (l == null)
            throw new IllegalArgumentException("OnShakeListener is required");

        mOnShakeListener = l;
        mIsCallbackAlways = isCallbackAlways;
        mAccelerometerList.clear();
        mAccelerometerSamplingSums = new float[] {
                0f, 0f, 0f
        };
        sensorManager.registerListener(this,
                sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), rate);
    }

    /**
     * センサー登録を解除します。
     * 
     * @param sensorManager
     */
    public void unregisterListener(SensorManager sensorManager) {
        sensorManager.unregisterListener(this);
        mOnShakeListener = null;
    }

    /**
     * 現在センサーが登録されているか判定します。
     * 
     * @return true の場合、登録されている。falseの場合、登録されていない。
     */
    public boolean isRegisteredListener() {
        return mOnShakeListener != null;
    }

    /**
     * 加速度の平均値と、絶対値の差がどれだけ差があると振られた事にするかを設定します<br>
     * この値を設定することで振りの検知の強弱を設定できます。 <br>
     * 初期値は500です。<br>
     * 最適な値は端末または使う人によって異なります
     * 
     * @param differenceThreshold
     */
    public void setDifferenceThreshold(int differenceThreshold) {
        mDifferenceThreshold = differenceThreshold;
    }

    // @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    // @Override
    public void onSensorChanged(SensorEvent event) {
        mAccelerometerSamplingSums[0] += event.values[SensorManager.DATA_X];
        mAccelerometerSamplingSums[1] += event.values[SensorManager.DATA_Y];
        mAccelerometerSamplingSums[2] += event.values[SensorManager.DATA_Z];
        mAccelerometerList.add(Arrays.copyOf(event.values, event.values.length));
        if (mAccelerometerList.size() > SAMPLING_SIZE) {
            float[] removedValues = mAccelerometerList.removeFirst();
            mAccelerometerSamplingSums[0] -= removedValues[SensorManager.DATA_X];
            mAccelerometerSamplingSums[1] -= removedValues[SensorManager.DATA_Y];
            mAccelerometerSamplingSums[2] -= removedValues[SensorManager.DATA_Z];
        }

        float xAverage = mAccelerometerSamplingSums[0] / mAccelerometerList.size();
        float yAverage = mAccelerometerSamplingSums[1] / mAccelerometerList.size();
        float zAverage = mAccelerometerSamplingSums[2] / mAccelerometerList.size();

        float xAbsTotal = 0;
        float yAbsTotal = 0;
        float zAbsTotal = 0;
        for (int i = 0; i < mAccelerometerList.size(); i++) {
            float[] values = mAccelerometerList.get(i);
            xAbsTotal += Math.abs(values[SensorManager.DATA_X] - xAverage);
            yAbsTotal += Math.abs(values[SensorManager.DATA_Y] - yAverage);
            zAbsTotal += Math.abs(values[SensorManager.DATA_Z] - zAverage);
        }

        int direction = 0;

        if (xAbsTotal > mDifferenceThreshold)
            direction |= DIRECTION_X;

        if (yAbsTotal > mDifferenceThreshold)
            direction |= DIRECTION_Y;

        if (zAbsTotal > mDifferenceThreshold)
            direction |= DIRECTION_Z;

        if (mOnShakeListener != null && (mIsCallbackAlways || direction != 0))
            mOnShakeListener.onShaked(direction);
    }
}

フッタにボタンを表示する

ボタン2つ


この画面がどういう構成になっているかソースコードを読む。

uninstall_confirm.xml
android.git.kernel.org Git - platform/packages/apps/PackageInstaller.git/blob - res/layout/uninstall_confirm.xml
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:orientation="horizontal"
    style="@android:style/ButtonBar"
>
    <Button android:id="@+id/ok_button"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/ok"
    />

    <Button android:id="@+id/cancel_button"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/cancel"
    />
</LinearLayout>

layout_widthに0dp、layout_weightに1を設定することで、ボタン大きさが画面の半分ずつになるようにしている。
フッタのViewGroupにLinearLayoutを使用しており、styleに@android:style/ButtonBarを設定している。

styles.xml
android.git.kernel.org Git - platform/frameworks/base.git/blob - core/res/res/values/styles.xml
<style name="ButtonBar">
    <item name="android:paddingTop">5dip</item>
    <item name="android:paddingLeft">4dip</item>
    <item name="android:paddingRight">4dip</item>
    <item name="android:paddingBottom">1dip</item>
    <item name="android:background">@android:drawable/bottom_bar</item>
</style>

ボタンがセンタリングされるようパディングを設定している。
背景色の灰色は@android:drawable/bottom_barで設定しているようだ。

bottom_bar.png

android.git.kernel.org Git - platform/frameworks/base.git/blob - core/res/res/drawable-hdpi/bottom_bar.png
9patchかxmlだとおもいきや普通のpng画像だった。
hdpi/mdpi/ldpiが用意されている。
一番上に境界線を表す明るい色が使われ、縦にグラデーションがかかっている。
高さが伸びるとグラデーションが荒くなるが、横に伸びる分には問題ないので、landscapeとportlateどちらでも使えそうだ。

ボタン1つ


この画面がどういう構成になっているかソースコードを読む。

android.git.kernel.org Git - platform/packages/providers/GoogleSubscribedFeedsProvider.git/blob - res/layout/manage_accounts_screen.xml
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="@android:drawable/bottom_bar">

    <View
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>

    <Button android:id="@+id/add_account_button"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:layout_marginTop="5dip"
        android:text="@string/add_account_label" />

    <View
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:layout_weight="1"/>
</LinearLayout>

Viewをおく事で適度な大きさになるようにしているようだ。
左右のViewのweightを1に、中央のボタンのweightを2にすることで中央に画面半分分のボタンを表示している。
左右のパディングは必要ないので、styleを使わずにボタンにマージンを設定して余白を調節している。
背景色はボタン2つの場合と同じで@android:drawable/bottom_barを使用している。

ボタン1つ(違うパターン)

以前、[twitter:@R246] さんに教えていただいた書き方

<LinearLayout
  android:orientation="horizontal"
  android:background="@android:drawable/bottom_bar"
  android:gravity="center_horizontal"
  android:weightSum="2"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">
  <Button
    android:id="@+id/doneButton"
    android:text="@string/done"
    android:layout_weight="1"
    android:layout_marginTop="5dip"
    android:layout_width="0dip"
    android:layout_height="wrap_content" />
</LinearLayout>

weightSumに2を指定することでLinerLayoutの全体のweightが2になり、ボタンのweightに1を指定することで、ボタンの大きさが画面の半分になる。
さらにgravityにcenter_horizontalを指定するとボタンがセンタリングされる。
このほうがViewが少なくなり、すっきり。

所感

やっと書き方覚えたぞ

ブラウザからlogcatを見る

こちらが最新です
ブラウザからlogcatを見る(アップデートしました) - 明日の鍵 
http://d.hatena.ne.jp/tomorrowkey/20111001/1317451235

充電していない状態でlogcatを確認したくて、どうすればいいか悩んでました。
思いついたのが、表示するアプリを作ればいいんじゃね!って事。
しかし、端末に表示するのでは画面が小さすぎる、読みづらいので
Socketでパソコンとつないで表示することに
ついでに気になってたWebSocketを使うようにすれば、新しい事もできて一石二鳥!わーい!

構成

サーバ側

Androidアプリ
画面は一つでサーバの起動と停止のみ行う
サーバが起動するのはWi-Fiで通信中のみ

クライアント

Chrome(11.0.696.68)
Android端末のIPアドレスとポート番号を指定することでソケットで接続を試みる
接続後はサーバから送られてくるテキストをhtmlで垂れ流す

通常のSocketについて調べる

Socketはさんざん触ったことあるので、大丈夫。
勉強したい人はTECHSCOREがオススメ

ソケットネットワークプログラミング-TECHSCORE- 
http://legacy.techscore.com/tech/J2SE/Network/2.html

そういや本も一冊買ってた
Socketの他にStreamについても勉強になった

TCP/IPソケットプログラミング Java編

TCP/IPソケットプログラミング Java編

WebSocketを調べる

The WebSocket API 
http://www.w3.org/TR/2011/WD-websockets-20110419/

WebSocketの一次情報
draftと書かれているので、仕様が変わるかもしれないってヤツですね。
英語分からん。

W3C - 『The WebSocket API』日本語訳 - HTML5.JP 
http://www.html5.jp/trans/w3c_websockets.html

日本語訳バージョン
日本語分からん。

WebSocket - Wikipedia 
http://ja.wikipedia.org/wiki/WebSocket

Wikipedia
通常のSocketみたく、繋げたら何してもおっけーってわけではなく
ハンドシェイクというやりとりが必要という情報が

WebSocketのプロトコル ― ありえるえりあ 
http://dev.ariel-networks.com/Members/inoue/websocket/

1年くらい前の記事だけど
WebSocketについてまとまってる
ハンドシェイクのやり方も丁寧に解説してある
意味あんのかって突っ込んでるけどw

http://www.google.co.jp/codesearch/p?hl=ja#CwgqP48SYDw/trunk/shared/J2SE/jWebSocketServer/org/jwebsocket/netty/engines/NettyEngineHandler.java

google code searchでjavaでのサーバ側の実装を見つけた
constructHandShakeResponseというメソッドでごにょごにょしてる
一番参考になった。コードが一番分かりやすい。

jWebSocket - The Open Source Java WebSocket Server 
http://jwebsocket.org/

どうやらこれのソースみたいだ

HTML5 WebSockets Tutorial 
http://www.tutorialspoint.com/html5/html5_websocket.htm

クライアントサイドはここを参照
簡単すなぁ…

作った

適当な実装だけど動くものはできた

実行イメージ


なんかそれっぽいの見えてるよね!

試すには

http://tomorrowkey.googlecode.com/svn/tags/LogcatSocketServer/1.0.0/bin/LogcatSocketServer.apk
このアプリをAndroid端末にインストール(※クリックするとapkファイルがダウンロードされます)
アプリ起動するとIPアドレスとポート番号が表示されます。*1

http://tomorrowkey.googlecode.com/svn/trunk/LogcatOnBrowser/Client/logcat.html
同じLAN内につながってるパソコンにこのHTMLを保存する
ブラウザから保存したhtmlファイルにアクセスする*2
IPアドレスとポート番号を入れるテキストがあるので、アプリに表示されているものを入力
connectボタンを押せばlogcatが読み込まれます


動くようには作りましたが、けっこう適当実装です。
自己責任で実行してください。責任は一切負いません。

やってみて

javascriptとかhtmlとか普段ぜんぜんやらないから楽しかった!
今は情報がたくさんあって、知識がなくてもなんとなくで作れてしまうね
なんか実装の悪いところあったら指摘ください。
WebSocketはすごく楽しい。
可能性を感じる。

*1:Wi-Fiに接続していない場合、そのまま終了します

*2:ここでアラートが表示された場合、使っているブラウザがWebSocketに対応していません

9patchを覚えよう!

角丸が綺麗に表示されない!

ボタンを作りました!
角丸のボタンです。

早速ボタンの背景に設定しました。

なんてこったい
設定するボタンが大きすぎて背景画像が引き伸ばされてしまいます。

9patchを使おう!

そこで9patchの出番です。
9patchは、画像よりモノが大きかった場合(今回は画像よりボタンが大きかった)
引き伸ばす部分を指定することで、画像の崩れを無くす事ができるのです!
Fireworksでいう所の9スライスという機能に似ています。
先ほどのボタン画像に9patchの処理を施してみます。

できました!
上側と左側に黒い1ピクセルが見えますでしょうか?
ちょっと拡大します。

拡大したのでボケてますが、これなら分かりますね。
9patchは上下左右に、アルファ無しの黒(#000000)の印を付けることで伸ばす部分を指定します。
上下左右それぞれ意味があります。
上と左の印は必須です。
下と右の印は省略することができます。
下と右の印の説明は後でやります。

図解9patch

さきほどの画像を解説すると

上に指定したピクセルの列(赤)が

横に伸びる時にコピーされます。

左に指定したピクセルの行(青)が

縦に伸びる時にコピーされます。
こうやって角丸部分が拡大されないようにします。
端末に設定してみましょう。

綺麗に角丸が表示されました。
さらに、どの部分にコピーされているのか確認したいため、こんな画像も用意してみました。

端末で表示してみましょう。

なるほどー、さっきのサンプルみたいにコピーされました。

さらに挙動を調べる

通常9patchの上と左の指定は1ピクセルだけで済みますが、実際には複数ピクセル指定することができます。
端末で表示させて挙動をたしかめてみましょう。

たとえば2ピクセル指定した場合はどうなるんでしょう。
端末で表示して見てみます。

半分ずつコピーされました。
おもしろいですね。

次はこんな画像はどうでしょう。

各々近いところを補うんですかね。

さらに活用する

挙動が分かれば、それを利用します。

この画像を使って

こんな風に表示したり*1

この画像を使って

こんな風に表示したり*2

グラデーションなら左側をほとんど塗っちゃってもいいんじゃないでしょうか

段差が見えちゃってますが、多少の拡大ならごまかせます。


ベタ塗りなのにこういう9patchの指定の仕方をしている人がいて、本当に理解しているのかなーって心配になります…。
ちゃんと表示はされるんだけどね…。

文字が変な位置に表示される

ボタンにはラベルが必要です。
ボタンを押した時にどんな機能が働くのかを、ラベルの文字によって表現しましょう。
今回は"こんにちは!"というラベルにします。
きっと押したら"ぽぽぽぽーん!"と音が鳴るに違いありません。
さっき作ったボタンにラベルを表示するよう変更してみます。

こんにちは!って表示されたけど、位置がおかしいです。
下に表示しているデフォルトのボタンではセンタリングされているのに、今回作ったボタンは変な位置に表示されています。

さらに9patch

これも9patchの機能を使って解決します。
上と左の印を使う事によって伸ばす部分を指定しましたが、
下と右の印を使う事によってコンテンツの位置を指定することができます。
ボタンの画像に対してコンテンツの位置を指定してみます。

できました!
例によって拡大します。

下と右に黒いピクセルがたくさんあるかと思います。
解説すると

下の黒いピクセルの列(赤)と
右の黒いピクセルの行(青)の
交わった場所(紫)がコンテンツの入る場所になります。
今回だと文字列の入る場所です。
端末で表示してみましょう。

ちゃんとセンタリングされました。
さっきの赤/青の画像でも表示してみましょう。

ちゃんと紫のところにラベルが表示されています。

さらに挙動をしらべる

調べようかと思ったのですが
何もできません。
下と右はこれですべてみたいです。

例えばこんな画像を用意して端末で表示してみようかと思ったのですが
eclipseさんに怒られてしまい、コンパイルが通りません。

さらに活用する


この画像を使って

こんな風に表示したり

この画像を使って

こんな風に表示したりできます。

9patchが苦手な画像

9patchには苦手な画像があります。
たとえば

ストライプ*3

グラデーション
例に出しましたが、段差が見えてしまうのでオススメできません。

その他連続した柄*4
他にもいろいろパターンはあると思うので、想像しながら作りましょう。
AndroidSDKの中に入ってるdraw9patchというツールを使うと、どういう風に表示されるか見ながら9patchのマークを付ける事ができます。
慣れないうちはそれを使うのも手でしょう。*5

さらに9patchを理解する

読むだけだと完全に理解はできません。
これ以上は実際に作って、端末に表示させてみて、挙動を自分で確かめてみるといいと思います。

その他9patch記事

Draw 9-patch | Android Developers 
http://developer.android.com/guide/developing/tools/draw9patch.html
公式の9patch解説
Draw 9-patch - ソフトウェア技術ドキュメントを勝手に翻訳
https://sites.google.com/a/techdoctranslator.com/jp/android/developing/tools/draw9patch
公式の9patch解説の日本語翻訳
9-patch - 3156note 
https://sites.google.com/site/3156note/home/android/9-patch
draw9patchの使い方を詳しく解説
チュートリアル:9patchで画像を作る « Tech Booster 
http://techbooster.jpn.org/andriod/environment/3996/
draw9patchの使い方を詳しく解説
draw9patch で NinePatch をつくる方法 - Hacking My Way 〜 itogのhack日記
http://d.hatena.ne.jp/itog/20100209/1265684439
draw9patchの使い方を詳しく解説

まとめ

  • 単純に拡大されたくない時は9patchを使う。
  • コピーされる場所を指定するには上と左に9patchのマークをつける。
  • 中に何かコンテンツを入れる時は下と右に9patchのマークをつける。
  • どんな画像になるか想像しながら作る。

*1:xmlで指定することにより、ボタンとアイコン(ハート、星)を分離することができます。分離できた方がデザイナさんもプログラマーさんも幸せなので、そちらを使った方がいいです。

*2:xmlで指定することにより、ボタンとアイコン(ハート、星)を分離することができます。分離できた方がデザイナさんもプログラマーさんも幸せなので、そちらを使った方がいいです。

*3:9patchではなく、これを使うと解決できるかもしれません http://developer.android.com/guide/topics/resources/drawable-resource.html#Bitmap

*4:9patchではなく、これを使うと解決できるかもしれません http://developer.android.com/guide/topics/resources/drawable-resource.html#Bitmap

*5:個人的にはすごく使いづらいのでFireworksで全部描いちゃってます

キーリピートを実装する

Buttonクラスを拡張して、長押しされている場合クリック動作を呼び出すようにします。

RepeatButton.java
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.widget.Button;

public class RepeatButton extends Button implements OnLongClickListener {

  /**
   * 連続してボタンを押す間隔(ms)
   */
  private static final int REPEAT_INTERVAL = 100;

  /**
   * 連打フラグ
   */
  private boolean isContinue = true;

  /**
   * ハンドラ
   */
  private Handler handler;

  public RepeatButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    setOnLongClickListener(this);
    handler = new Handler();
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);

    // キーから指が離されたら連打をオフにする
    if (event.getAction() == MotionEvent.ACTION_UP) {
      isContinue = false;
    }

    return true;
  }

  @Override
  public boolean onLongClick(View v) {
    isContinue = true;

    // 長押しをきっかけに連打を開始する
    handler.post(repeatRunnable);

    return true;
  }

  Runnable repeatRunnable = new Runnable() {
    @Override
    public void run() {
      // 連打フラグをみて処理を続けるか判断する
      if (!isContinue) {
        return;
      }

      // クリック処理を実行する
      performClick();

      // 連打間隔を過ぎた後に、再び自分を呼び出す
      handler.postDelayed(this, REPEAT_INTERVAL);
    }
  };
}

使用例

こんな感じで使えばNumberPicker的に使えます

イメージ

MainActivity.java
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener {

  private TextView txtNumber;

  private RepeatButton btnIncrement;

  private RepeatButton btnDecrement;

  private int number = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    txtNumber = (TextView)findViewById(R.id.txtNumber);
    txtNumber.setText(String.valueOf(number));
    btnIncrement = (RepeatButton)findViewById(R.id.btnIncrement);
    btnIncrement.setOnClickListener(this);
    btnDecrement = (RepeatButton)findViewById(R.id.btnDecrement);
    btnDecrement.setOnClickListener(this);
  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
      case R.id.btnIncrement: {
        increment();
        break;
      }
      case R.id.btnDecrement: {
        decrement();
        break;
      }
    }
  }

  private void increment() {
    number++;
    txtNumber.setText(String.valueOf(number));
  }

  private void decrement() {
    number--;
    txtNumber.setText(String.valueOf(number));
  }
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:gravity="center_vertical|center_horizontal"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <view
    class="jp.tomorrowkey.android.repeatpushbutton.RepeatButton"
    android:id="@+id/btnIncrement"
    android:text="Increment"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  <TextView
    android:id="@+id/txtNumber"
    android:text="0"
    android:textSize="32sp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  <view
    class="jp.tomorrowkey.android.repeatpushbutton.RepeatButton"
    android:id="@+id/btnDecrement"
    android:text="Decrement"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
</LinearLayout>

独自Viewの定義の仕方

独自ViewのXMLの定義の仕方は2つあります

viewタグを使う場合
<view
  class="jp.tomorrowkey.android.repeatpushbutton.RepeatButton"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />
クラス名でタグを書く場合
<jp.tomorrowkey.android.repeatpushbutton.RepeatButton
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />

サンプルソースではクラス名をタグに書くパターンをよく見ます。
2つの違いはというと
viewタグを使う場合ではviewのもっている属性名(android:layout_widthやandroid:idなど)を補完してくれます。
機能的に違いはないのでviewタグを使った方がコーディングは楽です。

Androidで縦書きを実現する

androidのTextViewは縦書きには対応していません。
縦書きを実現するためには自分で実装するしかありません。
縦書きについてさっぱり知らない状態から実装しました。
twitterでのやりとり

Togetter - 「Android縦書き」 
http://togetter.com/li/92001

誤っている箇所があれば指摘お願いします!
完成はこんな感じです。

参考にしたサイト

字体 - Wikipedia 
http://ja.wikipedia.org/wiki/%E5%AD%97%E4%BD%93

グリフがフォント1文字分、グリフがたくさん集まってフォントという単位になる

禁則処理 - Wikipedia 
http://ja.wikipedia.org/wiki/%E7%A6%81%E5%89%87%E5%87%A6%E7%90%86

行の最初に「、」「。」などの文字が配置される場合、前行の最後に表示するべきという処理
*1

ハイフネーションとは【hyphenation】 - 意味/解説/説明/定義 : IT用語辞典 
http://e-words.jp/w/E3838FE382A4E38395E3838DE383BCE382B7E383A7E383B3.html

英語版禁則処理みたいなもの
行をまたいで英単語を描画する場合ハイフンを使って繋ぐ
*2

ルビ - Wikipedia
http://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%93

ふりがなに使うルビ
*3

[SOLVED] Android: Using Custom Fonts « RussenReaktor's Weblog 
http://russenreaktor.wordpress.com/2010/04/29/solved-android-using-custom-fonts/

自分で用意したフォントを使う例
assetsには1MBまでしか入りません。この例ではassetsにフォントを入れていますが、これは英字フォントのみだからこそできることです。
日本語のフォントが入っていてると1MBなんて余裕で超えちゃうので別途SDカードに入れてあげる必要があります。
他のAndroid縦書きアプリでは大抵アプリインストール後にダウンロードする仕組みになっています。

テキストの描画(FontMetrics) - Android Wiki* 
http://wikiwiki.jp/android/?%A5%C6%A5%AD%A5%B9%A5%C8%A4%CE%C9%C1%B2%E8(FontMetrics)

FontMetrixについて
Canvas.drawText()ではbaseLineを基準に描かれます。

簡易禁則入ってるソース - inuchin104がiPhoneと格闘したメモ 
http://d.hatena.ne.jp/inuchin104/20110121

ちゃんと読めていませんが、
[twitter:@inuchin] さんの作ったDirectXを使ったiPhone用縦書きコードらしいです。
ルビと簡易禁則処理がついているそうです。

Typeface | Android Developers 
http://developer.android.com/reference/android/graphics/Typeface.html

フォントのクラスです。

Paint | Android Developers 
http://developer.android.com/reference/android/graphics/Paint.html

getFontから始まるメソッドを見ておくといいです。

基本的な作り

独自Viewを作り、onDrawでCanvas#drawText()を使って文字を縦に並べていきます。
ひらがなや、漢字などのだいたいの文字の表示は大丈夫ですが、横書き用フォントなので「ー」や「、」や「ぁ」などを使った場合表示が狂った様に見えます。特別な小文字や記号などには別途設定を設けて位置を調整するようにします。
横フォント問題の解決法は縦書きビューワを作られている[twitter:@2SC1815J] さんにもご意見頂きました。

今回は横フォントの位置を調整する方法をとってみようかと思います。

実装

独自フォントのロード
Typeface typeface = Typeface.createFromFile(FONT_PATH);
Paintm paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(FONT_SIZE);
paint.setColor(Color.BLACK);
paint.setTypeface(typeface);
canvas.drawText("こんにちは!", x, y, paint);


このコードを使えば独自フォントを使った文字列を表示する事ができます。
フォントはIPA明朝を使用しました。

IPAフォントのダウンロード || OSS iPedia 
http://ossipedia.ipa.go.jp/ipafont/index.html

こんにちはの文字は横に表示されていますが、縦書きにするために1文字ずつ描画します。

文字サイズ

テキストを縦に並べるためには1文字のサイズを取得する必要があります。
文字サイズはPaint#getFontSpacing()でピクセルサイズを取得する事ができます。
これを使えば固定ピッチで表示する事が可能です。
行間を1文字分、最初の行を1文字落とす設定で走れメロスの冒頭を表示してみました。

この状態でもけっこう様になってます。
しかし「、」「。」「っ」の位置がおかしいです。
横用フォントのため、位置がおかしく表示されます。

文字位置を調整する

文字位置調整の対象はだいたい思いつくもので

  • 記号
  • ひらがな小文字
  • カタカナ小文字
  • アルファベット半角大文字
  • アルファベット半角小文字
  • アルファベット全角大文字
  • アルファベット全角小文字

などがあります。
ひらがな・カタカナ小文字については位置の調整でいいですが、
アルファベット・記号については90度回転する必要があります。
管理が楽になるように設定用のクラスを作っておいてexcelで生成できるようにします。

public class CharSetting {
  /**
   * 文字
   */
  public final String charcter;

  /**
   * 回転角度
   */
  public final float angle;

  /**
   * xの差分
   * Paint#getFontSpacing() * xが足される
   * -0.5fが設定された場合、1/2文字分左にずれる
   */
  public final float x;

  /**
   * xの差分
   * Paint#getFontSpacing() * yが足される
   * -0.5fが設定された場合、1/2文字分上にずれる
   */
  public final float y;

  public CharSetting(String charcter, float angle, float x, float y) {
    super();
    this.charcter = charcter;
    this.angle = angle;
    this.x = x;
    this.y = y;
  }

  public static final CharSetting[] settings = {
      new CharSettings("、", 0.0f, 0.7f, -0.6f), new CharSettings("。", 0.0f, 0.7f, -0.6f),
      new CharSettings("「", 90.0f, -1.0f, -0.3f), new CharSettings("」", 90.0f, -1.0f, 0.0f),
      new CharSettings("ぁ", 0.0f, 0.1f, -0.1f), new CharSettings("ぃ", 0.0f, 0.1f, -0.1f),
      new CharSettings("ぅ", 0.0f, 0.1f, -0.1f), new CharSettings("ぇ", 0.0f, 0.1f, -0.1f),
      new CharSettings("ぉ", 0.0f, 0.1f, -0.1f), new CharSettings("っ", 0.0f, 0.1f, -0.1f),
      // 略
      new CharSettings("V", 90.0f, 0.0f, -0.1f), new CharSettings("W", 90.0f, 0.0f, -0.1f),
      new CharSettings("X", 90.0f, 0.0f, -0.1f), new CharSettings("Y", 90.0f, 0.0f, -0.1f),
      new CharSettings("Z", 90.0f, 0.0f, -0.1f), new CharSettings(":", 90.0f, 0.0f, -0.1f),
      new CharSettings(";", 90.0f, 0.0f, -0.1f), new CharSettings("/", 90.0f, 0.0f, -0.1f),
      new CharSettings("", 90.0f, 0.0f, -0.1f), new CharSettings(":", 90.0f, 0.0f, -0.1f),
      new CharSettings(";", 90.0f, 0.0f, -0.1f), new CharSettings("/", 90.0f, 0.0f, -0.1f),
      new CharSettings(".", 90.0f, 0.0f, -0.1f),
  };

  public static CharSetting getSetting(String character) {
    for (int i = 0; i < settings.length; i++) {
      if (settings[i].charcter.equals(character)) {
        return settings[i];
      }
    }
    return null;
  }
}

文字を描画する前にCharSetting#getSetting()を呼び、設定があったらその設定通りに描画します。
nullが帰ってきた場合、回転も位置調整もせずに描画します。
コードで表すとこんな感じ

CharSettings setting = CharSettings.getSetting(s);
if (setting == null) {
  canvas.drawText(s, x, y, paint);
} else {
  canvas.save();
  canvas.rotate(setting.angle, x, y);
  canvas.drawText(s, x + fontSpacing * setting.x, y + fontSpacing * setting.y, paint);
  canvas.restore();
}

また、CharSettingsをたくさん生成しているところはexcelで管理して生成できるようにしました。

CharSettings作成シート
https://spreadsheets.google.com/ccc?key=0AmA_8rkF2TwodHIyUWpMdldicVhhMGxZWVItNmQ1UkE&hl=ja&authkey=CK_XjqsK

これを使って描画したのがこれ

がんばって調整したのでそれっぽい位置に「、」や「っ」が描画されました。

android縦書き完成

あとは表示位置がおかしい文字の設定を随時追加していったり、
行末に「、」「。」などがくるよう禁則処理したり、
ルビの表示を実装したりすればいいと思います。
特にルビはデータをどのように保持するか、またパースのやり方を考えないといけないので大変かもしれません。

*1:今回は実装していません

*2:今回は実装していません

*3:今回は実装していません

*4:事前にSDカード内にフォントを用意しないとエラーで落ちます