タッチイベントについて

端末の画面をタッチした情報は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とポインタインデックスの値は、指を押した順番と反対に離す分には一致したままですが、押した順番とは異なる離し方をすると値がズレます。

ヒストリー

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

Read full post gblog_arrow_right

パララックスイメージのAppBarをListViewを使って実装しようとしてハマった話

AppBar(Toolbar、ActionBar)の部分が大きめの画像になっていて、コンテンツをスクロールするとそれに合わせて画像が縮んでいき、最終的にToolbarだけが残る(もしくは全部隠れる)みたいなデザインがありますよね。あれを実装しようと思って試行錯誤してみました。

試行錯誤になってしまった原因は、スクロール可能なコンテンツ部分を横着してListViewで作ってしまったからでした。見かけるサンプルはだいたいRecyclerViewを使っていたのですが、使ったことがないため使い慣れているListViewでやろうとしたのが間違いでした。

ListViewで実装すると、ListViewをスクロールしてもAppBarは連動して動いてくれません。AppBarの部分をスクロールすると伸縮してはくれますが、巷にあふれるパララックスAppBarはこんな残念な動きはしていません。

コードで何か手を加えないといけないのだろうかと調べるうちに、なぜListViewではAppBarが連動して動かないのか原因が分かりました。今回はそのお話です。

Patterns– Scrolling techniques

layout.xmlの設定

基本的にパララックスなAppBarを実装するには、レイアウトXMLの記述のみで実装できます。

サンプルコード – GitHub

このMaterial Design(Android desgin support library)による階層構造を初めて見ると、なんだかややこしく感じてしまいますが、1つずつ紐解いていけばそう難しい構造ではありません。

正確にはandroid.support.design.widget.〜とFQCN(パッケージ名を含めたクラス指定)になりますが、ここでは長くなるので省略しています。

CoordinatorLayout
├AppBarLayout
│└CollapsingToolbarLayout
│ ├ImageView
│ └Toolbar
├ListView(などスクロール可能なコンテンツ)
└FABなどお好みで

基本的にXML上でちゃんと必要な指定さえ行えば動きます。コードは不要です。

CoordinatorLayout

今回の例ではListViewのスクロールにあわせてAppBarLayoutを伸縮させるために存在しています(FABをToolbarとListViewの中間に配置する役割も担っていますが)。このCoordinatorLayout自体は内包したView同士を連携させたりする単なる入れ物です。全然「単なる」ではないですけど。

AppBarLayout

AppBar部分のLayoutを管理するコンテナで、AppBarの部分に表示するViewをこの中に入れてやります。Blank Activityを作成すると、この中にはToolbarだけが入っていると思います。

ここではAppBarの高さを指定してやります。android:layout_height="192dp"

CollapsingToolbarLayout

折りたためるToolbarのための入れ物です。スクロールによるAppBarの動き方を指定することができます。ここではapp:layout_scrollFlags="scroll|exitUntilCollapsed"と指定しています。

ImageView

AppBarが全開のときに表示されるイメージ画像です。コンテンツのスクロールに合わせて縮み、最終的にToolbarだけが残ります。ここではapp:layout_collapseMode="parallax"を指定しています。

Toolbar

Toolbarです。ここではapp:layout_collapseMode="pin"を指定しています。この指定でToolbar自体は隠れずに残ります。

Read full post gblog_arrow_right

BaseSaveStateにを拡張してカスタムViewの状態を復元する際の注意点

カスタムViewを作った場合、BaseSaveStateを拡張してViewの状態をカスタムView自身で復元できるようにできます。

この際に注意すべきことが3点あります。

Activityを保持しないを有効にしてチェックする

カスタムViewの復元機能を実装したら、必ず開発者オプションのActivityを保持しないを有効にしてちゃんどう動くかどうか確認しましょう。

自分ではちゃんと実装したつもりでも、これを有効にした状態で画面回転させるとアプリが落ちる場合があります。

フィールド名のタイポに注意

BaseSaveStateを拡張したクラスには、必ずpublic static final Parcelable.Creator<BaseSaveStateを拡張したクラス名> CREATORというフィールドが必要です。

