月別: 2016年2月

式の即時評価を利用してオブジェクトの状態を調べる

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

式の即時評価が便利だよねという話です。

私は以前Calendarクラスを使って日付の処理をしようとしていました。そのとき、どのフィールドを参照すれば目的の値が引っ張ってこれるかを確認するのに、愚直にLog.d()を使っていました。1つ1つメソッドの返り値を出力して(文字列の連結でさらにカオスになる)、目的の数値がちゃんと取れているのか確認していたのです。

ドキュメントを読めよっていう話なんですが、読んでもどういう値が取れるのかいまいち分からなかったんですよね・・・。

まあそんなアホなことをやっていたので、当然のようにバグを仕込んでいました。そんなバグに気づくきっかけとなったのが式の即時評価機能です。以来、とてもお世話になっています。

式の即時評価を使えば、ブレークポイントを設定してデバッグ実行するだけで、任意のメソッドや変数の確認ができるようになります。

式の即時評価

メソッド呼び出しとその結果が確認できる

ブレークポイントで一時停止させないと使えないので、状態の変化を追うのには向かないかもしれません。それでもlogcat頼みのデバッグより捗る場面があると思います。

どんなときに便利か

今日遭遇したエラーで、なんかのタイミングでNullPointerExceptionが発生してクラッシュする現象が発生しました。

特定の状況で例外発生によるクラッシュ

例外の発生する箇所はわかっているものの、どういう状況でそれが生じているのかがよく分かりませんでした。

そこで例外の発生する部分をtry-catch文で囲み、例外をキャッチした所にブレークポイントを置いて調べてみることにしました。

try-catchで止めてみる

ブレークポイントで止めればコールスタックを遡ってオブジェクトの状態を確認できますが、目当ての変数を探すのが大変なので、そういうときに式の即時評価が便利です。だと思います。

画面をタッチして線を描く お絵かきアプリを作るための第一歩

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

今回の記事のサンプルコードは、GitHubで公開しています。

お絵かきアプリを作ろうと思って格闘中です。とりあえず線を描くだけでも学びがいろいろあったのでまとめておこうと思います。

Pathを使って描画するとカクカクする問題

線を描くにはPathを使うのがオーソドックスのようですが、何も考えずにパスを使った描画を行うと、線がカクカクしてしまいます。(サンプルコードのPathPaintView)

path.lineTo(e.getX(), e.getY());
drawCanvas.drawPath(path, paint);

path.lineToによる描画

これはなぜ起こるのでしょうか。

MotionEventが配信される間隔の問題

その理由はまず線をPathではなく点で描画してみると分かります。(DotPaintView)

drawCanvas.drawPoint(e.getX(), e.getY(), paint);

drawPointによる描画

描画される点がまばらになっています。このドットはonTouch()が呼ばれるタイミングで描画されています。このドットの間隔がタッチイベントがViewに伝えられているタイミングだということです。これはスクリーンをタッチした情報が、逐一間断なくonTouch()に渡されているわけではないことを意味しています。

Historical情報を利用する

ではドットとドットの間のタッチイベントの情報は失われているのかというと、決してそうではありません。onTouchに渡されるMotionEventには、MotionEventが配信されていない時に生じた座標を保持しています。

その情報はMotionEvent.getHistoricalX()などで取得することができます。これを利用すれば、MotionEventの情報をより精細に取得することができます。(HistoricalDotPaintView)

int history = e.getHistorySize();
for (int h = 0; h < history; h++){
drawCanvas.drawPoint(e.getHistoricalX(h), e.getHistoricalY(h), paint);
}
drawCanvas.drawPoint(e.getX(), e.getY(), paint);

getHistoricalによる描画

ドットの間隔が狭まりました。指をゆっくり動かせばキレイな線が描画できます。しかしこのHistorical情報にも限度があり、指を少しでも早く動かすとやはり間隔が空いてしまいます。

Historical情報を使ってPathによる描画を行う

Historical情報を利用すれば、精度の高い座標情報を取得できることが分かりました。この座標情報をPathによる描画で利用してみます。(HistoricalPathPaintView)

int history = e.getHistorySize();
for (int h = 0; h < history; h++){
path.lineTo(e.getHistoricalX(h), e.getHistoricalY(h));
}
path.lineTo(e.getX(), e.getY());
drawCanvas.drawPath(path, paint);

Device 2016 02 13 163849

単にpath.lineTo(x, y)で描画した時に比べると随分なめらかになりました。しかし、高速で動かしたらやっぱりカクカクしてしまうのは避けられません。なぜならpath.lineTo()による描画は、HistoricalDotPaintViewで描画した点と点の間を直線で結んでいるにすぎないからです。

これを解決するには、点と点の間をなめらかな曲線で結べば解決できそうです。

ベジェ曲線を利用する

