端末の画面をタッチした情報は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.
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自体は隠れずに残ります。
ListView よく見かけるサンプルではRecyclerViewやNestedScrollViewが利用されています。しかし私はRecyclerViewの使い方がよくわからなかったのでListViewで代用しています。
ここでは必ずapp:layout_behavior="@string/appbar_scrolling_view_behavior"の指定が必要です。
この@string/〜はAndroid Support Libraryのstringリソースを参照していて、その中身はandroid.support.design.widget.AppBarLayout$ScrollingViewBehaviorとなっています。つまりこのListViewの振る舞いとして、AppBarLayoutのScrollingViewBehaviorを指定しているわけです。
ListViewを使うと、そのままではListViewがスクロールされるだけでAppBarが伸縮しません。ListViewのスクロールと連動させるためには、ListViewにandroid:nestedScrollingEnabled="true"を指定する必要があります。
なぜか。スクロール可能なコンテンツとAppBarの伸縮を連携させるためには、ListViewがスクロールされたということをCoordinatorLayoutに伝える必要があります。RecyclerViewやNestedScrollViewは標準でこれをやってくれるわけですが、ListViewは何もしません。そこでCoordinatorLayoutにスクロールイベントを通知するための設定を有効にしてやる必要があるのです。
android:nestedScrollingEnabled="true"(NestedScrollに関する処理)はAPI21以上のViewに実装されています。
余談:なぜAppBarが動くのか 仕組みを完全に理解したわけではないので、ざっくりとした説明です。
ListViewの上でスクロールを行うと、ListViewの中身がスクロールされます。これはListViewのonTouchEventで処理されています。これだけではListViewの中でスクロールイベントが処理されるだけで、AppBarの変形にはつながりません。
そこで登場するのがCoordinatorLayoutです。こいつが子Viewのスクロールと、別の子Viewを連携させるわけです。
連携させるためにはCoordinatorLayoutにスクロールイベントを通知する必要があり、その仕組がNestedScrollです。RecyclerViewやNestedScrollViewは初めからCoordinatorLayoutと連携する前提で作られていますし、ListViewなどでもSDK21からNestedScrollに関する処理が追加されています。ただし初期状態では無効化されているので、NestedScrollの処理を有効化してやる必要があり、それがandroid:nestedScrollingEnabled="true"になります。
NestedScrollの処理は子ViewのonTouchEvent(onTouchMove)でCoordinatorLayoutに伝わります。CoodinatorLayoutはonNestedPreScroll内でBehaviorが設定されている子Viewを探し、見つかったBehaviorに対してdispatchOnDependentViewChangedを呼び出します。今回の例ではScrollingViewBehaviorです。
最終的にAppBarのサイズを伸縮させる処理は、このBehaviorのonDependentViewChangedで行われているみたいです。
参考 Handling Scrolls with CoordinatorLayout
カスタム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.
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を使う方法もあるのかもしれませんが、動き始めに画像が元のサイズに戻ってしまうため、この方法がスマートな気がします。
ちなみにピンチイン・アウトを検出するにはScaleGestureDetectorが使えます。Dragging and Scaling – Android Developers
拡大縮小の起点を指定する場合、detector.getFocusX()を使うとイメージに近い動きになりました。ただし画像の範囲外でやると当然ながら動きがおかしくなるので、画像が画面の範囲外に移動できないように制限を実装しないとダメそうです。
現在の拡大率を取得するには? Matrixに設定された現在の拡大率を取得するのも一工夫必要です。getScaleXというような、Marixの現在の拡大率を直接取得するメソッドはありません。
Matrixは9つの値を保持していますが、その値を参照するためにはfloatの配列を渡してコピーしてもらうしか方法がありません。
float[] values = new float[9]; mMatrix.getValues(values); nowScale = values[Matrix.MSCALE_X]; ちなみにAPIリファレンスにある並びで並んでいるわけではないので、定数を使ってアクセスしましょう。
ちなみに0から順にMSCALE_X,MSKEW_X,MTRANS_X,MSCALE_Y,MSKEW_Y,MTRANS_Y,MPERSP_0,MPERSP_1,MPERSP_2の順に並んでます。
なんで変な並びになってるのかと思ったら、英語のWikipedia見たらその意味が分かるかもしれません。Transformation matrix – Wikipedia
Android特有の概念ではなく、Matrixを使った変形の概念があるんですね。数式だらけでサッパリ分かりませんが。
日本語で解説してあるサイトもありますが、いずれにしても奥が深そうで難解です・・・。
静止画像(pngなどの画像リソース)を用意してパラパラ漫画の要領でアニメーションさせるには、AnimationDrawableクラスを利用します。
Android APIs Reference – 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()メソッドを呼び出せばアニメーションさせることができます。
ただしできるのは再生するか停止するかくらいで、逆再生したりはできないみたいです。