このフィールドの名前はCREATORでなければなりません。

CREATERとタイポすると動きません。動かない上にエラーメッセージはjava.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.savestatecustomview/jp.gcreate.sample.savestatecustomview.MainActivity2Activity}: java.lang.RuntimeException: Parcel android.os.Parcel@18c09797: Unmarshalling unknown type code 2131296303 at offset 264のように、「フィールド名が違います」と教えてくれません。

writeToParcelで書き出す順番

writeToParcelで書き出す順番とコンストラクタで読み出す順番は同じ順番にしなければなりません。

書き出す順番と読み出す順番が異なるとうまく復元することができません。

順番を同じにすることと一緒に忘れていけないのは、最初にsuperを呼び出すことです。

        public ImageState(Parcel source) {
            super(source);
            savedUri = source.readParcelable(Uri.class.getClassLoader());
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeParcelable(savedUri, flags);
        }
```

コンストラクタで`super(source)`を最初に呼び出す、`writeToParcel`の最初で`super.writeToParcel(dest, flags)`を呼び出すことも忘れてはいけません。

単純なことですが、エラーメッセージからどこが悪いのか把握しづらいので、知らないとドはまりするので注意しましょう。

## サンプル

public class UriImageView extends ImageView{
 private Uri mUri;

 public UriImageView(Context context, AttributeSet attrs) {
 super(context, attrs);
 setImage();
 }

 private void setImage() {
 if(mUri == null){
 setImageDrawable(getContext().getResources().getDrawable(android.R.drawable.btn_star, getContext().getTheme()));
 }else{
 setImageURI(mUri);
 }
 }

 public void setUri(Uri uri) {
 mUri = uri;
 setImage();
 }

 @Override
 protected Parcelable onSaveInstanceState() {
 Parcelable superState = super.onSaveInstanceState();
 ImageState imageState = new ImageState(superState);
 imageState.savedUri = mUri;
 return imageState;
 }

 @Override
 protected void onRestoreInstanceState(Parcelable state) {
 ImageState imageState = (ImageState) state;
 super.onRestoreInstanceState(imageState.getSuperState());
 setUri(imageState.savedUri);
 requestLayout();
 }

 static class ImageState extends BaseSavedState{
 public static final Parcelable.Creator CREATOR = new Parcelable.Creator(){

 @Override
 public ImageState createFromParcel(Parcel source) {
 return new ImageState(source);
 }

 @Override
 public ImageState[] newArray(int size) {
 return new ImageState[size];
 }
 };
 Uri savedUri;

 public ImageState(Parcel source) {
 super(source);
 savedUri = source.readParcelable(Uri.class.getClassLoader());
 }

 public ImageState(final Parcelable superState) {
 super(superState);
 }

 @Override
 public void writeToParcel(@NonNull Parcel dest, int flags) {
 super.writeToParcel(dest, flags);
 dest.writeParcelable(savedUri, flags);
 }
 }
} “`

Read full post gblog_arrow_right

MatrixのpostScaleで画像を拡大縮小させる

ImageViewなり、自分で作ったCustom Viewなりで表示させる画像を、動かしたり拡大縮小させたりするのに使えるMatrixをいじって学んだことのメモです。

特にpostScaleを使った拡大縮小がイメージ通りに動かなくてハマってしまいました。

ちなみにMatrixクラスを使ってBitmapを加工する – Techoboosterを参考に始めました。

使い方

ImageViewに設定するには、setImageMatrixメソッドでMatrixを渡してやるといいです。

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

CustomViewで使う場合は、オーバーライドしたonDraw()で描画するときにMatrixを渡せばいいです。

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mImage, mMatrix, mPaint);
    }

こんな感じ。mImageはBitmapオブジェクトで、mMatrixとmPaintはそれぞれnew Matrix(),new Paint()したものを渡してます。

今回は下のCustom ViewでMatrixを操作していて分かったことを書きます。

タッチ操作で動かす

画面上を指でなぞると、その動きに応じて画像も移動するようにする場合はこうすればOK。

mMatrix.postTranslate(float X移動量,float Y移動量);