ベジェ曲線によりスムーズな線をひく方法はいろいろ考えられるでしょう。1つの方法としてこんなやり方ができます。(BezierPathPaintView)

private void onTouchMove(MotionEvent e){
float midX = (previousX + e.getX()) / 2;
float midY = (previousY + e.getY()) / 2;
path.quadTo(previousX, previousY, midX, midY);
previousX = e.getX();
previousY = e.getY();
}

自分で作っておきながら分かりやすく説明できないのですが、この処理のポイントは3つです。

  • 前回のMotionEventで配信された座標点を記憶すること
  • 前回の座標と今回の座標の中間点を計算すること
  • 前回の座標を調整点とする、前回の中間点から今回の中間点までの2次ベジェ曲線を描く

この方法では、正確にタッチした通りの線が描けるわけではないのですが、比較的簡単な処理でカクカクしない線を描くことができます。

ベジェ曲線による描画

Historical情報を利用してやれば、更に精度の高い線が描けるでしょう。

ちなみにpath.quadTo(x1, y1, x2, y2)は、(x1,y1)の座標が制御点で、後半の(x2,y2)の座標が終端になります。始点はpathが持っている最後の座標になります。つまり前に描画したpath.quadToの終点が次の描画の始点になるということです。

サンプルはGitHub

今回の記事のサンプルコードは、GitHubで公開しています。

Android Studio 2.0 beta 4を使って作っています。古いバージョンのAndroid Studioを利用している場合は、build.gradleのclasspath 'com.android.tools.build:gradle:2.0.0-beta4'をお使いの環境に合わせて修正すれば動くと思います。

単に線を描くだけでもなかなか奥が深いです。

DataBindingを試す

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

DataBindingがアツいらしいと聞いて試してみました。簡単な使い方をするなら想像以上に簡単でした。

今までActivityなどでfindViewByIdを書きたくないから、ButterKnifeをどのプロジェクトでも使っていたのですが、DataBindingを使えば同じようなことができます。

両者を使ってみて感じたのは、ButterKnifeがレイアウトXMLをJavaコードに持ってくるイメージであるとすれば、DataBindingはJavaコードをレイアウトXMLに持っていくイメージであるということです。

DataBindingを使うことで、Javaで作成したコードを、レイアウトXMLに埋め込むことができるようになります。レイアウトXMLでどのデータを使うか指定しておけば、Activityで「このクラス(のインスタンス)を使ってくれ」と指定するだけでその内容を表示できたりします。

具体的な使い方はData Binding Guide – Android Developersを参照してください。

DataBindingを使う設定

Android Studio 1.3以上であることが必須です。

Android Gradle Plugin 1.5.0-alpha1以上を使っていることが必須、でした。

Android Studio 2.0 betaになると、コード補完のサポートがより強力になってます。

build.gradleでDataBindingの設定を有効にすることで利用できます。

android {
....
dataBinding {
enabled = true
}
}

表示するデータを保持するクラスの作成

“`public class Character{
public String name;
public int age;
public String skill;

<pre><code>public Character(String name, int age, String skill){
this.name = name;
this.age = age;
this.skill = skill;
}
</code></pre>

}“`

DataBindingを使ってアクセスするには、publicなフィールドであるか、privateなフィールドである場合publicなgetterがあることが必須です。

レイアウトXML

“`
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>

<pre><code><data>
<variable
name="chara"
type="jp.gcreate.sample.databinding.Character"
/>
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="jp.gcreate.sample.databinding.MainActivity"
>

<TextView
android:id="@+id/chara_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{chara.name}"
/>

<TextView
android:id="@+id/chara_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(chara.age)}"
/>

<TextView
android:id="@+id/chara_skill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{chara.skill}"
/>

</LinearLayout>
</code></pre>

</layout>“`

ポイントはこんな感じ。

  • レイアウトXMLを<layout>タグで囲む
  • レイアウトXML内で利用するクラスを<data>タグ内で宣言する
  • <data>タグ内で宣言するクラス名は完全修飾ドメイン名
  • <variables>タグ内で宣言したnameを使ってアクセスする
  • Javaのコードを埋め込める(Integer.toString()とか)
  • コードは@{}で囲む

ちなみにandroid:textのところに埋め込むデータは、TextView.setText()を使って設定されるので、ここにint型のデータを埋め込むときは注意が必要です。何も考えずにint型のデータを表示しようとすると、そのint値がリソースIDとして認識されてしまいエラーになるからです。その際のエラー内容も、「そんなリソースIDないぞ」という内容で軽くハマりました。

Activityの処理

ちなみにDataBindingによって生成されるクラス名は、レイアウトXMLのファイル名をスネークケースからキャメルケースに変換したものに、Bindingを付け加えたものになります。

activity_main.xmlならActivityMainBindingに、hoge_hoge.xmlならHogeHogeBindingになります。

