LECTURE 6 キー入力を検出する

○ はじめに

ユーザと対話するアプリケーションにとって、ユーザ入力を受け付けることは非常に重要な機能です。というか、入力を受け付けられなかったら対話アプリケーションは作れません。

ここではユーザ入力のうち、キー入力について検出して対応する方法について説明します。

○ キーイベントとキーコード

Android アプリケーションでは、キー入力は android.view.KeyEvent として表現されます。この KeyEvent は、android.view.View クラスの onKeyDown() および onKeyUp() メソッドを通してアプリケーションに通知されます。

public boolean View#onKeyDown(int keyCode, KeyEvent event);
public boolean View#onKeyup(int keyCode, KeyEvent event);

これらのメソッドには、前述の KeyEvent のインスタンスと同時に、押されたキーを表すキーコードが引数として渡されます。キーコードは KeyEvent#getKeyCode() を使って取り出すことも可能です。API ドキュメントで用意されているキーコード定数(KEYCODE_で始まるメンバ)を確認出来ます。

また、ALT キーが押されているかどうかは

public final boolean KeyEvent#isAltPressed();

を使って調べることもできます。その他にも色々なメソッドが android.view.KeyEvent クラスには用意されていますので、一度 API ドキュメントを眺めてみて下さい。

これらの KeyEvent は、ソフトウェアキーボードの場合は通常は生成されない様です(API ドキュメントによる)。ソフトウェアキーボードの場合にはこれらのイベントに依存してはならないとあります。

○ サンプルプログラムと説明

キー入力を検出するサンプルプログラムについて説明します。006keyapp.zip からダウンロードできます。はじめに のページから、全てのサンプルプログラムを 固めた zip ファイルをダウンロードすることもできます。

なお、ソースコードは SDK Tools 22.0.4 / プラットフォーム Android 4.2 でコンパイル可能です。local.properties 内に sdk.dir 変数が定義されていますが、ここは環境依存になりますので、お使いの環境に合わせて変更してください。

ソースコード配布は修正 BSD ライセンスに従います(はじめに のページの免責事項を参照してください)。

配布内容のファイルツリーは↓の様になります。

  • 006/
    • AndroidManifest.xml
    • ant.properties
    • bin/
    • build.xml
    • libs/
    • local.properties
    • proguard-project.txt
    • project.properties
    • res/
      • drawable-ldpi/ic_launcher.png
      • drawable-mdpi/
        • ic_launcher.png
        • ball.png
      • drawable-hdpi/ic_launcher.png
      • drawable-xhdpi/ic_launcher.png
      • layout/main.xml (未使用)
      • values/strings.xml
    • src/riolab/sample/
      • KeyApp.java
      • KeyView.java

今回は res/drawable-mdpi/ball.png を表示します。キー入力によって、この画像の表示位置を変えてみます。

↓ball.png (24x24)
キー入力で動かす画像ファイル

QVGA(320x480:mdpi) のエミュレータで実行すると、↓の様に表示されました。
KeyApp 実行画面

JKHL(上下左右)のキーを押す/離すごとに、ボールが上下左右に移動します。DPADキーにしたかったのですが、自分の PC はテンキーが無くてうまく認識してくれなかったので^^; 特に画面外かどうかの判定は行っていないため、画面外のどこまでも移動しちゃいます。

ここでは

  • KeyApp.java
  • KeyView.java

