スレッドから別オブジェクトを触ると異常終了?! 解決編

 先日の「スレッドからオブジェクトをイジると異常終了する」件。
 2つの方法で解決した。

 おそらくは邪道な方法だと思うので、参考ぐらいにとどめた方がいいと思う。(^_^;

 前提となる、アプリの構造は以下のような感じだ。

	Activity
	  └GameRun (スレッド)
	     └TextView

※.この他にも、TextView と同列のView があるのだが、ここでは省略。

【動作】
 GameRun がスレッドとして動作し、表示メッセージをTextView へ送る。

【問題】
 GameRunスレッドから、setText や class変数の変更など、TextViewの操作を行うと異常終了する。

 解決するには、Handler を使用するしかないようだ。
 ただそれには不都合があり、今回は、別の方法を探ってみた。

リスナーを使う

 Handler ときて、最初に連想したのがこの方法だ。
 リスナーも通知先をハンドラという。
 ならば、リスナー経由で操作を行えば、根本の原因を回避できるのではないか?
 というワケ。

	GameRun
	 ↓ 通知 ( _Listener.setPadding( padding[] ); )
	TextView
	  method が実行され、変数に値が格納される。
	   setPadding( padding[] ){
	      mesPadding = padding;
	   }

 ただしこの方法は、必ずmethod を介さなければならない。
 上記の例でいえば、スレッドを使わない場合、次のようにシンプルだ。

	TextView.mesPadding = padding;

 ただの代入でも、method を介さなければならないのは、ちょいとシャク。
 ただそれだけの話ではあるんだけどね。

 この方法はことのほか、すんなり問題を解決した。
 ソースもすっきりシンプル。
 値を返すようなものにも使用できる。

リスナー と Handler を使う

 前記方法で問題は解決したように見えたが、別の箇所で再び同じ問題が起きた。
 そこではsetTextやLayoutView のremove を使っており、前記方法では解決されなかったのだ。
 おそらく、描画しているものに対して、直接操作を行おうとしたからだろう。

 解決策としてまず思い浮かんだのが、タイマーである。
 タイマーはキューを受けると、時間をおいて処理を実行する仕掛け。
 View側であらかじめ、setTextやremoveするようなタイマーを作り、スレッド側はキューを送る。
 処理の実行はView側なので、根本原因は回避できるハズだ。
 ……って、待て待て。
 それって、Handler そのものなんじゃね? (^_^;

 そこでView のmethod にHandler を仕掛け、↓のように改良してみた。
 結果は成功である。

View側 method

	private Handler mHandler = new Handler();
	private String mesStr = "";

	public void setMes( String mes ){
		mesStr = mes;
		mHandler.post( new Runnable() {
			public void run() {
				setText( mesStr );
			}
		});
		
	}

スレッド側 method実行

	_MesView.setMes( "うふふ……" );

 View側で、引数 mes を、class変数 mesStr に一旦代入しているのは、そうしないとrun内で認識されないから。(シンタックス・エラーになる)
 というのも、new Runnable() で暗黙的にThread を生成しているからだ。
 つまりrun内は、別class または別method の領域となってしまうのだね。
 それはわかるが、イマイチ好きになれない書き方…。
 まぁ、是非もナシやね。

 これで問題は解決し、もうひとつの問題が発生した。
 先のsetText をappend にした場合である。

	public void appendMes( String mes ){
		mesStr = mes;
		mHandler.post( new Runnable() {
			public void run() {
				append( mesStr );
			}
		});
	}

 append は現在表示しているText に文字列を追加するmethodである。
 たとえば、こんな動作をする。

	[ソース]
	setText( "うふふ……" );
	append( "えっち♪" );

	[表示動作]
	うふふ……
	 ↓append
	うふふ……えっち♪

 ソースからすると、上記のような動作になるハズなのだが。
 実際にやってみると…こうなってしまう。

	うふふ……
	 ↓append
	えっち♪えっち♪

 これはclass変数を介しているために起きてしまう現象だ。
 普段、意識しないが、Javaでの変数は参照が基本。
 内部的にsetText されたのは、class変数のメモリー位置で、class変数の内容が変われば、当然、変わってしまう…。
 おそらくは、スレッドを介しているのも関係があるんだろうね。

 そこでappend は使わず、自前で追加するように変更した。
 これで意図どおりに実行されるようになった。

	public void appendMes( String mes ){
		mesStr = mesStr+mes;
		mHandler.post( new Runnable() {
			public void run() {
				setText( mesStr );
			}
		});
	}

 以上の方法には、リスナーのみの時とは、異なる注意点がある。

1. 即応性がない。
 実行タイミングは、Handler(Looper) 任せなので、いつ実行されるか不明だ。
 付随して、返り値を求めるようなmethod には適応できない。

2. classローカルでも、Handler を介することになる。
 たとえば、例にあげたTextView自身がsetMesを実行すると、Handler を介した実行になる。
 なんだか、自分の尻を他人に拭いてもらうようで、まどろっこしい。(笑
 また、即応性の面からすると、潜在的なバグとなる可能性もある。
 必要なら、ローカル用とHandler用、別々のmethod を作った方がいいだろうね。

3. class変数で引数を保持する。
 先の失敗例のように、Handler が実行する前に、値が変わってしまわないようにしなければならない。

by the way

 setTextは、スレッドからの実行で、ちゃんと動作している場合と、異常終了する場合が見られた。
 これはsetTextだけの話ではない。
 おそらく動作する例は、その時はまだ画面に表示されていないか、UIがひとつだけの状態だったのが原因ではないかと思う。
 一度だけの実行で、不具合ナシと、安心してはいけないね。

補足

 Handler の使い方は、まだよく把握していない。
 見よう見まねの試行錯誤で、「一応、動作している」にすぎない。
 もっとうまく、正しい方法もあると思う。
 ちゃんとした解説を見て、試して、よく理解する必要があるね。(^_^;

 しかし…。
 なんだかJavaというより、Cocoaって感じの構造になったな。(w

Posted in Listener, 雑記. Tags: , , , . スレッドから別オブジェクトを触ると異常終了?! 解決編 はコメントを受け付けていません。 »

選択肢メニューを作ってみた

タイトル画面には、それぞれのモードへ行くためのボタンが必要だね。
フツウにボタンを配置してもいいんだけど、それだとタイトル画面専用になっちゃう。
別にかまわないんだけど、せっかくなんで、汎用選択肢メニューとして作ってみた。

仕 様

  • 選択肢ボタンは、選択肢専用 ViewGroup に属する。
  • ボタン生成と共に、それぞれへユニークなIDがふられる。
  • ボタンを押されると、それぞれのIDをハンドラ へ送る。
  • ハンドラはすぐにViewGroup とボタンを非表示にする。


 で、メニュー(ハンドラ) が保持するIDを見れば、なにが選択されたかわかる。
 もしくは、メニューからリスナーへの通知にしてもいいね。

 …って、ListView そのものじゃん! Σ
 じゃ、ListView をベースに作ればいいか…と、ちょっとやってみたがうまくいかない。
 どうもXMLを使わないと面倒らしい…。

「AndroidでListView」
 XMLを使わない場合だと、自前でBaseAdapterクラスを拡張して、getView()の中でList内の要素を生成しないといけないらしい

 おそらく、BaseAdapter がボタンからの通知を統括してたりするんだろうね。
 当初の予定どおり、自前で作るとするか。
 まぁ、Open & Close のエフェクトとかを考えたら、その方が融通効きそうだし。

プロトタイプ

 で、こんな感じで作ってみた。

package com.migimaki.android;

import android.content.Context;
import android.widget.LinearLayout;
import android.view.Gravity;
import android.graphics.Color;
import android.widget.TextView;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;

import android.util.Log;


public class SelectMenu extends LinearLayout implements OnClickListener{

	private Context context = null;
	private onSelectListener _listener = null;		//リスナー

	//
	public int chois = 1;	// 選択回数カウンター。残り値分、選択可能。
	
	public boolean selected = false;	// 選択されたか?
	public int select_id = -1;	// 選択されたボタンID
	//--
	
	//==
	
	
	public SelectMenu ( Context c ){
		super( c );
		context = c;
		//--
		setOrientation(LinearLayout.VERTICAL);		// View配置設定 : 縦並び
	}


	// init & make Menu
	public void init( String menuMes, String butName[] ){
		chois = 1;
		selected = false;
		select_id = -1;
		//--
		setGravity(Gravity.CENTER);		// センター配置
		
		// Padding
	//	setPadding( 100, 100, 200, 200 );
	//	setPadding( -1, -1, -1, -1 );	// Padding 未設定の状態に初期化
		//--
		
		// メニュー・メッセージ
		addMes( menuMes );
		
		// ボタン作成
		for ( int i=0; i<butName.length; i++ ){
			addButton( i, butName[i] );
		}
		
		//	setVisibility( View.VISIBLE );
	}

	
	// make Text Mes
	public void addMes( String mesStr ){
		TextView mes = new TextView( context );
		mes.setText( mesStr );
		
		mes.setBackgroundColor(Color.argb( 128, 0, 0, 255 ));	// 背景色設定
		mes.setHeight(40);		// 高さ設定
		mes.setTextSize(20.0f);	// フォントサイズ
		mes.setGravity(Gravity.CENTER);		// センター表示
		
		addView(mes);
	}
	
	// make Button
	public void addButton( int id, String butName ){
		Button but = new Button( context );
		but.setId( id );
		but.setText( butName );

		//	mes.setBackgroundColor(Color.argb( 128, 0, 0, 255 ));	// 背景色設定
		mes.setHeight(40);		// 高さ設定
		mes.setTextSize(20.0f);	// フォントサイズ
		mes.setGravity(Gravity.CENTER);		// センター表示

		addView(but);
		but.setOnClickListener( this );	// onClick の設定
		
//	Log.d("TEST", "add but(" +id +")"+" : " +but.getId() );
	}
	
	// close : remove All Views
	public void close( ){
		removeAllViews();
	}

	// Event : click
	public void onClick( View but ){
		
		if ( chois > 0 ){
			selected = true;
			select_id = but.getId();
			
			if ( _listener != null ){
				_listener.onMenuSelected( select_id );	// イベントの通知( -> Activty )
			}
			chois--;
			//--
			
			// 選択されたボタン以外を非表示にする。
			if( chois <= 0 ){
				int child = getChildCount();
				for ( int i=1; i<child; i++ ){
					if( i != ( select_id +1 ) ){
						getChildAt( i ).setVisibility( View.INVISIBLE );
					}
				}
			}
			//--
		}
		//--
//	Log.d("TEST", "Who click? : " +but.getId() +"  select_id : " +select_id );
	}
	
	
	
	////////////////////////////////////////////////////////////////

	// イベントの通知先
	public interface onSelectListener{
		public void onMenuSelected( int select_id );
	}

	// リスナーの登録
	public void setOnSelectListener( onSelectListener listener ){
		_listener = listener;
	}
	////////////////////////////////////////////////////////////////

}

●SelectMenu のオブジェクトを生成後、表示したいViewGroup にadd すれば使用できる。

●表示位置とボタンの横幅は、setPadding で設定する。
 setPadding はLinearLayout に元々あるmethod なので、オブジェクトに対しても設定できるよ。

●init() で、メニュー・メッセージと各ボタン名の設定。
 method を実行すると、メッセージのTextViewとボタンを生成して、メニューを表示する。

●選択肢が決定されると、ボタンのIDがselect_id に保持される。
 また、リスナー登録しておくと、select_id を通知してくれる。

●close() で、メニューを非表示にする。
 すべてのメッセージとボタンをLinearLayout から削除する。
 オブジェクトは生きてるので、init() すれば新しいメニューが表示されるよ。

●ちょっとお遊びで、chois という変数をつけてみた。
 chois回数分だけ、ボタンを押せる。
 正解を選ぶとか、正しい順番を押させるとか、…そんな使い方。

課 題

・ボタンの外観
	今はデフォなので味気なし。
	カーソルと、押された時の反応も変えるべし。
・ボタン・サイズ
	文字サイズと共に、今は固定。
	文字列に合わせて変わる、指定設定できるようにする。
・Open & Close エフェクト
	パッと表示されるだけじゃ味気ない。
	なにかエフェクトをつけよう。

 それと、ソースの洗練もね。(w

Posted in メニュー, 選択肢. Tags: , , , . 選択肢メニューを作ってみた はコメントを受け付けていません。 »

Listener ~リスナーってなによ?

 てっとりばやくいうと、onDraw()とかがソレ。
 正確にはイベント・リスナーというらしい。

 class 外部でイベントが発生すると、それに対応したmethod が実行される…と。
 onDraw() でいえば、

	1. 画面更新イベントが発生
	2. OSがclass のonDraw() を実行

 で、onDraw()の実行処理が「イベントハンドラ」。
 こんなトコだろうかね。

 ボタンなんかもそうだね。
 押されたら、その処理をbutton.classの中で行うんじゃなくて、別のclass でやってもらう。

	button.class
	「ボタン」
	    ↓押された
	View.class
	    onClick()
	    メッセージ表示
	    「押された」

リスナーが「通知先」で、その処理が「イベントハンドラ」。

 こういう仕組みが、Android にはそこかしこにある。
 おそらく、ViewのonDraw() のような、よく使うもの、必須となるようなものはあらかじめclassに登録されているのだろうね。
 では、登録されていないものは…?
 implements でそれを埋め込むことになる。
 つまり、「このclass で、○○イベントを受け取ります」って「宣言」するってことやね。

で? その構造はどうやって作るのよ?

 自前classでも作りたくなることは多々あるよね。
 例えば、スレッドで処理をしていて、その処理が終わったとか、結果を別classへ送りたいとか。
 複数スレッドを動かしてると、特に必要性を感じるね。

 ということで。
 「ボタン」を例に、サンプルを作ってみた。

	[クリック]
		↓
	MyButton.onTouchEvent()
		↓[通知]
	_listener.onClick()
		↓
	[メッセージ更新]

	◆listener_Test
		・2つのオブジェクトを生成。
		・MyButton のリスナーに、ListenerViewを登録
		  _listener = ListenerView
	■MyButton
		onTouchEvent()
		・クリック・イベントを受信したら、リスナーに通知。
		  _listener.onClick()
	■ListenerView
		 onClick()
		・イベントを受信すると、メッセージを更新

listener_Test.java

package com.migimaki.android;

import android.app.Activity;
import android.os.Bundle;
import android.widget.FrameLayout;
import android.view.ViewGroup;

import com.migimaki.android.MyButton.onClickListener;

public class listener_Test extends Activity {

	private final int WC = ViewGroup.LayoutParams.WRAP_CONTENT; 
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		FrameLayout frameLayout = new FrameLayout(this);

		// オブジェクトを生成
		ListenerView LV = new ListenerView(this);
		MyButton myButton = new MyButton( this );
		
		// リスナー登録
		myButton.setOnClickListener( (onClickListener)LV );
		
		
		// レイアウト追加&表示
		frameLayout.addView( LV, createParam(WC, WC));
		frameLayout.addView( myButton, createParam(WC, WC));
		setContentView(frameLayout);
	}
	
	private ViewGroup.LayoutParams createParam(int w, int h){
		return new ViewGroup.LayoutParams(w, h);
	}

}

MyButton.java

package com.migimaki.android;

import android.content.Context;
import android.widget.ImageView;
import android.view.MotionEvent;

public class MyButton extends ImageView {
	
	public MyButton( Context c ){
		super( c );
		setImageResource( R.drawable.icon );
	}

	private onClickListener _listener = null;		//リスナー

	// リスナーの登録
	public void setOnClickListener( onClickListener listener ){
		_listener = listener;
	}

	// click イベント
	public boolean onTouchEvent( MotionEvent event ){
		_listener.onClick();	// イベントの通知
		return true;
	}

	// イベントの通知先
	public interface onClickListener{
		public void onClick();
	}

}

ListenerView.java

package com.migimaki.android;

import android.content.Context;
import android.widget.TextView;
import android.graphics.*;

import com.migimaki.android.MyButton.onClickListener;

public class ListenerView extends TextView implements MyButton.onClickListener {

	public ListenerView( Context c ){
		super( c );

		setPadding( 50, 50, 200, 300);	//文字表示エリア
		setTextColor( Color.WHITE );
		setText("Listenerテスト");
	}

	// イベントの受信
	private int count=0;
	public void onClick(){
		count++;
		setText( ""+count+ "回、押された?!" );
	}

}

 作ってみたものの…イマイチなサンプルだなぁ。(^_^;
 本当は、2つのclassで納めたかったんだけど…。
 ボタンじゃなく、スレッドを例にすればよかったかな。

 ちなみに、interface 宣言したものは、implements 先で設置が求められる。
 設置されていないと、eclipse がエラー出してくれるよ。

Posted in Listener. Tags: , . Listener ~リスナーってなによ? はコメントを受け付けていません。 »