“`public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
Character chara;

<pre><code>@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
chara = new Character("桃太郎", 18, "きびだんご");
binding.setChara(chara);
}
</code></pre>

}“`

レイアウトXMLに紐付けるクラスを、setChara()で渡すことで、渡したCharacterクラスのデータが表示されます。ちなみにレイアウトXMLで<variables name="chara" .../>と設定しているからsetChara()になっています。nameをhogeにしていたらsetHoge()になります。

データの変更を反映させたい

単に表示するだけでは「何が便利なのか」という感じるかもしれません。

例えば桃太郎のスキルを「鬼退治」に変更したいとします。findViewByIdを使わずとも、DataBindingを使えばレイアウトXMLでidを割り振ったViewにアクセスすることができます。

@Override
public boolean onTouchEvent(MotionEvent event) {
binding.charaSkill.setText("鬼退治");
return super.onTouchEvent(event);
}

これを追加すると、タッチイベントが発生したらスキルの内容が「鬼退治」に変わります。

インスタンスの内容が変わったら自動的に反映されるようにする

レイアウトXMLと紐付けるクラスを以下のように書き換えると、インスタンスの内容が変更されるだけでUIに表示されるデータも変わります。

“`public class Character extends BaseObservable {
public String name;
@Bindable
public int age;
public String skill;

<pre><code>public Character(String name, int age, String skill) {
this.name = name;
this.age = age;
this.skill = skill;
}

public void countUp(){
age++;
notifyPropertyChanged(jp.gcreate.sample.databinding.BR.age);
}
</code></pre>

}“`

  • BaseObservableのサブクラスにする
  • 自動的に変更させたいpublicなフィールド、もしくはgetterに@Bindableアノテーションを付ける
  • データの変更が生じるメソッドでnotifyPropertyChanged()を呼ぶ

Activityのコードを以下のように書き換えてみます。

@Override
public boolean onTouchEvent(MotionEvent event) {
chara.countUp();
return super.onTouchEvent(event);
}

こうするとタッチイベントが生じる度に桃太郎が年をとるようになります。いちいち自分でTextViewに変更済みのデータを設定する必要がなくなります。

今のところ私はこの程度しか使っていないのですが、これだけでも充分便利だなぁと思います。とりあえずは簡単なところから試してみてはいかがでしょうか。

タッチイベントについて

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

端末の画面をタッチした情報はMotionEventとしてActivityやViewに通知されます。

MotionEventはさまざまな情報を持っています。

MotionEvent – Android Developers

  • アクション(触れたのか、動かしたのか、離したのか)
  • ポインタの数(何本の指で触っているのか)
  • タッチした座標

これらは全てポインタごと別々に識別されていて、全てポインタのインデックスでアクセスすることが出来ます。(ポインタのIDではありません)

その辺りをごっちゃにしてハマった結果、Stackoverflowに投稿した質問がこちらです。Androidでマルチタッチ時のポインターIDを検出する方法(ちなみに投稿後に勘違いが原因であることに気づいた)

ポインタインデックス

ポインタのインデックスは必ず0から始まり、getPointerCount() - 1まで割り振られます。

例えば2本の指でタッチしている場合、getPointerCount()は2を返します。1本目の指がポインタインデックス0で、2本目がインデックス1となります。

さらにこの状態で1本目の指を離すと、2本目の指のインデックスが0に変わります。

指を離す順番によってインデックスはころころ変わるため、特定のポインタを識別するのには使えません。

例えば人差し指、中指、薬指を使ったタップを考えましょう。途中で人差し指、薬指は離したり触れたりしているとします。しかし常に中指はつけたままにして、これをトラッキングしたいとします。この場合にはポインタインデックスを使うことは出来ません。

特定のポインタを識別するにはポインタIDを利用します。

ポインタID

一度タッチするとポインタにはIDが割り当てられ、そのIDは指を離すまで変わりません。

上記の例で言うと、中指を画面から離さないかぎり中指を示すポインタのIDは常に同じです。

一方で注意しなければいけないのは、座標を取得したりするメソッドの引数はポインタインデックスであるということです。

ポインタはIDで識別するけど、そのポインタの情報を取得するために必要なのはポインタインデックスです。

そのため、特定のポインタIDの座標を取得したりするには、findPointerIndex()メソッドを使って、IDからポインタインデックスを引き出す必要があります。

インデックスとIDの違い

ポインタインデックスは常に0から始まり、他のポインタが増減する度に再割当てされます。一方でポインタを識別するIDは、指が触れたときに割り振られ画面に触れている限りその値は変わりません。

例えばこんな感じになります。

インデックス0 ID0 人差し指
インデックス1 ID1 中指
インデックス2 ID2 薬指
 ↓この状態で人差し指を離す
インデックス0 ID1 中指
インデックス1 ID2 薬指
 ↓人差し指でタッチする
