2012年5月30日水曜日

動的にImageViewのサイズと位置を同時に変更する

動的にImageViewのサイズと位置を同時に変更する

動的にImageViewのサイズと位置を変更することは、簡単そうに思える。本来なら簡単であらねばならない。しかし、苦労をしたので備忘録として書き留めておく。

静的にサイズを決定するのであれば、xmlにサイズを記載すれば良い。

TextViewやButtonは、その枠の大きさを動的に変更するメソッドが用意されている。そして、また、TextViewやButtonに多くの文字列が代入された場合、TextViewやButtonの枠が自動的に拡張されて、挿入した文字列の全てが表示される。

このような機能がImageViewにも用意されているだろうと期待する。しかし、残念なことに、そのようなものは無い。

ImageViewに描画されている画像を拡大しても、ImageViewのは拡大しない。画像を拡大した場合、ImageViewの枠内に収まっている画像は表示される。しかし、枠外に行ってしまった部分の画像は表示されない。
例えば「能」と描いた画像をImageViewに表示していたとしよう。この画像を2倍に拡大した場合、ImageViewには「ム」という画像(能という文字の左上部分)が表示される。「月」や「ヒ」は表示されない。
ImageViewのの大きさと、ImageViewに表示される画像の大きさとは、別物であり、各々別々に取り扱わねばならない。むむむ。

では、どのようにして、ImageViewの枠を拡大するのであろうか。
ImageViewを載せたxmlは次のとおりである。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    >
    <TextView
        android:text="@string/Menu_PieceSize"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textAppearance="?android:attr/textAppearanceMedium"
        >
    </TextView>
    <SeekBar
        android:id="@+id/SeekBarPieceSize"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
    </SeekBar>
    <TextView
        android:id="@+id/TextPieceSize"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        >
    </TextView>
    <ImageView
        android:id="@+id/ImagePieceSize"
        android:contentDescription="@string/Menu_PieceSize"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:src="@drawable/ic_launcher"
        android:scaleType="matrix"
        >
    </ImageView>
</LinearLayout>

ここでのポイントはImageViewのandroid:layout_widthやandroid:layout_heightに"fill_parent"を設定していることだ。xmlにおいて、ImageViewの枠を画面一杯に広げてしまおう、ということである。この技法により、枠に関する問題は解消する。

次の話題は、画像の拡大・縮小と移動とを同時に行うことである。これはJavaでMatrixを使って実現する。
下記のsample codeは、SeekBarをスライドすれば、その移動量に応じて、ドロイド君が拡大・縮小する。
拡大・縮小は、ImageViewの左上端を起点にして行われる。画像を常に画面の中央に位置付けるために、拡大・縮小の程度に応じて、画像の左上端を上下左右に移動させている。
public class PieceSizeActivity extends Activity

    implements
    OnSeekBarChangeListener
    {
    final String TAG = "PieceSizeActivity";
    SeekBar sb;
    ImageView iv;
    final int iMulti = 4;//1倍から4倍までの間で拡大縮小させる。
    final int iMaxSeekBar = 30;//1.0から4.0まで31個の目盛がある。
    int iImageSize;//ドロイド君の画像の一辺の長さ
    int iPieceSize;//利用者が選択した長さ
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.piecesize);
        Intent it = getIntent();
        
        //他のActivityからstartActivityForResultメソッドで呼ばれる。
        iPieceSize = it.getIntExtra("PieceSize", -1);
        if(iPieceSize<100 || iPieceSize>400) iPieceSize = 100;
        float f = iPieceSize;//整数から浮動小数点数へ変換する。
        f /= 100;//小数点数にする。
        TextView tv = (TextView)findViewById(R.id.TextPieceSize);
        tv.setText(String.valueOf(f));//小数点数を画面に表示する。
        
        sb = (SeekBar)findViewById(R.id.SeekBarPieceSize);
        sb.setMax(iMaxSeekBar);//最大値設定
        iPieceSize /= 10;
        iPieceSize -= 10;
        sb.setProgress(iPieceSize);//現在値設定
        sb.setOnSeekBarChangeListener(this);
        
        //ドロイド君の画像サイズを取得する。
        Resources r = getResources();
        Bitmap b = BitmapFactory.decodeResource(r, R.drawable.ic_launcher);
        iImageSize = b.getWidth();
        
        //ImageViewのアドレスを取得
        iv = (ImageView)findViewById(R.id.ImagePieceSize);
    }
    
    @Override  
    public void onWindowFocusChanged(boolean hasFocus) {  
        super.onWindowFocusChanged(hasFocus);
        if(iPieceSize>=0){//起動直後に表示する画像の設定
            Matrix m = new Matrix();
            m = new Matrix();
            float f = iPieceSize + 10;
            f /= 10;
            float fX = iImageSize * f;
            fX = (sb.getWidth() - fX) / 2;
            m.setTranslate(fX, 0);
            m.preScale(f, f);
            iv.setImageMatrix(m);
            iPieceSize = -1;
        }
    }  

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        if(seekBar==sb && fromUser==true){
            Matrix m = new Matrix();
            float f = progress + 10;
            f /= 10;//倍率としての値に変換する。
            float fX = iImageSize * f;
            fX = (sb.getWidth() - fX) / 2;//横位置を計算する。
            m.setTranslate(fX, 0);//最初に位置を設定する
            m.preScale(f, f);//次に拡大倍率を設定する。
            iv.setImageMatrix(m);//位置及び倍率を適用する。
            
            TextView tv;
            tv = (TextView)findViewById(R.id.TextPieceSize);
            tv.setText(String.valueOf(f));//画面に倍率を表示する。
            return;
        }
    }