移動量を取得するにはTouchEventを自分で判定するなり、GestureDetectorを使うなりして取得します。

GestureDetectorの使い方はDetecting Common Gestures – Android Developers参照。また別途記事書こうと思います。

移動に関しては特に難しくはありませんでした。ただし、移動制限を設けようとするとこれはこれでまた大変そうです。

こちらの記事が移動制限を実装するのに非常に役立ちそうな予感です。実装できたらまた記事書きたいと思います(そればっか)。

拡大縮小

ピンチイン・アウトで画像を拡大縮小させる場合がクセモノでした。

mMatrix.postScale(float X拡大率, float Y拡大率, float 拡大の起点X, float 拡大の起点Y);

ハマったポイントはここで渡す拡大率の扱いです。

postScaleに渡す拡大率は、Matrixを指定した拡大率に変形させるのではありません。現在のMatrixを渡した拡大率で拡大縮小させます。Matrixの拡大率が0.1のときにpostScale(0.1f,0.1f)するとMatrixの拡大率は0.01になります。

画像が過剰に縮小・拡大されないように渡す拡大率の値を制限したとしても、制限した値をそのまま渡してしまったら制限が効きません。

指定した拡大率に画像を変形させたい場合は変化量を計算して渡すようにします。

ScaleGestureDetectorを使って拡大縮小させていて、頭の中がこんがらがっていました(現在進行形ですけど)。ScaleGestureDetectorを使うと、onScaleメソッド内でdetector.getScaleFactor()を使うことでピンチイン・アウトによる拡大率を取得することができます。

この拡大率は、ピンチ操作が始まった段階では1.0から始まります。そのためこの値をそのままMatrixのpostScaleに渡すと、拡大縮小の開始時に一旦元の縮尺に戻ってしまいます。そのこととごっちゃになっていて間違ったこと書いてました。

float deltaScale = targetScale / nowScale;
mMatrix.postScale(deltaScale, deltaScale);

postScaleではなくsetScaleを使う方法もあるのかもしれませんが、動き始めに画像が元のサイズに戻ってしまうため、この方法がスマートな気がします。

Read full post gblog_arrow_right

AnimationDrawable 静止画を使ったアニメーション

静止画像(pngなどの画像リソース)を用意してパラパラ漫画の要領でアニメーションさせるには、AnimationDrawableクラスを利用します。

Android APIs Reference – AnimationDrawable

AnimationDrawableのサンプル

文字が変わってるだけですが、3つの画像でアニメーションしてます。画像を準備するのが面倒くさかったので、文字だけの画像を使いました。

アニメーションに使う静止画像

画像は解像度に合わせてres/drawable/hdpiなどのディレクトリに用意します。

今回はanime_test1.png,anime_test2.png,anime_test3.pngの3つの画像ファイルを用意しました。画像と言いつつ数字の1,2,3が書かれているだけの画像です。

ちなみにファイル名として使えるのは小文字のアルファベット、数字、アンダースコア(_)とドット(.)のみです。それ以外の文字(大文字アルファベットなど)を使うと以下のようにコンパイルエラーとなります。

Invalid file name: must contain only lowercase letters and digits ([a-z0-9_.])

アニメーション設定のXMLファイル

どの画像を何秒間表示させるのかという設定をXMLファイルに記述します。今回はres/drawable/test_animation.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/anime_test1"
          android:duration="500"/>
    <item android:drawable="@drawable/anime_test2"
          android:duration="500"/>
    <item android:drawable="@drawable/anime_test3"
          android:duration="500"/>
</animation-list>

android:oneshot=trueで、アニメーションを1回のみ再生する設定になります(最後の画像でアニメーションが止まる)。falseだとループ再生されます。

アニメーションを再生する

test_animationは何もしなければ単なる静止画と同じで、Drawableとして扱うことができます。ImageViewのsrc属性に設定したり、TextViewのbackground属性に設定したりすることができます。

今回はImageButtonに上記で作成したdrawableを設定してやり、ボタンを押したらアニメーションが再生されるようにしてみます。

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                tools:context=".MainActivity">

    <ImageButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image_button"
        android:src="@drawable/test_animation"
        />

