汎用的な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を持つビューは取得できない。