(以下省略)

2012年5月27日日曜日

SurfaceViewの留意点

SurfaceViewの留意点

SurfaceViewは、他のViewと違い、取扱いが特殊である。
SurfaceViewを用いたsample codeはLunarLanderである。このsample codeは本格的なゲームであるため、(私から見て)難しくて学習向きであるようには思えない。
このsample codeで気になるのは、背景を常に書き換えていることである。

        /**
         * Draws the ship, fuel/speed bars, and background to the provided
         * Canvas.
         */
        private void doDraw(Canvas canvas) {
            // Draw the background image. Operations on the Canvas accumulate
            // so this is like clearing the screen.
            canvas.drawBitmap(mBackgroundImage, 0, 0, null);
doDrawメソッド内の第一行目でBitmapを使って、画面全体を塗り潰している。そして、第二行目以降で、ゲームに登場するキャラクターを描画していくのである。「変化が無い領域をも再描画することは無駄ではないか」と思ってしまうが、これがSurfaceViewの流儀であるようだ。
Canvas and DrawablesのOn a SurfaceViewには次のように書かれてある。
Note: On each pass you retrieve the Canvas from the SurfaceHolder, the previous state of the Canvas will be retained. In order to properly animate your graphics, you must re-paint the entire surface. For example, you can clear the previous state of the Canvas by filling in a color with drawColor() or setting a background image with drawBitmap(). Otherwise, you will see traces of the drawings you previously performed.
滑らかなアニメーションを実現するために、画面全体を再描画しなければならないのである。動いているのが、主人公たった一人だけであってもだ。

画面内の個々の構成要素のサイズや位置を、アニメが発生する毎に計算するのは手間や時間がかかる。
LunarLanderの場合だと、黒色一色だけで塗り潰せば良いので問題は無い。
しかし、空や太陽や山や家や木等を重ね合わせる計算処理を、アニメが発生する毎に行うのは手間と時間がかかる。

そのような手間を、ある程度緩和するために、事前に、動きの無い背景の絵をBitmapに描いて、それを再描画の度に背景に表示するようにすれば良い。

Canvasをnewする時の引数にBitmapを設定した上で、このCanvasに描画していけば、その描画がBitmapに反映されるのである。

この技法を使ったsample codeは下記のとおりである。
このサンプルではグラデ―ションを作成しているが、この作成が仮に、手間がかかり時間がかかる処理だとする。そんなことをrunが実行される度に行うと、多くの計算処理を行うことになる。
このため、最初の描画の時に、Bitmap画像として大域変数に保管しておき、必要に応じて、このBitmap画像を表示すれば良い。

public class MySurfaceView extends SurfaceView
    implements
    Callback,
    Runnable
    {
    Thread t;
    Bitmap bSource, bBackground;
    int iY;
    
    public MySurfaceView(Context c){
        super(c);
        t = null;
        iY = 50;
        getHolder().addCallback(this);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder h) {
        Canvas c = h.lockCanvas();
        
        //背景用の、空っぽのBitmapを用意する・
        bBackground = Bitmap.createBitmap(c.getWidth(), c.getHeight(), Bitmap.Config.ARGB_8888);
        
        //Bitmapに書き込むCanvasを用意する。
        Canvas cBackground = new Canvas(bBackground);
        
        LinearGradient(c);//表示用の書き込み
        LinearGradient(cBackground);//背景用画像の作成
        
        //ドロイド君の画像を表示する。
        Resources r = getResources();
        bSource = BitmapFactory.decodeResource(r, R.drawable.ic_launcher);
        c.drawBitmap(bSource, 100, iY, null);
        
        h.unlockCanvasAndPost(c);
        t = new Thread(this);
        t.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
    
    @Override
    public void run() {
        while(t!=null){
            Canvas c = getHolder().lockCanvas();
            if(c!=null){
                //背景画像を表示する。
                c.drawBitmap(bBackground, 0.0f, 0.0f, null);
                iY++;
                if(iY>400) iY = 50;
                c.drawBitmap(bSource, 100, iY, null);
                getHolder().unlockCanvasAndPost(c);
            }
            try {
                Thread.sleep(10);
            }
            catch (InterruptedException e) {
                t = null;
            }
        }
    }
    
    private void LinearGradient(Canvas c){
        LinearGradient lg = new LinearGradient(
                c.getWidth()/2, 0.0f, c.getWidth()/2, c.getHeight(),
                Color.BLUE, Color.RED, Shader.TileMode.CLAMP);
        Paint p = new Paint();
        p.setShader(lg);
        c.drawPaint(p);
    }
}

SurfaceChanged method sample code

SurfaceChanged method sample code

SurfaceHolder.Callback interfaceには、surfaceChanged、surfaceCreated及びsurfaceDestroyedの3個のメソッドがある。
このうち、surfaceChangedメソッドが不明だったのでsample codeを作成してみた。

このsample codeのアプリを実行すると、1個のボタンと、その下にSurfaceViewが表示される。
ボタンを押し下げると、ボタンの大きさが変化する、このボタンの大きさに影響されてSurfaceViewの表示領域が変化し、surfaceChangedメソッドが呼ばれる。

SurfaceViewの表示領域が変化した時にsurfaceChangedメソッドが呼ばれるという、まさに当たり前の振る舞いではあるが、念のために確認した次第である。

取り敢えず、ゲームの作成には関係ないや、という安心感とともに、使いもしないメソッドを実装させられるムナシサを心に秘めつつ、訳の分からんドキュメントの解析にイソシムandroid programmerであった。

SurfaceHolder#setFixedSizeメソッドは、1回使うと、2回目以降は機能しない。
SurfaceHolder#setFormatメソッドを使うと、surfaceChangedメソッドは呼び出される。しかし、その呼出時に Canvas c = h.lockCanvas(); を行うと、「既にロックされている」という趣旨の実行時エラーが発生する。これは、詳しくは調べていない。

ちなみに、surfaceCreatedが呼ばれるのは、SurfaceViewの生成時である。
そして、それに加えて、startActivityメソッドを使って他のViewを呼び出し、そしてそのViewが消えて、再度、SurfaceViewが表示される時にもsurfaceCreatedが呼ばれる。この場合には SurfaceViewのコンストラクタは呼ばれない。
「戻る」キーを押し下げてSurfaceViewを消した場合には、次回のSurfaceViewの表示には、コンストラクタの実行が伴う。

public class SurfaceChangedActivity extends Activity
    implements
    OnClickListener
    {
    final String TAG = "me";
    MySurfaceView msv;
    Button bu;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
        msv = new MySurfaceView(this);
        LinearLayout llParent;
        llParent = (LinearLayout)findViewById(R.id.Main);
        llParent.addView(msv);
    }
    
    @Override
    public void onClick(View v) {
        if(v==bu){
            if(bu.getHeight()>100) bu.setHeight(80);
            else bu.setHeight(500);
            return;
        }
    }

    public class MySurfaceView extends SurfaceView implements Callback{
        public MySurfaceView(Context context) {
            super(context);
            getHolder().addCallback(this);
        }
        
        @Override
        public void surfaceCreated(SurfaceHolder h) {
            Canvas c = h.lockCanvas();
            LinearGradient(c);
            h.unlockCanvasAndPost(c);
        }

        @Override
        public void surfaceChanged(SurfaceHolder h,
                int format, int width, int height) {
            if(h.isCreating()==true) return;
            Log.d(TAG, "surfaceChanged : " + height);
            Canvas c = h.lockCanvas();
            LinearGradient(c);
            h.unlockCanvasAndPost(c);
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
        }
        
        private void LinearGradient(Canvas c){
            LinearGradient lg = new LinearGradient(
                    c.getWidth()/2, 0.0f, c.getWidth()/2, c.getHeight(),
                    Color.BLUE, Color.RED, Shader.TileMode.CLAMP);
            Paint p = new Paint();
            p.setShader(lg);
            c.drawPaint(p);
        }
    }
}

2012年5月17日木曜日

SurfaceView sample code:小さな画像を敷き詰め、スクロールさせる

SurfaceView sample code:小さな画像を敷き詰め、スクロールさせる

小さな画像を敷き詰め、その画像をスクロールさせようとする場合のロジックとして、取り敢えず思いつくのが、画像を4個の象限に分割して、対角線上の象限の画像を入れ替えた画像を準備し、それを敷き詰めていく、というロジックだ。

しかし、これをプログラミングするのは苦労する。コードが読み難くなる。

幸いなことに、Canvasクラスのメソッドには、マイナスの座標の値を設定できる。マイナスの座標値を設定して、画面の上端の更に上(又は左端の更に左)の、画面には表示出来ない箇所を起点とする画像を描画するようにすれば、象限分割の技法を用いることなく画像を敷き詰めることができる。

参考:ゲームの起動直後の画面を作成する

Javaのsample codeは次のとおりである。
public class MySurfaceView extends SurfaceView
    implements
    Callback,
    Runnable
    {
    //学習・実験を目的としているので。停止処理を実装していない。
    Thread t;
    int g_iX, g_iY;
    Bitmap bSource;
    
    public MySurfaceView(Context context) {
        super(context);
        g_iX = 0;
        g_iY = 0;
        t = null;
        getHolder().addCallback(this);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder h) {
        Canvas c = h.lockCanvas();
        Resources r = getResources();
        int iX, iY;
        
        //小さな画像を、画面いっぱいに敷き詰める。
        bSource = BitmapFactory.decodeResource(r, R.drawable.tilerepeat);
        for(iY=0; iY<c.getHeight(); iY+=bSource.getHeight()){
       for(iX=0; iX<c.getWidth(); iX+=bSource.getWidth()){
                c.drawBitmap(bSource, iX, iY, null);
       }
        }
        h.unlockCanvasAndPost(c);
        
        t = new Thread(this);
        t.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
    
    @Override
    public void run() {
        while(t!=null){
            Canvas c = getHolder().lockCanvas();
            if(c!=null){
                //1回当たりのスクロールさせる程度を設定する。
                g_iX += 5;
                if(g_iX>=bSource.getWidth()) g_iX = 0;
                g_iY += 8;
                if(g_iY>=bSource.getHeight()) g_iY = 0;
                
                //画像1枚分を大目に設定する。
                int iXMax = c.getWidth() + bSource.getWidth();
                int iYMax = c.getHeight() + bSource.getHeight();
                
                for(int iY=0; iY<iYMax; iY+=bSource.getHeight()){
                    for(int iX=0; iX<iXMax; iX+=bSource.getWidth()){
                        float fX, fY;
                        
                        if(g_iX==0) fX = 0.0f;
                        else{//左端の、画面に表示されない部分から描画を開始する。
                            fX = (float)(bSource.getWidth() - g_iX) * -1;
                        }
                        fX += iX;
                        
                        if(g_iY==0) fY = 0.0f;
                        else{//上端の、画面に表示されない部分から描画を開始する。
                            fY = (float)(bSource.getHeight() - g_iY) * -1;
                        }
                        fY += iY;
                        
                        c.drawBitmap(bSource, fX, fY, null);
                    }
                }
                getHolder().unlockCanvasAndPost(c);
            }
            try {
                Thread.sleep(10);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
                t = null;
            }
        }
    }
}


上記を呼び出すのは下記のコードである。
public class StartingActivity extends Activity{
    private final String TAG = "It's me!";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        MySurfaceView sv = new MySurfaceView(this);
        setContentView(sv);
    }
}

2012年5月15日火曜日

SurfaceViewと、それ以外のViewとの混在

SurfaceViewと、それ以外のViewとの混在

SurfaceViewと、それ以外のViewとを混在させる場合、RelativeLayoutを使うのがよろしいでしょう。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#880000ff"
    >
    <TextView
        android:id="@+id/TextTop"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="@string/app_name"
        >
    </TextView>
    <TextView
        android:id="@+id/TextBottom"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name"
        >
    </TextView>
    <SurfaceView
        android:id="@+id/SurfaceView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/TextTop"
        android:layout_above="@id/TextBottom"
        >
    </SurfaceView>
</RelativeLayout>

SurfaceViewと、それ以外のViewとの、Z軸上での混在

SurfaceViewと、それ以外のViewとの、Z軸上での混在

問題点
SurfaceViewと、それ以外のViewとを、各々半透明にして、Z軸上で重ねた場合、それらの色は混じり合わず、どちらか一方の半透明色しか表示されない。
「それ以外のView」には、別のインスタンスとしてのSurfaceViewも含まれると思われる。
参考:
Androidのカメラの表示優先で質問です
GLSurfaceViewや、SurfaceViewを複数使った場合に順番がおかしくなる。
GLSurfaceViewの背景を透過させる
セカイカメラ開発から見たAndroidアプリケーション開発の現状とAndroid内部構造

再現方法
SurfaceView以外のViewとして、この記事ではRelativeLayoutを用いた。
このRelativeLayoutを半透明にして、背景にある壁紙を表示する。具体的方法は次のとおりである。

AndroidManifest.xmlに次の1行を加える。これにより、これが設定されたActivityは透明や半透明に設定できるようになる。
android:theme="@android:style/Theme.Translucent"

レイアウト用xmlは次のとおりとする。名前はmain.xmlである。このxmlでは、半透明の値として"#880000ff"を設定する。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/MainParent"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#880000ff"
    >
    
    <!-- このTextViewはSurfaceViewが重なると表示されない。 -->
    <TextView
        android:text="@string/app_name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textColor="#ff00ff00"
        >
    </TextView>
</RelativeLayout>

上記の設定で、ActivityのonCreateメソッド内でsetContentView(R.layout.main)すると、当該アプリの背景にある壁紙が青色の半透明になって透けて見えるようになる。ここまでは期待どおりである。

このアプリで半透明のSurfaceViewを表示するようにすると、青色の半透明は無くなり、SurfaceViewだけを基にした半透明になる。
Javaのcodeは次のとおりである。

public class StartingActivity extends Activity
    implements
    Callback
    {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        RelativeLayout rl = (RelativeLayout)findViewById(R.id.MainParent);
        SurfaceView sv = new SurfaceView(this);
        sv.getHolder().setFormat(PixelFormat.TRANSLUCENT);
        sv.getHolder().addCallback(this);
        rl.addView(sv);
        
        TextView tv = new TextView(this);
        tv.setText(R.string.app_name);
        tv.setTextColor(0xff000000);
        rl.addView(tv);//このTextViewは表示される。
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder h) {
        Canvas c = h.lockCanvas();
        Paint p = new Paint();
        p.setARGB(150, 255, 0, 0);
        c.drawCircle(200, 200, 200, p);
        h.unlockCanvasAndPost(c);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}

解決方法
次のようにsetZOrderOnTop(true)を実行すれば良い。
        SurfaceView sv = new SurfaceView(this);
        sv.getHolder().setFormat(PixelFormat.TRANSLUCENT);
        sv.setZOrderOnTop(true);//これを追加する。
        sv.getHolder().addCallback(this);
        rl.addView(sv);

考察
public void setZOrderOnTop (boolean onTop)の説明に次のとおり書かれてある。
Control whether the surface view's surface is placed on top of its window. Normally it is placed behind the window, to allow it to (for the most part) appear to composite with the views in the hierarchy. By setting this, you cause it to be placed above the window.
この説明を読むと、上記の問題解決にsetZOrderOnTop(true)は効果が無いように解釈できる。behind the windowから、above the windowに変えます、というだけだ。私が接した事例ではSurfaceViewが表示されてしまっているのに、その状態が何故behind the windowなんだ。そもそも、私はthe windowが何たるか分かっていないのだが。

振る舞いから考えると、setZOrderOnTop(true)は、同一のZ軸上に存在する他のViewも表示できるようにした上で、それらのViewの中からSurfaceViewをtopに位置付ける、ということかもしれない。

2012年5月6日日曜日

TextView#getLineCountメソッドの戻値がゼロ

TextView#getLineCountメソッドの戻値がゼロ

TextView#getLineCountメソッドの戻値がゼロになった。
android developersのTextViewには、getLineCountメソッドについて次の解説がある。
Return the number of lines of text, or 0 if the internal Layout has not been built.
internal Layoutが構築されていないことが原因である、ということだ。
TextViewが表示されない状況下でTextViewを生成した場合に「戻値ゼロ」になってしまうようであるが、詳細は不明である。

なお、私が遭遇した事例では、一旦この「戻値ゼロ」が発生すると、当該TextViewが画面に表示された後であっても、この 「戻値ゼロ」 は発生し続けた。

調査した結果、原因はTextViewを定義したxmlにあった。

 「戻値ゼロ」 が発生するxmlファイル
<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/app_name"
    >
</TextView>

 「戻値ゼロ」 が発生しないxmlファイル
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        >
    </TextView>
</LinearLayout>

TextViewの外側をLinearLayoutで囲ってやれば良い。

また、Javaのcodeにおいて、TextViewを単体でnewした場合にも発生する。例えば、次のように。
TextView tv = new TextView(this);
おそらく、このJava codeにおいても、TextViewの外側をLinearLayoutで囲うようにすれば良いだろう。

internal Layoutの構築と、TextViewを単体で生成することとの因果関係については、私には分からない。むむむ!

2012年5月1日火曜日

透明のActivityにはconfiguration changeが発生しない件

透明のActivityにはconfiguration changeが発生しない件

android:theme="@android:style/Theme.Translucent"を施すと、画面は透明になる。
しかし、configuration changeが発生しなくなる。
このため、端末を横位置にしても、画面は縦位置状態のままである。

ただし、全てがそうではない。

そこで実験をしてみた。
次のAndroidManifest.xmlに示したように、3個のActivityを設け、Intentにより、TransparentActivityはAnotherActivityを起動させ、AnotherActivityはCを起動させるようにした。
そして、各々のActivityにandroid:theme="@android:style/Theme.Translucent"を設けたり、設け無くしたりして、configuration changeに関する振る舞いを観察した。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.Androyer.Transparent"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="8" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        >
        <activity
            android:name=".TransparentActivity"
            android:theme="@android:style/Theme.Translucent"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".AnotherActivity"
            >
        </activity>
        <activity
            android:name=".C"
            android:theme="@android:style/Theme.Translucent"
            >
        </activity>
        </application>
</manifest>


上記実験から得た知見は次の3点である。

・android:theme="@android:style/Theme.Translucent"を設けていないActivityでは、端末の位置を縦から横にした場合、onSaveInstanceStateメソッドが呼ばれる。

・android:theme="@android:style/Theme.Translucent"を設けていないActivityから呼ばれたActivityでは、android:theme="@android:style/Theme.Translucent"を設けていても、いなくても、端末の位置を縦から横にした場合、onSaveInstanceStateメソッドが呼ばれる。

・上記以外の場合には、onSaveInstanceStateメソッドは呼ばれない。

上記AndroidManifest.xmlファイルの例で説明すると次のとおりである。

・TransparentActivityの画面が表示されている状態では、端末を横位置にしても、画面は縦位置状態のままである。

・TransparentActivityからIntentでAnotherActivityの画面を表示させると、AnotherActivityにおいて、端末の位置を縦から横にした場合、onSaveInstanceStateメソッドが呼ばれる。 。

・AnotherActivityからIntentでCの画面を表示させると、Cにおいて、端末の位置を縦から横にした場合、onSaveInstanceStateメソッドが呼ばれる。

ここの説明において、「onSaveInstanceStateメソッドが呼ばれる」ということと、
「configuration changeが発生する」ということは、同義である。

参考:
半透明の程度を動的に変更する
実験:configuration changeは、画面の無いActivityでも発生するのか

Spannable.SPAN_COMPOSING その2

Spannable.SPAN_COMPOSING その2

Spannable.SPAN_COMPOSING sample codeの続編です。

setSpanメソッドの第一引数の値が、第1回目に実行される場合と第2回目に実行される場合とで、同一である場合には、Spannable.SPAN_COMPOSINGの効果はあります。
しかし、別の値になった場合には、Spannable.SPAN_COMPOSINGの効果が無くなり、第1回目の装飾は残ったままになります。

第一引数の値が前回の値と異なる場合には、前回の装飾を消すためにremoveSpanメソッドを使います。

sample codeは次のとおりです。前回のspanをremoveSpanメソッドで削除しているため、setSpanメソッドの第4番目の引数Spannable.SPAN_COMPOSINGはダミー(空間を埋めるためだけを目的とした意味の無い言葉)になります。
public class TextViewSpanRemoveActivity extends Activity
    implements
    OnClickListener
    {
    final int iMaxChar = 255;
    int iCounter;
    Button bu;
    TextView tv;
    SpannableString ss;
    BackgroundColorSpan spanBC;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        int i;
        char c = '亜';
        StringBuilder sb;
        
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
        tv = (TextView)findViewById(R.id.TextView);
        
        //表示する文字列の作成
        sb = new StringBuilder();
        for(i=0; i<iMaxChar; i++) sb.append(c++);
        
        //装飾の作成
        ss = new SpannableString(sb.toString());
        spanBC = new BackgroundColorSpan(0xff880000);
        
        //第ゼロ番目の文字に装飾を施す。
        iCounter = 0;
        ss.setSpan(spanBC, iCounter, iCounter+1, Spannable.SPAN_COMPOSING);
        tv.setText(ss);
    }


    @Override
    public void onClick(View v) {
        if(v==bu){//クリックした場合
            int i, iRed, iBlue;
            
            //もし、removeSpanメソッドを実行しない場合、
            //前回の装飾はそのまま残り、かつ次の文字も装飾される。
            ss.removeSpan(spanBC);//既に設定されているSpanを消す。
            
            iCounter++;//次の文字に移行する。
            if(iCounter>iMaxChar) iCounter = 0;
            
            //適当に色を変える
            iRed = iCounter & 0x0f;
            iRed = 0x0f - iRed;
            iRed <<= 20;
            iBlue = iCounter % 16;
            iBlue <<= 4;
            i = 0xff000000 + iRed + iBlue;
            spanBC = new BackgroundColorSpan(i);
            
            ss.setSpan(spanBC, iCounter, iCounter+1, Spannable.SPAN_COMPOSING);
            tv.setText(ss);
            return;
        }
    }
}