んでもってHandler

今回はHandlerを試してみました。前回、SurfaceViewの復習で作成したcodeを利用し、実装をしてみたいと思います。

Handlerとは

HandlerはHandler objectを作成したThreadとそのMessageQueueに紐づけられます。Handler object作成後は紐づけられたMessageQueueにMessageやRunnableをenqueueしたり、enqueueされたMessage objectやRunnableを実行します。(原文ここ)

Handlerの使用用途は主に2つ

  1. MessageやRunnableで定義された処理をscheduling
  2. 別の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..."と表示するというものです。

実装の要点をまとめます

  1. UI thread上でHandlerを作成する
  2. 作成されたHandlerを別threadでもaccessできるようにする
  3. touch eventを契機にSurfaceのDrawingThreadからHandler#sendEmptyMessage()でMessageをUI threadのMessageQueueにenqueue
  4. Handler#handleMessage()をoverrideし、そこで、Messageの結果をUIに反映する処理を行う

気づいた点を記載します

  • 始め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)もあるので、そちらも利用価値あります。

HashTag #android, #java