Unmotivated

やる気はない

Android のタッチイベントを理解する(その2)

Activity にタッチイベントが流れてくるまで

前回は View のヒエラルキーのなかをどうやってタッチイベントが伝搬するかを追いました。
参考にした資料には、 Activity#dispatchTouchEvent() からイベントが始まるとありますが、折角なのでそこまではどうなっているかも確認します。

先に概要としてまとめてしまうとこんな雰囲気でした。

Activity に渡るまでのタッチイベント概要

View システムの根っこの部分をきちんと理解できてないので、勘違いがあるかもしれません。 (親子関係が特に怪しい)

Android のアニメーションを自作する

Android で独自定義のカスタムアニメーションを作りたかったので試してみたところ、 Animation クラスを継承して割と簡単に作れたのでメモ。

ここでは例として、 View を円周に沿って動かすようなアニメーションを作ってみました。
アニメーションさせたい View に対して中心点を指定して、開始角度と終了角度を与えてアニメーションさせてみます。

android-arc-translate-animation.png

1
2
3
4
5
6
7
8
9
10
11
/**
 * 円弧上に沿うように移動させるアニメーション
 * @param startDegrees  開始角度
 * @param endDegrees    終了角度
 * @param centerXType   中心点のX座標のタイプ
 * @param centerXValue  中心点のX座標を表す値
 * @param centerYType   中心点のY座標のタイプ
 * @param centerYValue  中心点のY座標を表す値
 */
public ArcTranslateAnimation(int startDegrees, int endDegrees,
        int centerXType, float centerXValue, int centerYType, float centerYValue);

ファイル名に特定の記号が使用できる場合とできない場合がある

Android のバージョンは同じなのに、端末によってファイルの移動(renameTo())に失敗してしまうことがあって悩まされたが、オチは移動先のファイル名にクエスチョンマーク(“?”)が含まれていたことが原因だった。

クエスチョンマークが入ってしまっていたこと自体がバグだったのだけれど、そもそも何で端末によってこのようなことが起きるのかを軽く追ってみた。

原因はファイルシステム(っぽい)

成功する端末では内部ストレージに書き込みを行っており、失敗する端末では外部ストレージに書き込みを行っていた。

ファイルシステムを見てみると、内部ストレージは FUSE でマウントされているのに対し、外部ストレージは VFAT でマウントされていた。

mount の状態
1
2
3
4
5
# 内部ストレージとして認識されているディレクトリ
/dev/fuse /storage/sdcard0 fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

# 外部ストレージとして認識されているディレクトリ
/dev/block/vold/179:33 /storage/sdcard0/external_sd vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0602,dmask=0602,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0

正式な出典が見つからなかったけれど、 VFAT ではファイル名に以下の文字の使用を禁止しているようなので、恐らくこれが原因っぽい。
試しにその他の記号を含めたファイル名を作成してみたところ、全て失敗した。

1
\ : * ? < > | / ;

参考 : ファイル名とフォルダ名で使用できない文字

というわけで、環境によって(サンプル数は少ないが、恐らく外部SDカードでは)一部の記号がファイル名に使えなくなることがある、ということが分かった。

メモ

Android のファイルシステム周りについては全く調べられていないが、いくつか気になることがあったのでメモ。

  • エミュレータでは FUSE ではなく YAFFS2 でマウントされていた(どこのレイヤーで変わってるのか不明)
  • ここを見ると、 Android 4.4 からは外部ストレージも FUSE 経由でマウントするようになるっぽい?が、4.4 のエミュレータが起動せず、実機も外部メモリがマウントできない端末しかなく確認できず (raw storage をラップしているとあるので、今回の件は実体の方でこけそう)

ListView#setEmptyView() について

ListView の要素が無い場合に表示する View を指定できる setEmptyView() だが、その名前から想像できるのとは少し違う動きをする。

  • 違 : 「要素が無い場合に ListView の子供として表示させる View が設定できる」
  • 正 : 「要素が無い場合に ListView の代わりに表示する View が設定できる」

実際の動作は setEmptyView() で指定した View と ListView の Visibility を View.GONE と View.VISIBLE で入れ替えるだけとなっている。

そのため、 inflate() しただけでどこにも addView() していない View などを setEmptyView() で指定しても表示されないし、 ListView の子として表示される Header や Footer も表示されないこととなる。

通常は ListView と同階層に兄弟 View として xml に定義しておくのがよさそう。
EmptyView として設定する View の Visibility は特に指定しなくても ListView 側でやってくれるので問題ないです。

1
2
3
4
5
6
7
<ListView
    android:id="@+id/listView"
    ... />

<View
    android:id="@+id/emptyView"
    ... />
1
2
ListView listView = (ListView) findViewById(R.id.listView);
listView.setEmptyView(findViewById(R.id.emptyView));

以下は実装箇所の確認。