</RelativeLayout>

APIリファレンスではandroid:background属性に設定していますが、これはandroid:src属性に設定しても動きました。src属性にAnimationDrawableを設定した場合、getBackground()ではなくgetDrawable()でAnimationDrawableを取得します。

MainActivity.java(onCreateを抜粋)

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageButton imageButton = (ImageButton) findViewById(R.id.image_button);
        final AnimationDrawable animationDrawable = (AnimationDrawable) imageButton.getDrawable();

        imageButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                animationDrawable.start();
            }
        });
    }

AnimationDrawableを取得して、start()メソッドを呼び出せばアニメーションさせることができます。

Read full post gblog_arrow_right

ペーパープロトタイピングをまずやろう

アプリを開発する上で、コーディングを始める前にペーパープロトタイプを作るといろいろなメリットを享受できます。

  • どんな画面が必要になるか検討できる
  • 作り終わってから使い勝手の悪いところに気づいて、実装をやり直す事態を未然に防げる可能性がある
  • 必要な機能の絞り込みができる
  • 作ろうとしているアプリのイメージがはっきりしてくる
  • などなど

デザインに疎いとどうしてもコーディングを優先してしまいがちです。画面設計よりシステムを作っている方が性に合ってますし。ついついデザインを後回しにしてしまいます。

しかしその作り方をすると、アプリ完成したけれど残念な見た目だったり、使い勝手が悪くて使えないアプリになってしまったりしているかもしれません。それを後から直そうとすると、せっかく実装したシステムも作りなおしになる可能性もあります。せっかく作ったコードを、使い勝手が悪いからと泣く泣く切り捨てることになったら、目も当てられません。

POP

ありがたいことに、ペーパープロトタイピングを簡単に行うことのできるツールが世の中には存在しています。

POPというアプリを利用すると、紙に書いた画面デザインにいとも簡単に動きをつけることができます。ここを押したらこの画面に移動して・・・なんていうのが簡単に作れて非常に便利です。

Android版のPOP2.0だとジェスチャーを設定する項目があるものの、設定しても動かなかったり、スマホで撮影した画面を削除しようとしたらゾンビのごとく復活してきたりと、使い勝手が微妙なところがあります。私の使ってる端末固有の問題なのかもしれませんけれども・・・。

ただこれがあるだけでも画面の動きのイメージがつきます。この画面遷移は使いづらいとか、こういう画面も用意しないといけないなというのが簡単に分かります。このアプリ、便利なのは間違いありません。

Androidからだけでなく、パソコンのブラウザやiPhoneなどからも使うことができるので、ブラウザで編集してスマホで動作を確認するなんて使い方ができます。

POPブラウザからアクセス

テンプレート

紙にアプリのデザインを書くのに、さり気なくネックなのが画面を描画するところです。画面の外枠です。私はこれが面倒くさくて、画面を紙に書けないでいました。

枠を決めるのが難しいのであれば、最初から用意されているものを利用すればいい。そこで私は、iPhone5のワイヤーフレームに使えるアイデアシートをイラレで作りましたさんで公開されているアイデアシートを利用させていただきました。

現在はリンク切れで見れなくなってますね・・・。

iPhone5向けのテンプレートではありますが、画面の比率はAndroidとそう大差ありません。画面の外枠が決まっているだけでも、やりやすさが段違いです。

どう実装するかはとりあえず考えない

とりあえず考えるのは、ボタンをどこに置いて、どの画面に移動するのかだけに絞った方がいいと思います。できるだけシンプルに考えるのが大事です。どうやってコーディングしようかと考えだすと、何も書けなくなってしまいます。

実装方法を考えながらやってしまうと、実装しやすさを優先するあまりに使い勝手が犠牲になるのが悲しいです。とりあえずアイデアシートを印刷して、どしどし書くべしです。書いていればいいアイデアが浮かんだりします。紙に書くだけなのでやり直しも簡単ですしね。

せっかくアプリを作るのであれば、多くの人に使ってもらいたいです。そのためにも、せめて使い勝手がよくなるような努力はしておきたいですよね。

デザインを考える