について説明します。

  • KeyApp.java
        1	/*
        2	 * Copyright (c) 2013, Ryo Ichinose
        3	 * All rights reserved.
        4	 *
        5	 * Redistribution and use in source and binary forms, with or without 
        6	 * modification, are permitted provided that the following conditions are met:
        7	 *
        8	 *    * Redistributions of source code must retain the above copyright 
        9	 *      notice, this list of conditions and the following disclaimer.
       10	 *    * Redistributions in binary form must reproduce the above copyright 
       11	 *      notice, this list of conditions and the following disclaimer in the 
       12	 *      documentation and/or other materials provided with the distribution.
       13	 *    * Neither the name of the auther nor the 
       14	 *      names of its contributors may be used to endorse or promote products 
       15	 *      derived from this software without specific prior written permission.
       16	 *
       17	 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
       18	 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
       19	 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
       20	 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
       21	 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
       22	 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
       23	 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
       24	 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
       25	 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
       26	 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
       27	 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       28	 */
       29	
       30	package riolab.sample;
       31	
       32	import android.app.Activity;
       33	import android.os.Bundle;
       34	import android.view.Window;
       35	
       36	public class KeyApp extends Activity
       37	{
       38	    /** Called when the activity is first created. */
       39	    @Override
       40	    public void onCreate(Bundle savedInstanceState)
       41	    {
       42	        super.onCreate(savedInstanceState);
       43		requestWindowFeature(Window.FEATURE_NO_TITLE);
       44	        setContentView(new KeyView(this));
       45	    }
       46	}
    
    

    これまでのサンプルプログラムの Activity クラスと、特に変わりはありません。

  • KeyView.java
        1	/*
        2	 * Copyright (c) 2013, Ryo Ichinose
        3	 * All rights reserved.
        4	 *
        5	 * Redistribution and use in source and binary forms, with or without 
        6	 * modification, are permitted provided that the following conditions are met:
        7	 *
        8	 *    * Redistributions of source code must retain the above copyright 
        9	 *      notice, this list of conditions and the following disclaimer.
       10	 *    * Redistributions in binary form must reproduce the above copyright 
       11	 *      notice, this list of conditions and the following disclaimer in the 
       12	 *      documentation and/or other materials provided with the distribution.
       13	 *    * Neither the name of the auther nor the 
       14	 *      names of its contributors may be used to endorse or promote products 
       15	 *      derived from this software without specific prior written permission.
       16	 *
       17	 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
       18	 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
       19	 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
       20	 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
       21	 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
       22	 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
       23	 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
       24	 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
       25	 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
       26	 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
       27	 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       28	 */
       29	
       30	package riolab.sample;
       31	
       32	import android.content.Context;
       33	import android.content.res.Resources;
       34	import android.graphics.Bitmap;
       35	import android.graphics.Canvas;
       36	import android.graphics.Color;
       37	import android.graphics.Paint;
       38	import android.graphics.drawable.Drawable;
       39	import android.view.KeyEvent;
       40	import android.view.View;
       41	
       42	public class KeyView extends View
       43	{
       44	    private final Paint paint = new Paint();
       45	    private Bitmap bitmap;
       46	    private int x;
       47	    private int y;
       48	
       49	    public KeyView(Context context)
       50	    {
       51		super(context);
       52		setFocusable(true);
       53	
       54		Resources res = context.getResources();
       55		Drawable drawable = res.getDrawable(R.drawable.ball);
       56	
       57		int w = drawable.getIntrinsicWidth();
       58		int h = drawable.getIntrinsicHeight();
       59		
       60		bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
       61		Canvas canvas = new Canvas(bitmap);
       62	
       63		drawable.setBounds(0, 0, w, h);
       64		drawable.draw(canvas);
       65	    }
       66	
       67	    @Override
       68	    protected void onSizeChanged(int w, int h, int oldw, int oldh)
       69	    {
       70		x = w / 2;
       71		y = h / 2;
       72	    }
       73	
       74	    @Override
       75	    public void onDraw(Canvas canvas)
       76	    {
       77		super.onDraw(canvas);
       78		canvas.drawColor(Color.BLUE);
       79		canvas.drawBitmap(bitmap, x, y, paint);
       80	    }
       81	
       82	    @Override
       83	    public boolean onKeyDown(int keyCode, KeyEvent event)
       84	    {
       85		if (processKeyCode(keyCode)) {
       86		    invalidate();
       87		    return true;
       88		}
       89	
       90		return super.onKeyDown(keyCode, event);
       91	    }
       92	
       93	    @Override
       94	    public boolean onKeyUp(int keyCode, KeyEvent event)
       95	    {
       96		if (processKeyCode(keyCode)) {
       97		    invalidate();
       98		    return true;
       99		}
      100	
      101		return super.onKeyUp(keyCode, event);
      102	    }
      103	
      104	    private boolean processKeyCode(int keyCode)
      105	    {
      106		if (keyCode == KeyEvent.KEYCODE_K) {
      107		    y -= 10;
      108		    return true;
      109		}
      110	
      111		if (keyCode == KeyEvent.KEYCODE_L) {
      112		    x += 10;
      113		    return true;
      114		}
      115	
      116		if (keyCode == KeyEvent.KEYCODE_J) {
      117		    y += 10;
      118		    return true;
      119		}
      120		
      121		if (keyCode == KeyEvent.KEYCODE_H) {
      122		    x -= 10;
      123		    return true;
      124		}
      125	
      126		return false;
      127	    }
      128	}
    
    

    キー入力に応じて画像の表示位置を変える View クラスです。画像の表示自体については、LECTURE 5 画像を表示する を参照してください。

    • 46 〜 47 行目

      画像の表示位置を保存する変数です。画像の左上の角を表します。

    • 67 〜 72 行目 (onSizeChanged())

      View のサイズが変わった時に実行されるメソッドです。本プログラムでは、View のほぼ真ん中(x、y は画像左上の座標のため、正確には真ん中ではありませんが)に画像を表示するために、本メソッドをオーバライドしています。

    • 74 〜 80 行目 (onDraw())

      79 行目の canvas.drawBitmap(bitmap, x, y, paint); により、保存してある x/y の位置に画像を表示します。

    • 82 〜 91 行目 (onKeyDown())

      キーが押された際に実行されるメソッドです。KeyEvent を処理した場合は true、処理しなかった場合は false を返します。

      自作の processKeyCode() によってキーに応じて画像の表示位置を更新し、View#invalidate() にて画面の再描画を促します。View#invalidate() には領域指定有り/無しバージョンがありますが、ここでは領域指定無しバージョンを使うことで画面全体の再描画を指示しています。

      View#invalidate() は、UI スレッド(UI 関連の処理を行うスレッド。コールバックメソッドは UI スレッドから実行される)以外のスレッドから実行してはいけません。UI スレッド以外から画面の再描画を指示するには、View#postinvalidate() を使用してください。

    • 93 〜 102 行目 (onKeyUp())

      キーが離された際に実行されるメソッドです。onKeyDown() の説明を参照してください(本プログラムでは、キーを押したのか離したのかでは処理を区別していません)。

    • 104 〜 127 行目 (processKeyCode())

      JKHL(上下左右)が操作された場合に、画像の表示位置(x/y) を更新するメソッドです。本メソッド実行後に View#invalidate() を実行することで、新しい表示位置に画像が表示されます。

