Canvas#drawTextするときのメモ

どの値が何を表しているかよく忘れるのでメモ

原点


Canvas#drawText() に指定する座標はここになる。


文字の幅を取得するにはPaint#measureText(:String):floatを使う。

高さ

Top 一番上
Ascent 文字の上限
Leading 原点のyと同じ位置
Descent 文字の下限
Bottom 一番下

文字のそれぞれのy位置が取得できる。
Paint#getFontMetrics()で取得できるFontMetrixから、フィールドの値を取り出せる。
bottom - topをすれば高さが取得できる。

値(追記)

Paint.setTextSize(:int)に96を設定した場合のそれぞれの値
leadingは必ず0になる
他はleadingからの相対的な位置の差

top -100.59375
ascent -89.109375
leading 0.0
descent 22.640625
bottom 26.015625

ブラウザからlogcatを見る(アップデートしました)

前回記事

ブラウザからlogcatを見る - 明日の鍵 
http://d.hatena.ne.jp/tomorrowkey/20110514/1305379371

前回作ったブラウザから見るlogcatをデ部にて発表しました。

ブラウザからlogcatを見る
http://d.hatena.ne.jp/tomorrowkey/files/20111001_slide.pdf

発表ついでに以下を変更しました。

  • 外観を整えました。
  • 普通のSocketにも対応しました。

実行ファイルたち

Androidアプリ(Server)
http://tomorrowkey.googlecode.com/svn/trunk/LogcatOnBrowser/LogcatSocketServer/bin/LogcatSocketServer.apk
・jarファイル(Client)
http://tomorrowkey.googlecode.com/svn/trunk/LogcatOnBrowser/LogcatSocketClient/jar/wifilogcat.jar
・htmlファイル(Client)
http://tomorrowkey.googlecode.com/svn/trunk/LogcatOnBrowser/Client/logcat.html

WebSocketを使ったバージョンはそのうち使えなくなります。
すでにchromeでは動かなくなりました。
現在動作確認がとれているのはMacSafariのみです。

Socketで接続する場合、引数にIPアドレスとポート番号を指定してあげる必要があります。
コマンド例は以下です。

java -jar wifilogcat.jar -ipaddress 192.168.0.2 -port 10008


コマンドラインで実行なので、grepできたりcoloredlogcatが使えたりしてさらにべんり!!

Androidで簡単にWi-Fiに接続する方法


Wi-Fiの設定ってめんどうですね。
長く複雑なパスワードをandroidで入力するのは、正直ストレスです。


ZXingのソースコードを読んでいたのですが、Wi-Fiに接続するための機能がついていました。
ZXingはバーコードをエンコード/デコードするためのライブラリです。

zxing - Multi-format 1D/2D barcode image processing library with clients for Android, Java - Google Project Hosting 
http://code.google.com/p/zxing/


ZXingはさまざまなプラットフォーム向けにクライアントが作られています。
Android向けにはQRコードスキャナーという名前で公開されています。

QRコードスキャナー - Android マーケット 
https://market.android.com/details?id=com.google.zxing.client.android&feature=search_result


Wi-Fiに接続するためのQRコードは、このサイトで作れます。
もちろんこれもZXingのプロジェクトの一部です。

QR Code Generator 
http://zxing.appspot.com/generator/

ここでQRコードを作って印刷しておくと便利ですね。

android国際化メモ

フォーマット

すごいよ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が少なくなり、すっきり。

所感

やっと書き方覚えたぞ