んでもってHandler
今回はHandlerを試してみました。前回、SurfaceViewの復習で作成したcodeを利用し、実装をしてみたいと思います。
Handlerとは
HandlerはHandler objectを作成したThreadとそのMessageQueueに紐づけられます。Handler object作成後は紐づけられたMessageQueueにMessageやRunnableをenqueueしたり、enqueueされたMessage objectやRunnableを実行します。(原文ここ)
Handlerの使用用途は主に2つ
- MessageやRunnableで定義された処理をscheduling
- 別のthreadに処理をenqueueする(お願いする)
1の例でいくと、例えば、あるeventが発生した後に5秒間隔で同じ処理を繰り返したい。
2の例でいくと、Androidの場合、GUIの操作を許されてるのはUI threadのみなので、Background(UI threadとは別thread)で行っていた処理の結果をUIに反映したいというような時にHandlerを使います。
今回は2の例を実装しました。前回のSurfaceViewでした実装に加え、(大まかに言って)移動するiconをtouchしたら"Ouch! You hit me!"とTextViewに表示し、missしたら"Oops. You missed me..."と表示するというものです。
実装の要点をまとめます
気づいた点を記載します
- 始めIconの描画範囲を指定する為にWindowManagerからDisplay objectを取得していたが、Display sizeだとtitle barおよびstatus barのsizeも含んだsizeとなってしまう
- 前回、UI eventのhandlingをしていなかったので問題なかったのですが、event取得の為にSurfaceViewでsetFocusable(true)のrequestFocus()をしなくてはならない。(ただ、やらなくてもtouch eventの場合、問題なかった。KeyEvent時必要??)
- SurfaceViewはsizeを直指定せず"fill_parent"や"wrap_content"とすると、親のViewに含まれるwidgetを無視して親のViewと同じsizeになる
んで、実装はこんな感じ
Layout XML
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- SurfaceViewはsizeを直指定せず"fill_parent"や"wrap_content"とすると、 親のViewに含まれるwidgetを無視して親のViewと同じsizeになる--> <com.android.practice.MySurfaceView android:id="@+id/surface" android:layout_width="fill_parent" android:layout_height="fill_parent"/> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/text" android:visibility="visible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" android:text="@string/hit_me_msg" android:textColor="#88ffffff" android:textSize="24sp" android:background="#00000000"/> </RelativeLayout> </FrameLayout>
Activity抜粋
protected void onResume() { super.onResume(); setContentView(R.layout.main); mSurfaceView = (MySurfaceView) findViewById(R.id.surface); mSurfaceView.setTextView((TextView) findViewById(R.id.text)); }
SurfaceView
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback{ private static final String TAG = "MySurfaceView"; public static final int HIT = 0; public static final int NO_HIT = 1; public static final int CLEAR = 2; private static final String THREAD_NAME = "DrawingThread"; private MyDrawingThread mDrawingThread; private Context mContext; private Handler mHandler; private TextView mMsgTextView; public MySurfaceView(Context context) { super(context); mContext = context; getHolder().addCallback(this); //UI thread上でHandlerを作成する mHandler = getMyHandler(); //作成されたHandlerを別threadでもaccessできるようにする mDrawingThread = new MyDrawingThread(THREAD_NAME, mHandler, this, mContext); //event取得の為にSurfaceViewでsetFocusable(true)のrequestFocus()をしなくてはならない setFocusable(true); requestFocus(); } public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; getHolder().addCallback(this); //UI thread上でHandlerを作成する mHandler = getMyHandler(); //作成されたHandlerを別threadでもaccessできるようにする mDrawingThread = new MyDrawingThread(THREAD_NAME, mHandler, this, mContext); //event取得の為にSurfaceViewでsetFocusable(true)のrequestFocus()をしなくてはならない setFocusable(true); requestFocus(); } public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { } public void surfaceCreated(SurfaceHolder holder) { Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceCreated()"); startDrawing(); } private Handler getMyHandler(){ return new Handler(){ //Handler#handleMessage()をoverrideし、そこで、Messageの結果をUIに反映する処理を行う @Override public void handleMessage(Message msg) { Log.d(TAG, "handleMessage"); if(mMsgTextView == null){ Log.d(TAG, "mMsgTextView == null"); return; } switch(msg.what){ case HIT: mMsgTextView.setText(R.string.hit_msg); break; case NO_HIT: mMsgTextView.setText(R.string.no_hit_msg); break; case CLEAR: mMsgTextView.setText(R.string.clear_msg); default: break; } } }; } public void surfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceDestroyed()"); stopDrawing(); } private void startDrawing(){ mDrawingThread.startDrawing(); } private void stopDrawing(){ mDrawingThread.stopDrawing(); try{ mDrawingThread.join(); }catch(InterruptedException e){ } } public void setTextView(TextView textView){ mMsgTextView = textView; } public Rect getDrawingBoundary(){ Rect drawingBoundary = new Rect(); drawingBoundary.left = 0; drawingBoundary.top = 0; drawingBoundary.right = getWidth(); drawingBoundary.bottom = getHeight() - mMsgTextView.getHeight(); return drawingBoundary; } @Override public boolean onTouchEvent(MotionEvent event) { switch(event.getAction() & MotionEvent.ACTION_MASK){ case MotionEvent.ACTION_UP: Log.d(TAG, "TouchEvent: Action up"); break; case MotionEvent.ACTION_DOWN: Log.d(TAG, "TouchEvent: Action down"); mDrawingThread.checkBoundaryHit((int)event.getX(), (int)event.getY()); break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "TouchEvent: Action move"); break; default: Log.d(TAG, "TouchEvent: Default"); break; } return true; } private class MyDrawingThread extends Thread{ private static final int SPEED_MAX = 30; private static final int INIT_SPEED = 5; private static final int LEFT = -1; private static final int RIGHT = 1; private static final int UP = -1; private static final int DOWN = 1; private volatile boolean mRun = false; private Handler mHandler; private SurfaceView mSurfaceView; private Drawable mTargetImage; private Random mRandom; private int mSpeedX = INIT_SPEED; private int mSpeedY = INIT_SPEED; private Rect mDrawingBoundary; private Rect mTargetImageBoundary = new Rect(); private int mTargetImageWidth; private int mTargetImageHeight; private int mDirectionX = RIGHT; private int mDirectionY = DOWN; private Paint mPaint = new Paint(); public MyDrawingThread(String name, Handler handler, SurfaceView surfaceView, Context context){ super(name); //作成されたHandlerを別threadでもaccessできるようにする mHandler = handler; mSurfaceView = surfaceView; mTargetImage = context.getResources().getDrawable(R.drawable.icon); mRandom = new Random(); } private void initUiEnv(SurfaceView surfaceView){ mDrawingBoundary = ((MySurfaceView)mSurfaceView).getDrawingBoundary(); mTargetImageWidth = mTargetImage.getIntrinsicWidth(); mTargetImageHeight = mTargetImage.getIntrinsicHeight(); mTargetImageBoundary.left = 0; mTargetImageBoundary.top = 0; mTargetImageBoundary.right = mTargetImageWidth; mTargetImageBoundary.bottom = mTargetImageHeight; mPaint.setColor(Color.WHITE); } public void startDrawing(){ mRun = true; initUiEnv(mSurfaceView); this.start(); } public void stopDrawing(){ mRun = false; } public synchronized void checkBoundaryHit(int x, int y){ if(mTargetImageBoundary.contains(x, y)){ mSpeedX = mSpeedX + (mRandom.nextInt(2) * 5); mSpeedY = mSpeedY + (mRandom.nextInt(2) * 5); if(mSpeedX >= SPEED_MAX || mSpeedY >= SPEED_MAX){ mSpeedX = 0; mSpeedY = 0; //Handler#sendEmptyMessage()でMessageをUI threadのMessageQueueにenqueue mHandler.sendEmptyMessage(MySurfaceView.CLEAR); }else{ //Handler#sendEmptyMessage()でMessageをUI threadのMessageQueueにenqueue mHandler.sendEmptyMessage(MySurfaceView.HIT); } }else{ //Handler#sendEmptyMessage()でMessageをUI threadのMessageQueueにenqueue mHandler.sendEmptyMessage(MySurfaceView.NO_HIT); } } @Override public void run() { while(mRun){ Canvas canvas = mSurfaceView.getHolder().lockCanvas(); doDraw(canvas); mSurfaceView.getHolder().unlockCanvasAndPost(canvas); } } private void doDraw(Canvas canvas){ canvas.drawColor(Color.BLACK); updateBoundary(); mTargetImage.setBounds(mTargetImageBoundary); mTargetImage.draw(canvas); } private synchronized void updateBoundary(){ int newLeft = 0; int newTop = 0; newLeft = mTargetImageBoundary.left + (mSpeedX * mDirectionX); if(mDirectionX == LEFT && newLeft <= mDrawingBoundary.left){ mDirectionX = RIGHT; mTargetImageBoundary.left = mDrawingBoundary.left; mTargetImageBoundary.right = mTargetImageWidth; }else if(mDirectionX == RIGHT && (newLeft + mTargetImageWidth) >= mDrawingBoundary.right){ mDirectionX = LEFT; mTargetImageBoundary.right = mDrawingBoundary.right; mTargetImageBoundary.left = mTargetImageBoundary.right - mTargetImageWidth; }else{ mTargetImageBoundary.left = newLeft; mTargetImageBoundary.right = mTargetImageBoundary.left + mTargetImageWidth; } newTop = mTargetImageBoundary.top + (mSpeedY * mDirectionY); if(mDirectionY == UP && newTop <= mDrawingBoundary.top){ mDirectionY = DOWN; mTargetImageBoundary.top = mDrawingBoundary.top; mTargetImageBoundary.bottom = mTargetImageHeight; }else if(mDirectionY == DOWN && (newTop + mTargetImageHeight) >= mDrawingBoundary.bottom){ mDirectionY = UP; mTargetImageBoundary.bottom = mDrawingBoundary.bottom; mTargetImageBoundary.top = mTargetImageBoundary.bottom - mTargetImageHeight; }else{ mTargetImageBoundary.top = newTop; mTargetImageBoundary.bottom = mTargetImageBoundary.top + mTargetImageHeight; } } } }
今回の実装はHandlerを使用しなくてもTextViewの文言を変更することが出来ますが、学習の為にHandlerを使用してみました。
基本的にapi demosに入っていたLunar Landerをもとにしてます。(ちょっとはinteractiveになりましたかね。今後もこれ使って学習してこっかな。)
今回はHanlder#sendEmptyMessage(int)しか使ってませんが、enqueue絡みのmethodにはHandler#post(Runnable)やHandler#sendMessage(Message)もあるので、そちらも利用価値あります。