以上で解説は終わりです。次回は、複数 View を同時に画面に表示するために必要となる、レイアウト管理用クラスについて説明する予定です。

○ OnKeyListener を使ってキー入力処理を切り出す

サンプルプログラムでは、View クラスの onKeyDown()/onKeyUp() をオーバライドしてキー入力を処理しました。この方法は簡単ですが、プログラムがある程度大きくなってくると、キー入力処理は別のクラスに任せてコードをすっきりさせたいという要求が出てきたりします。

この様な場合に使えるのが、android.view.View.OnKeyListener インタフェースです。OnKeyListener インタフェースは、キー入力を処理するクラスが実装すべきインタフェースで、実装クラスを View#setOnKeyListener() に指定するとキーイベントが転送されます。

public void View#setOnKeyListener(View#OnKeyListener l);

OnKeyListener インタフェースで定義されているメソッドは、onKey() メソッドのみです。実装クラスは、本メソッドを実装します。

public boolean onKey(View v, int keyCode, KeyEvent event);

KeyEvent が発生した View と、keyCode および KeyEvent のインスタンスが引数によって伝えられます。View#onKeyDown()/onKeyUp() 同様、KeyEvent を処理した場合は true、処理しなかった場合は false を返します。キーが押されたのか離されたのかは、KeyEvent#getAction() にて取得します。

public final int KeyEvent#getAction();

KeyEvent#getAction() メソッドは、以下のいずれかを返します。

  • KeyEvent#ACTION_DOWN
  • KeyEvent#ACTION_UP
  • KeyEvent#ACTION_MULTIPLE