2012年6月28日木曜日

SimpleAdapter : データの変更を画面に反映させる。

SimpleAdapter : データの変更を画面に反映させる。

Activityの起動時に(onCreateメソッド内で)SimpleAdapterを使ってListViewに値を設定する方法は既に述べた。
参考:android.R.drawableのアイコンを一覧表示する


ListViewが表示された後で、データの内容が変更された場合における、その変更をListViewに反映させる方法は次のとおりである。
この例では、任意のレコードに表示されている文字列を、別の文字列に変更する。
public class SimpleAdapterActivity extends ListActivity {

    SimpleAdapter sa;
    ArrayList<Map<String, Object>> data;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        int i;
        String sFrom[] = {"Image", "String"};
        int iTo[] =  new int[] {R.id.Image, R.id.Text};
        data =new ArrayList<Map<String, Object>>();
        
        for(i=0; i<IconRawData.length; i++){
            Map<String, Object> m = new HashMap<String, Object>();
            m.put("Image", IconRawData[i++]);
            m.put("String", IconRawData[i]);
            data.add(m);
        }
        sa = new SimpleAdapter(this, data, R.layout.child, sFrom, iTo);
        setListAdapter(sa);
    }
    
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        Map<String, Object> m;
        m = data.get(position);//クリックした位置にあるレコードを取得する。
        m.put("String", "This data has been modified.");//文字列を書き変える。
        data.set(position, m);//レコード内のデータを変更する。
        sa.notifyDataSetChanged();//変更を画面に反映させる。
    }


    public Object IconRawData[] = {
        android.R.drawable.ic_btn_speak_now,
        "ic_btn_speak_now",
        
        android.R.drawable.ic_delete,
        "ic_delete",
        
        android.R.drawable.ic_dialog_alert,
        "ic_dialog_alert",
        
        android.R.drawable.ic_dialog_dialer,
        "ic_dialog_dialer",
(以下省略)        

2012年6月19日火曜日

res>drawableにある画像ファイルをBitmapに変換する

res>drawableにある画像ファイルをBitmapに変換する

Resources r = getResources();
Bitmap b = BitmapFactory.decodeResource(r, R.drawable.green);

2012年6月9日土曜日

surfaceDestroyedメソッドの引数の意味

surfaceDestroyedメソッドの引数の意味

surfaceDestroyedメソッドには引数としてSurfaceHolderがある。

SurfaceHolderについては、surfaceCreatedメソッドや、surfaceChangedメソッドにおいて、lockCanvasメソッドを使ってcanvasに描画するのが主な使い方になる。

しかし、surfaceDestroyedメソッドは、終了をする時点で呼ばれるのであるため、こんな時点で描画を行っても、あまり意味は無い。
では、surfaceDestroyedメソッドの引数SurfaceHolderは何に使うのか。

これは、他のスレッドでcanvasを使わせることを阻止するための、排他制御の仕掛けとして使うのである。「描画をする」という目的では使わない。

Threadクラスでは、例えば、stopメソッドはdeprecatedであるとされている。スレッドを外部から止めることはできない。スレッドの外部で、何かフラグを立ててやることになる。

SurfaceViewが非表示になった後で、描画用スレッドがSurfaceViewのcanvasに書き込みを行うと、何か不都合が発生すると思われる。
この不都合を阻止するため、surfaceDestroyedメソッド内で、SurfaceHolderのlockCanvasメソッドを使って、canvasを排他的に取得し、取得後「SurfaceViewは終了しました」という趣旨のフラグ等を設定するのである。canvasには描画しない。
そうして、描画用スレッドでは、フラグを見て終了を検知し、canvasへの書き込みを行わないようにすれば良い。

この場合、描画用スレッドがcanvasに描画している最中に、surfaceDestroyedメソッドが呼ばれる、という事態も想定しなければならないと思われる。

下記のsample codeではBitmapを解放することを主眼にしてしまっているが、肝心なことは、描画用スレッドに対して終了を通知しcanvasへの書き込みを止めさせれば良いのである。

    @Override
    public void surfaceDestroyed(SurfaceHolder h) {
        
        //Bitmapを解放することが目的である。
        Canvas c = h.lockCanvas();//排他的にキャンバスを使う
        if(c==null){//もし、他のスレッドでキャンバスを使っていた場合。
            tDestroy = new Thread(this);
            tDestroy.start();//このスレッド内でBitmapを解放する。
        }
        else{
            go.Destroy();//Bitmapを解放する。
            go = null;//終了を示すフラグを代入する。
            h.unlockCanvasAndPost(c);
        }
    }
    
    @Override
    public void run(){
        
        //tDestroyは、Bitmapを解放することを目的としている。
        if(Thread.currentThread()==tDestroy){
            Canvas c = getHolder().lockCanvas();
            if(c==null){//他のスレッドがCanvasを使っている場合
                try {
                    Thread.sleep(10);
                }
                catch (InterruptedException e) {
                }
                tDestroy.start();//再度、呼び出す。
                return;
            }
            go.Destroy();//Bitmapを解放する。
            go = null;//終了を示すフラグを代入する。
            getHolder().unlockCanvasAndPost(c);
            return;
        }
        
        //tは、描画用のスレッドである。
        if(Thread.currentThread()==t){
            
            //終了スレッドが動いている場合、tスレッドではCanvasを使えないようにする。
            if(tDestroy!=null) return;
            
            Canvas c = getHolder().lockCanvas();
            if(c==null) return;//他のスレッドがCanvasを使っている場合
            if(go!=null
            && go.getBackground()!=null){
                //まだ、稼働中である場合、描画する。
                c.drawBitmap(go.getBackground(), 0, 0, null);
            }
            getHolder().unlockCanvasAndPost(c);
        }
    }





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);
    }
}