汎用的なStatusLineを作る - part3
次はタイトル(Title)を使ったステータスラインの実装だ。元々こちらの機能をメインに考えているが、いろいろと仕掛けが必要だ。
Titleとは?
以前にもこの日記でとり上げたことがあるが、androidにおけるActivityはデフォルトで"android.R.id.title"というIDで定義されたビュー(TextViewクラス)をウインドウの一番上(システムステータスバーの真下)に配置する。今回の実装はこの"タイトル"をステータスラインとして利用する。
TitleStatusLine.java
public class TitleStatusLine implements IStatusLine { protected final Activity activity; protected Drawable errorIcon; protected final String origTitle; protected CharSequence lastMessage; protected CharSequence errorMessage; protected CharSequence guideMessage; protected TextView title; public TitleStatusLine(Activity activity, Resources resource) { this.activity = activity; Resources res = ( resource != null ) ? resource : activity.getResources() != null ? activity.getResources() : activity.getApplication().getResources(); this.errorIcon = resource.getDrawable(R.drawable.indicator_input_error); if ( activity != null ) { this.title = (TextView)activity.findViewById(android.R.id.title); if ( this.title == null && activity.getParent() != null ) { this.title = (TextView)activity.getParent().findViewById(android.R.id.title); } } if ( this.title != null ) { this.origTitle = this.title.getText().toString(); } else { this.origTitle = ""; } if ( this.title != null ) { activity.runOnUiThread(new Runnable() { @Override public void run() { title.setSingleLine(); title.setEllipsize(TruncateAt.MARQUEE); title.setHorizontallyScrolling(true); title.setFocusable(true); title.setFocusableInTouchMode(true); } }); } @Override public CharSequence getErrorMessage() { return this.errorMessage; } @Override public void setErrorMessage(final CharSequence message, final View... views) { this.errorMessage = message; if ( this.isStatusLineExist() ) { this.notificationError(message); this.lastMessage = message; } if ( views != null && views.length > 0 ) { this.getActivity().runOnUiThread(new Runnable() { @Override public void run() { for ( View v : views ) { v.requestFocus(); break; } } }); } } @Override public void setMessage(CharSequence message, final View... views) { this.guideMessage = message; if ( this.isStatusLineExist()) { this.notificationNormal(message); this.lastMessage = message; } if ( views != null && views.length > 0 ) { this.getActivity().runOnUiThread(new Runnable() { @Override public void run() { for ( View v : views ) { v.requestFocus(); break; } } }); } } private void notificationError(final CharSequence message) { final Activity a = this.getActivity(); if ( a != null ) { a.runOnUiThread(new Runnable(){ @Override public void run() { a.getWindow().setFeatureDrawable( Window.FEATURE_LEFT_ICON, errorIcon); a.setTitle(message); if ( title != null ) title.requestFocus(); }}); } } private void notificationNormal(final CharSequence message) { final Activity a = this.getActivity(); if ( a != null ) { a.runOnUiThread(new Runnable(){ @Override public void run() { a.getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null); a.setTitle(StrUtil.isNotEmpty(message) ? message : origTitle ); if ( title != null ) title.requestFocus(); }}); } } @Override public void resetErrorMessage() { this.errorMessage = ""; this.lastMessage = ""; this.notificationNormal(""); } @Override public CharSequence getMessage() { return this.guideMessage; } @Override public void invalidateCurrentMessage() { if ( this.isStatusLineExist() ) { this.notificationNormal(this.lastMessage); } } private Activity getActivity() { return this.activity; } }
以下ポイント。
コンストラクタ中
public TitleStatusLine(Activity activity, Resources resource) { this.activity = activity; : : this.errorIcon = resource.getDrawable(R.drawable.indicator_input_error); : : if ( this.title != null ) { activity.runOnUiThread(new Runnable() { @Override public void run() { title.setSingleLine(); title.setEllipsize(TruncateAt.MARQUEE); title.setHorizontallyScrolling(true); title.setFocusable(true); title.setFocusableInTouchMode(true); } }); }
コンストラクタの引数にわざわざResourceを指定しているのは、リソースが必ずしもActivityに紐付けされたものであるとは限らないから。(R.drawable.indicator_input_errorは独自に作成したアイコン)
タイトルに対して必要な属性を設定しているが、runOnUiThreadを使用しているのはUIスレッド外から呼ばれる可能性が排除できないから。EllipsizeにTruncateAt.MARQUEEをセットしているのはこのステータスラインに表示するメッセージをマーキー表示としたいからだ。
setErrorMessageメソッド中
if ( views != null && views.length > 0 ) { this.getActivity().runOnUiThread(new Runnable() { @Override public void run() { for ( View v : views ) { v.requestFocus(); break; } } }); }
エラー表示後、に引数で指定があった場合はそのビューにフォーカスを設定している。このメソッドもやはりUIスレッド外から呼ばれる可能性があるので、runOnUiThreadを使用している。
notificationErrorメソッド
a.getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null); if ( title != null ) title.requestFocus();
Window#setFeatureDrawableメソッドを使ってコンストラクタで取得したエラーアイコンを左端に表示している。また、マーキー表示をさせるためにはタイトル自身にフォーカスが必要なためrequestFocusを実装している。
なお、エラーアイコンの表示はこの操作だけでは駄目で、使用するActivity側でsetContentViewメソッドの実行前までに以下の操作を実行する必要がある。
this.requestWindowFeature(Window.FEATURE_LEFT_ICON);
クラスの責務からいえばこの処理はステータスライン側で行うのが定石だが、Windowクラスはルートコンテンツが設定された以降、このメソッドの実行には例外を投げるので、Activity側それもsetContentView実行前に呼ばなければならない。(私はActivityの移譲クラスから呼んでいる)
Code snipet (Activityのライフサイクル中を前提にしている)
: IStatiusLine statusLine = new TitleStatusLine(this, null); : : statusLine.setErrorMessage("エラーですよ! マーキー表示が判りやすいように長い文字列にしていますよ!");
実行結果
トーストを利用した実装に比べて書くコードが多いが、メッセージをスティッキーにしたり、エラーアイコンを表示したり、マーキー表示をしたりと、ユーザへより強く訴求できるのがTitleStatusLineの良い所だ。PhoneWindowの実装に依存していたり(恐らくHoneyCombでは動かない)、画面の構成によっては使えない※ケースもあるが、私はトーストよりもこちらの方を好んで使う。アプリケーションの用途や制限によって使い分けるのが良いだろう。
※マニフェストのactivity要素中テーマにタイトルバー無し(android:theme="@android:style/Theme.NoTitleBar")を指定した場合、"android.R.id.title"というIDを持つビューは取得できない。