インデックス0 ID0 人差し指
インデックス1 ID1 中指
インデックス2 ID2 薬指

ポインタIDとポインタインデックスの値は、指を押した順番と反対に離す分には一致したままですが、押した順番とは異なる離し方をすると値がズレます。

ヒストリー

タッチイベントはリアルタイムに配信されるわけではありません。

開発者向けオプションでポインタの位置を表示するようにすると、ポインタの軌跡がそのまま表示されますが、onTouchEvent()にMotionEventが配信される間隔はマチマチです。例えばgetX()で取得できる座標は飛び飛びになってしまいます。

手書きの文字を描画しようと思うと、getX()メソッドだけを使っていると、描画処理の分MotionEventが配信される間隔が空いてしまい、描画できる線がカクカクしてしまうことでしょう。

しかしちゃんとMotionEventには、前回onTouchEventに配信されてから今回配信されるまでの間に記録している情報が格納されて配信されています。

getHisorySize()を使うことで、以前のonTouchEventが呼ばれてから今回のイベントが呼ばれるまでに、いくつのイベントを保持しているかが分かります。

ヒストリー情報を使ってポインタの情報を取得するには、getHistoricalX()といったメソッドを利用することになります。

アクション

タッチイベントの種類(触れたのか、離したのか、動かしたのか)はgetAction()で取得できます。

しかしgetAction()で取得できる情報は、ポインタのインデックスとポインタごとのアクションがごちゃまぜになった情報になります。例えば2本指同時押しだとgetAction()では261という数字が返ります。ちなみに1本でタッチすれば0です。

これはgetAction()がアクションの発生したポインタインデックスと、ポインタインデックスごとのアクションを全てまとめた値を取得するメソッドだからです。

getActionIndex()でアクションが発生したポインタのインデックスが分かります。

getAcitonMasked()は動作を表す純粋なアクションだけを返します。

つまりタップ(MotionEvent.ACTION_DOWN)を検出したい場合、マルチタッチを考慮するとgetActionMasked()を使う必要があるということです。getAciton()では二本指での同時押しを検出できない可能性があります。

座標

座標はgetX()でX座標、getY()でY座標を取得できます。

引数にポインタインデックスを渡すことで、指定したポインタインデックスの示す座標を取得できます。引数を省略した場合には、インデックス0の座標が取得できます。

getRawX()getRawY()と、getX()getY()の違いは、どこを基準とした座標数値が取得できるかです。

getRawX()などは座標の補正を行わない、端末のスクリーン上の座標を示します。スクリーンの左上をX=0,Y=0とした座標になります。Raw座標はポインタインデックス0のものしか取得できないみたいです。

対してgetX()はMotionEventを受け取るViewの左上をX=0,Y=0とした座標に変換されます。

サイズ

getSize()でサイズが取得できます。

このサイズは何かというと、多分タッチパネルが認識しているタッチの範囲とでも言いましょうか、指のサイズみたいなイメージです。

指の触れる範囲を増やしていくとサイズも大きくなります。

圧力

getPressure()で圧力を取得できます。

感圧式のタッチパネルならそのまま圧力(どれくらいの強さで押しているのか)が分かるのだと思います。

静電気を検出する静電容量方式タッチパネルでも値は変動しますが、純粋な意味での圧力を示しているわけではありません。指の触れている範囲が大きくなれば圧力も大きくなるみたいです。

ツールタイプ

指で触れているのか、スタイラスなのかというのが、getToolType()を使うことで検出できます。

しかしこの情報でスタイラスを識別するには、当然ながらスタイラスが端末に「自分はスタイラスである」と情報を送信している必要があります。

スタイラスを識別する万能メソッドではないことは注意が必要でしょう。少なくとも端末とペアリングするタイプのスタイラスでないと、ダメだと思います。

試していませんが、Bluetoothのマウスを端末にペアリングして使うと、これでマウスのポインタが識別できるのかもしれません。

タッチイベントを確認するサンプル

ActivityであればonTouchEventをオーバーライドすればタッチイベントを受け取ることが出来ます。例えばこんなコードを利用することでタッチイベントを確認することが出来ます。

public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.text);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int actionMask = event.getActionMasked();
        int actionIndex = event.getActionIndex();
        int count = event.getPointerCount();
        String text = "ACTION:" + action + " INDEX:" + actionIndex + "(id:" + event.getPointerId(actionIndex)
                + ") MASK:" + actionMask + "\n"
                + " pointer count:" + count + " x:" + event.getX() + " y:" + event.getY() + "\n";
        for (int i = 0; i < count; i++) {
            int id = event.getPointerId(i);
            text += " pointer index:" + i + " pointer id:" + id
                    + " x:" + event.getX(i)
                    + " y:" + event.getY(i)
                    + "\n";
        }
        textView.setText(text);
        return true;
    }
}