ださいデザインからの脱却

1分間タイマーは、最初のバージョンでは文字とボタンだけが表示されている、非常にダサいアプリでした。それに比べると現在の見た目はだいぶましになったように思います。あくまで最初の頃よりはましになったなというだけで、カッコイイ見た目にするにはどうすればいいのかよく分かりません。

ただ、見た目をかっこ良くするという観点からのアプローチは難しくとも、使いやすくするという観点からのアプローチであれば、少し突破口が見えるような気がします。私がSmashing Android UI レスポンシブUIとデザインパターンという本を読んで、1分間タイマーに加えた変更を例にしてみましょう。

開始ボタンを押しやすくする

1分間タイマーを自分で使っていてとても不便だったのが、タイマーの開始ボタンが押しづらいことでした

1分間タイマーの当初のバージョンでは、デフォルトのButtonを開始ボタンとして利用していました。私はタイマーを開始させたらすぐに紙に向かって文字を書き出していきたいため、アプリの開始ボタンは横目で見ながら押すような感じで使っていました。しかし以前のバージョンの四角い小さなボタンでは、開始させたつもりが押せていないということがよくあってテンポが悪かったのです。

ボタンを大きくすれば使い勝手はかなり向上します。

新旧1分間タイマーの変化

残り時間の表示方法

ボタンの巨大化にともなって、残り秒数の表現方法も変えることにしました。開始ボタンを円形にしたので、その周りにバーのような感じで残り秒数が分かればスマートかなと考えました。

以前は単に文字で残り秒数を表示していました。しかし自分で使っていて、あと何秒残っているかを文字で読み取ることはいままで一度もありませんでした。そもそも音声による通知もあるので、残り秒数を文字で把握する必要性はありません。

円形のバーであれば視覚的に横目であとどれくらい残っているかが分かるので、文字で表示されるよりもマシだと思います。

ただ、このバーの動きがカクカクしているのが残念なところです。スムーズに動くように見せることができれば、見た目もよくなるのですが、実装方法が分かりませんでした。いずれやり方を調べて実装できたらいいなぁと思っています。

アプリ終了時の確認ダイアログをなくした

以前のバージョンでは、バックボタンを押した時に「終了しますか?」という確認ダイアログを表示していました。この終了するかどうか確認をとるアプリは、世の中にまだまだ多く存在しています。

みなさんは、アプリを使っていて終了時に確認ダイアログが出ることについてどう考えていますか?

「そんな確認はいらないんでさっさと終了しろよ」派でしょうか、「わざわざ確認してくれて親切やね、ありがとう」派でしょうか。

Smashing Android UI レスポンシブUIとデザインパターンでは、この終了時に確認ダイアログを出すのは、多くの場合開発者の都合によってつけられているものであると断じていました。

確認ダイアログは悪である

終了時に限らず、何らかの操作を行う際に確認ダイアログを表示するのは、取り返しの付かないことを実行する場合にユーザーに責任を転嫁させるため、開発者の都合で使われている悪しきものだとバッサリでした。

確認ダイアログを出したところでユーザーがちゃんと確認するかどうかは分かりません。操作ミスを恐れて確認ダイアログを表示させるという考え方もあるかもしれませんが、そのダイアログのボタンを操作ミスしないとは言い切れないはずです。

そこで確認ダイアログを使うくらいなら、操作を実行した後にそれを取り消すための手段をユーザーに提供することこそが、真のユーザーフレンドリーであるとこの本には書いてありました。ごもっともだと思いました。

終了時の確認で言えば、一度終了してしまうと起動するのに膨大な時間がかかってしまうため、操作ミスによる終了を防止する意味で出すというのはありかもしれません。起動するのに1分かかるゲームアプリで、やっと起動したと思った時に手が滑ってバックボタンを押してしまった。そういう場合であれば、確認ダイアログを表示する方が親切かもしれません。

ですが、その確認ダイアログは必要なのかと自問することが大切です。終了前の操作状態を復元することで対処できないか、いつ終了されてもデータが保存されるように作ればすむ話ではないか。そう考えることで使い勝手が向上していくのです。

Read full post gblog_arrow_right