スポンサーリンク

Android SDK の動かないコード(中級編) invalidate しても再描画されないエラー


以下のAndroidアプリのコードが意図した動作をしないのは,なぜですか。

(制限時間1分)


やりたい事:

  • ボタン押下時に,ImageView上に画像を2枚連続で表示する。表示のタイミングをずらす事により,疑似的にスライドのように見せたい。(Gifアニメ的なものを作りたい)
package com.example;

import com.example.R;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;


public class CodeTestActivity extends Activity implements OnClickListener
{
    Button button1;
    ImageView imageview1;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // UI部品を取得
        button1 = (Button)findViewById(R.id.button1);
        imageview1 = (ImageView)findViewById(R.id.imageview1);

        // ボタンにイベントをセット
        button1.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        // Android SDK(android.R)に内蔵されている画像を2つ表示する。
        // 間隔を空けて表示することにより,アニメーションのように見せる。


        // 1つ目の画像を表示する(検索ルーペのアイコン)
        imageview1.setImageResource(android.R.drawable.ic_menu_search);
        imageview1.invalidate(); // ビューを再描画し,UI上で画像変更を反映

        // 2秒待って,タイムラグを生む
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ignore) {
        }

        // 2つ目の画像を表示する(フロッピーの保存アイコン)
        imageview1.setImageResource(android.R.drawable.ic_menu_save);
        imageview1.invalidate(); // ビューを再描画し,UI上で画像変更を反映

    }
}


レイアウト用のmain.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >


	<!-- ボタン -->
	<Button
		android:id="@+id/button1"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="画像を連続で表示"
	/>


	<!-- 画像 -->
    <ImageView
        android:id="@+id/imageview1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"
    />


</LinearLayout>

発生する問題

ボタンを押しても,1番目の画像が表示されない。

しばらくたってから2番目の画像が表示されるだけ。


invalidate() メソッドを呼んで,ImageViewの再描画を強制しているにもかかわらず,

1番目の画像を表示した後の再描画が行なわれていない。

invalidate() メソッドが,ちゃんと機能していないように見える。なぜか?


原因

View#invalidate() は,「呼ばれた瞬間にすぐ再描画するメソッド」ではない。

UIスレッドが次回,アイドル状態になり次第,できるだけ早く再描画するメソッド」である。

Why isn't view.invalidate immediately redrawing the screen in my android game
(訳:Androidでゲーム作ってるんだけど,Viewにinvalidateかけても画面がリフレッシュしない。どうして?)
http://stackoverflow.com/questions/14...

回答:

View#invalidate tells the system to redraw (via onDraw) the view as soon as the main thread goes idle. That is, calling invalidate schedules your view to be redrawn after all other immediate work has finished. If the code in your Game class is being called from the main thread and it puts that thread to sleep, you aren't just pausing rendering, you are pausing input processing all together (usually a bad idea). As a rule of thumb, never sleep a thread that you didn't spawn yourself unless you know what you are doing.

If you'd like to have your game logic periodically delay using Thread#sleep then run it in a separate thread and use view.postInvalidate() to signal the main thread to wake up and call onDraw.


訳:

View#invalidateは,メインスレッドがアイドル状態になり次第ビューが(onDraw経由で)再描画されるように,システムにお願いするメソッドですよ。

つまり,invalidateをコールすると,今やっている全部の仕事が終わったら再描画されるよう,該当ビューに対してスケジューリングする事になります。

もしゲーム作ってて,あるクラスがメインスレッドから呼び出されていて,スレッドにsleep()をかけたとしたら,画面のレンダリングを停止させてるのみならず,その間は何の入力もできなくなるよ。まあ大抵,良いアイデアじゃないよ。

経験から言うと,意味が分かった上でやってるんじゃない限り,自分で起動したんじゃないスレッドをsleepさせるべきじゃないね。


ゲームのロジック内でThread#sleepを使って定期的に遅延を発生させたいなら,それは別スレッドで走らせるようにして,メインスレッドに対してはview.postInvalidate() を呼んでonDrawされるようにしたらいいよ。

書籍「Androidゲームプログラミング A to Z」のp149にも,下記のようなくだりがある。


UIスレッドでの連続的なレンダリング:

invalidate() は,UIスレッド上で都合がつき次第再描画をする。




そういうわけで,同一スレッド内で下記のようなステップを踏んでも,効果はないのである。

  • 画像1を表示:無効
  • invalidate():無効(再描画はスケジューリングされるだけで,直ちに実行されるのではない)
  • 数秒sleep()
  • 画像2を表示:有効
  • invalidate():有効