android.widget.AdapterView(1.6_r2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void More ...updateEmptyStatus(boolean empty) {
    if (isInFilterMode()) {
        empty = false;
    }

    if (empty) {
        // EmptyView が設定されていた場合は自身を消して、 EmptyView を表示させている
        if (mEmptyView != null) {
            mEmptyView.setVisibility(View.VISIBLE);
            setVisibility(View.GONE);
        } else {
            // If the caller just removed our empty view, make sure the list view is visible
            setVisibility(View.VISIBLE);
        }
        // ...

    } else {
        // ...
    }
}

Android のタッチイベントを理解する(その1)

タッチイベントがうまく流れてこなくて困ったり、自力でイベントをルーティングしたりするときに困ったりと、ちょこちょことタッチイベントについて勉強したのでまとめておきます。
主にタッチイベントがどう流れてどう止まるかなどについて調べています。

イベントの流れを理解するには以下の資料がかなり参考になりました。

毎度のことながら、間違いがありましたらご指摘頂ければ幸いです。

Octopress + Disqus でコメントを表示しようとしたがうまくいかなかった話

Octopress ルートディレクトリの _config.xml の disqus_short_name に short_name を書き込むだけで Disqus と連携されてコメントモジュールが表示される。

1
disqus_short_name: <short_name>

ところが以下のエラーが表示されてしまい、コメントモジュールがロードされない。

1
We were unable to load Disqus. If you are a moderator please see our troubleshooting guide.

オチは Disqus の short_name を理解していなかったのが原因で、ずっと Disqus ID を disqus_short_name に書き込んでいたのが問題だった。 # Disqus 使ったこと無かった上に説明を読んでなかった

Disqus では、コメントを投稿する単位としてまず Site を登録する必要があり、この Site の識別子として使われるのが short_name だった。 Disqus にログインした状態でここにアクセスして Site を登録したところ、無事に short_name が生成された。

StackTrace を文字列として取得する

Java において StackTrace を取得する。
どこかに仕込んでおくとデバッグ時に何かと便利。

1
2
3
4
5
6
7
8
9
public static String getStackTrace() {
    StackTraceElement[] stacks = new Throwable().getStackTrace();
    StringBuilder sb = new StringBuilder();
    for(StackTraceElement e : stacks) {
        if(e == stacks[0]) continue;
        sb.append(String.format("\tat %s(%s:%s)", e.getMethodName(), e.getFileName(), e.getLineNumber()));
    }
    return sb.toString();
}

ViewGroup では onDraw() の代わりに dispatchDraw() が呼ばれる

FrameLayout で onDraw() のタイミングで処理をしたいが、どうも onDraw() が呼ばれないように見える。

確認したところ、 ViewGroup を継承したクラスは onDraw() の代わり dispatchDraw() が呼ばれるようだった。
(その名の通り子の View に対して draw() をコールしたりしなかったりするメソッド)

ViewGroup を継承したクラスに対して onDraw() のタイミングで実行したい処理は、 dispatchDraw() に書けば問題なさそう。

1
2
3
4
5
6
7
@Override
protected void dispatchDraw(Canvas canvas) {
    /*
        canvas に対して処理をしたりなんだり...
    */
    super.dispatchDraw(canvas);
}

Octopress の Preview は Generate を兼ねる

今まで気がつかなかったけれど、わざわざ generate してから preview しなくても、 preview 中に source ファイルを書き換えれば勝手にそれを検知して regenerate してくれるっぽい。

1
2
3
4
5
$ rake preview
...
>>> compass is watching for changes. press ctrl-c to stop.
<ここでファイルを編集>
[2013-11-25 22:35:05] regeneration: 1 files changed

これは便利! ちなみに新規ファイルを保存してもきちんと捕足して regenerate してくれるみたい。
投稿のハードルを限りなく低くしたいと思っている身からは、preview を立てっぱなしに出来るのは捗りそう。

Android 4.3 で ImageView#getImageMatrix() の挙動が変わっている

今まで ImageView#getImageMatrix() で取得した Matrix のインスタンスに対して、直接 post*() のメソッドを呼んだりして操作すれば表示に反映されていたが、どうやら 4.3 で挙動が変わったご様子。

結論から言うと、横着せずにちゃんと毎回 setImageMatrix() をしてやるか、はじめに setImageMatrix() してから getImageMatrix() を使うようにすれば想定通りに動作する。

1
2
3
4
5
6
7
ImageView imageView = findViewById(R.id.imageView);

float ratio = 2.0f;
Matrix matrix = imageView.getImageMatrix();
matrix.postScale(ratio, ratio);

matrix.setImageMatrix(matrix);
1
2
3
4
5
6
7
8
ImageView imageView = findViewById(R.id.imageView);

Matrix matrix = new Matrix();
imageView.setImageMatrix();

float ratio = 2.0f;
matrix = imageView.getImageMatrix();
matrix.postScale(ratio, ratio);

どう変わってるのか確認

4.2 以前では mMatrix (initImageView()で初期化されてる)がそのまま返っているのに対して、 4.3 以降では mDrawMatrix が返るようになっている。 (mDrawMatrix は configureBounds() でセットされている)

mDrawMatrix がセットされていない状態だと、その場で Matrix のインスタンスが作られているため、このインスタンスに対して直接操作を行っても反映されないというわけでした。

4.2_r1 android.widget.ImageView#getImageMatrix()link
1
2
3
public Matrix getImageMatrix() {
    return mMatrix;
}
4.3_r1 android.widget.ImageView#getImageMatrix()link
1
2
3
4
5
6
public Matrix getImageMatrix() {
    if (mDrawMatrix == null) {
        return new Matrix(Matrix.IDENTITY_MATRIX);
    }
    return mDrawMatrix;
}

Android はマイナーバージョンアップでも、結構こういう細かい挙動変更が入ってくるので、以前動いていたものが突然動かなくなることが良くあるのだけれど、Android のコードを直接見に行けば答えが書いてあるので安心感がありますね。