同一スレッド上で順番に処理している限り,メインスレッドがふさがった状態になってしまい,最後に行なったUI操作だけが意味を持つ事になる。


解決策

同一スレッドで処理している事が問題なので,別スレッドに分ければよい。

この手の処理は,Handlerを定義して,別スレッド上からHandler経由でUIスレッドにpostする,という手を取るケースが多い。

Android で再開する Java プログラミング(2) - 図形の描画
http://www.hakkaku.net/articles/20090...

  • 定期的に再描画を行うために、Timer クラスを利用しています。この中で、invalidate() メソッドを呼び出すことで、再描画を促しています。(直接、onDraw() メソッドを呼ぶべきではありません。)
  • Timer を使った場合、イベントは別スレッドで実行されますので、定期的なタイマーを処理する中で、GUI を操作する invalidate() メソッドを呼ぶことはできません
  • そこで、android.os.Handler を利用して、post() メソッドを利用して GUI を更新するようにします


画面を定期的に再描画する
http://andromemo.seesaa.net/article/1...

  • Androidでは、UI関連のメソッドはUIスレッドからしか呼び出せないため、別スレッドを作ってループさせ、その中でinvalidateを呼び出しても、上手く行きません。そこで、MessageとHanelerを使って実装します


だが,Handlerオブジェクトを生成しなくても,Activity#runOnUiThreadで済んでしまう。

こちらのほうがコードがシンプルである。


その戦略で,冒頭のコードを正常に動作するように書き換えてみる。

package com.example;

import com.example.R;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;


public class CodeTestActivity extends Activity implements OnClickListener
{
    Button button1;
    ImageView imageview1;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // UI部品を取得
        button1 = (Button)findViewById(R.id.button1);
        imageview1 = (ImageView)findViewById(R.id.imageview1);

        // ボタンにイベントをセット
        button1.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        // Android SDK(android.R)に内蔵されている画像を2つ表示する。
        // 間隔を空けて表示することにより,アニメーションのように見せる。


        // 1つ目の画像を表示する(検索ルーペのアイコン)
        imageview1.setImageResource(android.R.drawable.ic_menu_search);

        // ↓この行は無くてもいい
        imageview1.invalidate(); // ビューを再描画し,UI上で画像変更を反映


        // この時点で,別スレッドに処理をゆだねる
        final Activity activity = this;
        new Thread(new Runnable(){

            @Override
            public void run() {
                // これは別スレッド上での処理


                // 2秒待って,タイムラグを生む
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ignore) {
                }


                // UIスレッド上で,
                // 2つ目の画像を表示する(フロッピーの保存アイコン)
                activity.runOnUiThread(new Runnable(){
                    @Override
                    public void run() {
                        imageview1.setImageResource(android.R.drawable.ic_menu_save);
                        imageview1.invalidate(); // ビューを再描画し,UI上で画像変更を反映
                    }
                });
            }

        }).start();

    }
}

これでOK。

ボタンを押下すると,1秒間はルーペアイコンが現れ,そのあとでフロッピーアイコンが表示される。

簡易的なスライドショーが実現できる。


このケースだと,invalidate() しなくても,スレッドをブロックしなければ自然に再描画してくれるので,invalidate() の行を消しても大丈夫。


補足:invalidate() に関する補足情報

invalidate() は,意外といろんなところで使われている。


ただし,コーディング時には意識しないで済むように隠ぺいされているのがほとんど。

Viewの状態変更系のメソッドは,いずれも内部的にはinvalidate() している。

そして,invalidate() はonDraw() を呼び出している。

AndroidのHandlerとは何か?
http://www.adamrocker.com/blog/261/wh...
基本的にinvalidate()で画面の描画を指示します。
例えば、TextView.setText(…)も、内部ではinvalidate()を呼出しています。


How to force an entire layout View refresh?
http://stackoverflow.com/questions/59...

  • when the Activity resumes, it makes every View to draw itself. No call to invalidate() should be needed.
  • ※アクティビティがonResume状態になった時,全Viewは自動的に再描画されるので,明示的にinvalidate() する必要はない


ViewのCanvasに円を描画するサンプルコード
http://www.hakkaku.net/articles/20090...

  • 画面の描画は、onDraw() メソッドの中で行うことになっています。この onDraw() メソッドは、View の処理の中で、描画が行われるタイミングごとに呼び出される仕組みになっているのです


指でタッチした場所に画像表示するAndroidアプリを作成する
http://www.techfirm.co.jp/lab/android...

  • invalidate(); でonDraw(Canvas canvas)が呼ばれ、画面が再描画されます。


SurfaceViewならAndroidで高速描画ゲームが作れる
http://www.atmarkit.co.jp/fsmart/arti...

  • 通常のViewはinvalidate()を呼び出して、間接的にonDraw(Canvas)を呼び出します。これまでのJava(Java SEやJava ME)は「repaint()」というメソッド名でしたが、AndroidではWindows APIっぽい「invalidate()」というメソッド名が使用されています


このように,invalidate() の中で onDraw() が呼ばれるわけだが,両者の違いは他にもある。

Android 開発ガイド > フレームワークトピック > b. グラフィックス
http://www.techdoctranslator.com/andr...

  • View クラス ( またはその派生 ) を拡張 し、onDraw() コールバックメソッドを定義します。このメソッドは自身のView の描画を要求するために、 Android フレームワークにより呼び出されます。Android フレームワークは、必要に応じて onDraw() を呼び出すだけです。
  • アプリケーションで描画の準備ができたタイミングで、毎回 invalidate() を呼び出し、View が無効化されるように要求する必要があります。これは、View が描画されることを望んでいて、Android がそれを受け onDraw() メソッドを呼び出すということを意図しています ( が、このコールバックが即座に呼び出されるという保証はありません ) 。

つまり

  • onDraw() : canvasに対する処理内容を記述するメソッド。Androidフレームワーク側で呼ぶ。描画を実際に実行する時点でのコールバックメソッド。
  • invalidate() : ユーザがこのメソッドを呼び出す事により,Androidフレームワークに対して,都合がつき次第 onDraw() を呼ぶように要求するメソッド。リクエストを出すだけであり,onDraw() は即座に実行されるわけではない。

という違いがある。



ところで,「invalidate」という単語自体は,

英語では「失効させる,無効にする,取り消す」といった意味がある。


やりたい事は「再描画」なのに,どうして「無効にする」という語が当てられているのか?

それは,Win32APIや.NETの世界の事情を知るとイメージがつかめる。

Refresh、Update、Invalidateメソッドの違い
http://dobon.net/vb/dotnet/control/re...

  • Invalidate メソッド : コントロールの特定の領域を無効にし、そのコントロールに描画メッセージを送信します。


再描画
http://wisdom.sakura.ne.jp/system/msn...
ウィンドウが他のウィンドウに隠れたり、最小化されたりすると
その部分は画面から消えるため、情報を失います

そして、またウィンドウが表示されると隠された部分は無効な状態になっているため
無効リージョンを再び描画する必要があるので、OnPaint() が呼び出されるのです

明示的に再描画してほしいという場合があるでしょう
例えばイベントによって情報が変化し、クライアント領域を再描画するなどです

再描画には Control.Invalidate() メソッドを使います
このメソッドは、強制的に無効リージョンを作りだして再描画します

UI領域の「無効化」と「再描画」の関係がつかめただろうか。

UIの中に無効な領域が発生するからこそ,再描画が必要になる。

だから,再描画するとは,UI中の無効な領域に対する処理と言い換える事も出来るのだ。



話をAndroidに戻すと,

MapViewでマップを表示させるときなんかには,invalidate() に頻繁にお世話になる。

例えば,「animateTo() で地図の中心をずらした後は,invalidate() による明示的な再描画処理は必要なのか?」といった疑問と闘いながらのコーディングになる。(答えは,不要。やはり内部的に隠ぺいされているから。)

Androidアプリで,Google Maps API+GPS+Geocoderを使って,現在地の地図と地名を表示させよう
http://language-and-engineering.hatenablog.jp/entry/20110828/p1

なお,冒頭のコードでは「Android SDKに最初から組み込み済みのアイコン画像」を使用した。

それらの画像の一覧表は,下記のURLなどで閲覧できる。

Android R Drawables Taken from Android 2.2
http://androiddrawableexplorer.appspo...


関連する記事:

Android SDK の動かないコード(中級編) 端末を「縦横切り替え」すると,ダイアログやアクティビティが死に WindowLeaked エラー
http://language-and-engineering.hatenablog.jp/entry/20110905/p1


Android SDK の動かないコード(初級編) ダイアログ上の要素にアクセスするとNullPointerExceptionになるエラー
http://language-and-engineering.hatenablog.jp/entry/20110908/p1


Android SDK の動かないコード(中級編) ListView内の要素にアクセスしようとするとNullPointerExceptionで落ちるエラー
http://language-and-engineering.hatenablog.jp/entry/20111013/p1


Androidで,「ビットマップのピクセル操作」をリアルタイムに実行するサンプルコード
http://language-and-engineering.hatenablog.jp/entry/20120626/AndroidManipulat...