スポンサーリンク

Androidで,「ビットマップのピクセル操作」をリアルタイムに実行するサンプルコード


Androidで,View上にビットマップ画像等を表示し,ピクセル単位で画素を操作する。

しかも一回きりではなく,繰り返しcanvas上でピクセル操作する。


この方法は,「動的かつ連続的に,画面上の見かけを微調整したい。」という要件にマッチする。


Androidアプリの画面上で,動きや見かけ上の変化を付けるためには,いくつか方法がある。

下記にまとめてあるが,ピクセル操作は,わりと「最後の手段」的な位置付けだ。

  • OpenGLを導入し,2Dないし3DのSpriteでアニメーションする。
    • →定番だが,そこまで大がかりな事をしたいのではない。このツールの導入は,微調整レベルではない。
  • Viewに対して,Animation APIを使う。
    • →手軽だが,View単位でしか適用できない。つまり適用対象の粒度が荒い。それに,アニメーションの合成はできるが,連続ができない。(※補足を参照)
  • ViewのCanvas上で,描画処理を行なう。
    • →基本的な図形や画像リソースを描画し,1コマずつ定期的に外見を更新するのには向いている。だが,画素のレベルでの微調整までは手が届かない。
  • ViewのCanvasをBitmapとして扱い,ピクセル操作する。
    • →画素レベルでの調整が効く。ピクセル単位で,RGB値とアルファ値を設定できる。きめ細やかな調整が動的にできる。ただし,すごく低いレイヤでのコーディングになるので,実装は面倒。パフォーマンスにも要注意。

下記は,実際に動作するサンプルコード。

ピクセル操作のサンプルコード


適当なAndroidプロジェクトを作成し,下記のような独自Viewクラスを作成する。


MyBitmapView.java

package com.example;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;

/**
 * ビットマップ操作のサンプルのためのView。
 * @author id:language_and_engineering
 *
 */
public class MyBitmapView extends ImageView
{
    // 領域の縦幅と横幅
    private static final int bitmap_width_px = 500;
    private static final int bitmap_height_px = 400;

    // canvas関連
    private boolean isCanvasInitFinished = false;

    // bitmap関連
    private Bitmap bitmap;
    private int[] pixels;
    private Paint paintForBitmap;


    // ----------- 初回 ----------


    /**
     * Viewを初期化
     */
    public MyBitmapView(Context context, AttributeSet attr) {
        super(context, attr);
    }


    @Override
    protected void onDraw(Canvas canvas)
    {
        // 注:invalidate() などが呼ばれない限り,
        // このメソッドによる再描画処理が呼ばれないことに注意。

        // 初回のみビットマップ情報を初期化
        if( ! isCanvasInitFinished )
        {
            // onDrawの初回
            initCanvas( canvas );

            isCanvasInitFinished = true;
        }
        else
        {
            // onDrawでcanvasにbitmapを描画
            canvas.drawBitmap(
                bitmap,
                0, 0, // 描画座標のオフセット
                paintForBitmap
            );
                // NOTE: canvas⇔bitmap間では「変換」のような処理は行わない。
                // かわりに,canvasにbitmapを渡す。コンストラクタで渡す手もある。
                // http://yakinikunotare.boo.jp/orebase2/android_dev/convert_canvas_to_bitmap
        }
    }


    /**
     * 初回のセットアップ処理
     */
    private void initCanvas(Canvas canvas)
    {
        // 空のbitmapを作成する場合
        //bitmap = Bitmap.createBitmap(bitmap_width_px, bitmap_height_px, Bitmap.Config.ARGB_8888);

        // bitmapを画像リソースから読み込む場合
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
        bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); // 編集可能なコピーを作成
        bitmap = Bitmap.createScaledBitmap(bitmap, bitmap_width_px, bitmap_height_px, false); // Viewのサイズまで拡大
            // 「Bitmap で Pixel 操作してみた」
            // http://weide-dev.blogspot.jp/2010/04/bitmap-pixel.html

        // bitmapをcanvasとひもづける
        canvas = new Canvas(bitmap);

        // View全体の領域全体を塗りつぶす場合
        //canvas.drawColor( Color.GRAY );
            // 「描画領域を塗りつぶす」
            // http://ichitcltk.hustle.ne.jp/gudon/modules/pico_rd/index.php?content_id=80


        // bitmapにロードするためのピクセル配列を作成
        pixels = new int[bitmap_width_px * bitmap_height_px];

        // bitmapの描画を実行するためのPaintを準備
        paintForBitmap = new Paint();


        // 画面に反映
        invalidate();
    }


    // ----------- ビットマップ操作関連 ----------


    @Override
    public boolean onTouchEvent(MotionEvent e)
    {
        // ビットマップを更新する
        modifyBitmapOnTouch();

        // 表示状態に反映させる
        invalidate();

        return true;
            // 「お絵かきアプリを作りたい。描画編」
            // http://andante.in/i/%E6%8F%8F%E7%94%BB/%E3%81%8A%E7%B5%B5%E3%81%8B%E3%81%8D%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E3%82%8A%E3%81%9F%E3%81%84%E3%80%82%E6%8F%8F%E7%94%BB%E7%B7%A8/
    }


    /**
     * ビットマップを更新する
     */
    private void modifyBitmapOnTouch()
    {
        // NOTE: 下記の流れでピクセル操作が実行される。
        // 1. getPixels()で,bitmap(canvas)からピクセル情報を取得。
        // 2. ピクセル情報を操作する。
        // 3. setPixels()で,bitmap(canvas)にピクセル情報をセットする。
        // 4. invalidate()で,onDraw()の再実行を要求する。
        // 5. onDraw()内のdrawBitmap()で,bitmapの最新状態をcanvasの画面表示に反映する。


        // 全ピクセルをロード
        loadPixelsFromBitmap();

        // ピクセル操作
        handlePixelsOfBitmap();

        // 全ピクセルを反映
        updateBitmapPixels();
    }


    /**
     * ピクセルレベルでビットマップを操作する
     */
    private void handlePixelsOfBitmap()
    {
        // ランダムな座標を設定
        int base_x = 25 + (int)( Math.random() * ( bitmap_width_px - 50 ) );
        int base_y = 25 + (int)( Math.random() * ( bitmap_height_px - 50 ) );
            //Log.d("bitmap-test", "座標は" + base_x + ", " + base_y);

        // 該当ピクセルの周辺を操作
        int targetIndex;
        Point targetPoint;
        for( int x = base_x; x < base_x + 10; x ++)
        {
            for( int y = base_y; y < base_y + 10; y ++)
            {
                targetPoint = new Point( x, y );
                targetIndex = point2bitmapIndex( targetPoint );

                // このピクセルを操作
                modifyOnePixelByIndex(targetIndex);
            }
        }
    }


    // ------- ピクセル操作関連 -------


    /**
     * ビットマップからピクセルをロード
     */
    private void loadPixelsFromBitmap() {
        bitmap.getPixels(pixels, 0, bitmap_width_px, 0, 0, bitmap_width_px, bitmap_height_px);
    }


    /**
     * Bitmap内の全ピクセルを更新
     */
    private void updateBitmapPixels() {
        bitmap.setPixels(pixels, 0, bitmap_width_px, 0, 0, bitmap_width_px, bitmap_height_px );
    }


    /**
     * ビットマップ上の座標から,ピクセル配列のインデックスに変換
     */
    private int point2bitmapIndex(Point p)
    {
        // ピクセル配列中には,ビットマップのy行x列目の画素情報が
        // 一次元に畳まれて格納されている
        return p.y * bitmap_width_px + p.x;
    }


    /**
     * 1ピクセルを操作
     */
    private void modifyOnePixelByIndex(int targetIndex)
    {
        // 配列の範囲外は防止
        if( pixels.length <= targetIndex ) return;

        // 範囲内の正常なインデックスの場合
        int targetPixel = pixels[ targetIndex ];

        // 対象ピクセルを更新。
        // α値+RGB値として,それぞれ0〜255までの値を渡す
        pixels[ targetIndex ] = Color.argb(
            Color.alpha(targetPixel) / 2, // 半分に透過
            Color.red(targetPixel), // 該当ピクセルの元の値を保持
            Color.green(targetPixel),
            255 // 青味をつける
        );
            // [Android] Bitmapピクセル操作(getPixels / setPixels)
            // http://www.adakoda.com/adakoda/2009/01/android-bitmapgetpixels-setpixels.html
    }


    /**
     * 全削除
     */
/*
    public void eraseAll() {
        // 全ピクセルを初期化
        pixels = new int[bitmap_width_px * bitmap_height_px];

        // 表示に反映
        updateBitmapPixels();
        invalidate();
    }
*/

}


そしたら,スタブとして自動生成された main.xml の内容を,下記のように書き換える。


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"
    >

    <!-- ビットマップ操作用の独自View -->
	<com.example.MyBitmapView
    	android:layout_width="500px"
    	android:layout_height="400px"
    />

</LinearLayout>


アクティビティの内容はそのままで良い。


これで,プロジェクトをAndroidアプリケーションとして実行。


ドロイド君のアイコンが表示される。

描画領域にタッチすると,そのたびにランダムな座標でピクセル操作処理が行なわれる。

(ドロイド君に,虫食いのように穴があいていくのが分かるだろう。)


このサンプルでは,タッチしたタイミングでピクセル処理が走るようにした。

タイマーを設置して,ピクセル操作+invalidate() が定期実行されるように書き換える事もできる。




コードに対して,数点の補足:

  • 独自にView要素のクラスを作った場合,レイアウトXML上では,パッケージ名を含めた完全修飾名で該当Viewを指定する必要がある。
  • invalidate() の書き忘れに注意。ないと,画面上では何も変化が起こらない。このメソッドの呼び出しは,onDraw() が呼び出されるために必要。onDrawとinvalidateの関係に注意。

Android SDK の動かないコード(中級編) invalidate しても再描画されないエラー
http://language-and-engineering.hatenablog.jp/entry/20120404/AndroidInvalidat...

  • onDraw() : canvasに対する処理内容を記述する。
  • invalidate() : ユーザがこのメソッドを呼び出す事により,Androidフレームワークに対して,都合がつき次第 onDraw() を呼ぶように要求する。
  • pixelsという配列の扱いに注意。
    • 配列のサイズが大きいので,もし配列全体をスキャンして全画素を更新するようなアルゴリズムだと,アプリのパフォーマンスが著しく低下する。狙った画素の周りだけを限定的にピクセル操作するように。
    • 初歩的な点だが,下手にメソッドを切り分けようとしないこと。Javaでは,配列をメソッドの引数として渡すと,「その配列オブジェクトへの参照のコピー」が渡される。ポインタではなく,ポインタのコピーが渡されるのである。メソッド内部でいろいろ変更を加えても,メソッド呼び出し元には配列の変更が反映されない,なんて事が起こり得る。

Javaの参照渡しについて
http://detail.chiebukuro.yahoo.co.jp/...

  • 参照型の場合は、実体を参照するポインタがコピーして渡されます。(参照渡し)
  • canvasとbitmapとpixelsの関係がちょっとわかりづらいので,ソース中のコメントに注意。要は,互いにひもづいている。(=互いにロードしたりロードされたりしている)
  • 1ピクセルだけ操作しても,画面上では目視で確認できない場合があるので注意。最近の端末は解像度が高い(または,低くない)ので,たった1ピクセルの変化では全く分からないのである。だからサンプルコード中では,変化が目に見えて分かるよう,ターゲット座標の周り10ピクセルに対して処理を施している。

デタラメPhotoshop 解像度(dpi)
http://www.detarame.jp/dpi.html

  • 「解像度」=「dpi (Dots Per Inch)」=1インチの長さにいくつのピクセル(ドットつまり画素)が入るか=画像の「密度」
  • bitmapリソースをロードする部分のコードが,少し見慣れないかもしれない。
    • ARGB_8888とは,アルファ値とRGBのそれぞれの値を8ビット(0〜255)の階調で表現するという事を表す。
    • bitmapをわざわざcopyしている理由は,読み込んだ直後は「immutable」(編集不可)だから。もしcopyを欠かすと,下記のようなスタックトレースのエラーが出る。
ERROR/AndroidRuntime(3802): FATAL EXCEPTION: main
ERROR/AndroidRuntime(3802): java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor
ERROR/AndroidRuntime(3802):     at android.graphics.Canvas.<init>(Canvas.java:83)
ERROR/AndroidRuntime(3802):     at com.example.MyBitmapView.initCanvas(MyBitmapView.java:82)
ERROR/AndroidRuntime(3802):     at com.example.MyBitmapView.onDraw(MyBitmapView.java:55)
ERROR/AndroidRuntime(3802):     at android.view.View.draw(View.java:6918)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
ERROR/AndroidRuntime(3802):     at android.view.View.draw(View.java:6921)
ERROR/AndroidRuntime(3802):     at android.widget.FrameLayout.draw(FrameLayout.java:357)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.drawChild(ViewGroup.java:1644)
ERROR/AndroidRuntime(3802):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
ERROR/AndroidRuntime(3802):     at android.view.View.draw(View.java:6921)
ERROR/AndroidRuntime(3802):     at android.widget.FrameLayout.draw(FrameLayout.java:357)
ERROR/AndroidRuntime(3802):     at com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:1947)
ERROR/AndroidRuntime(3802):     at android.view.ViewRoot.draw(ViewRoot.java:1539)
ERROR/AndroidRuntime(3802):     at android.view.ViewRoot.performTraversals(ViewRoot.java:1275)
ERROR/AndroidRuntime(3802):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1876)
ERROR/AndroidRuntime(3802):     at android.os.Handler.dispatchMessage(Handler.java:99)
ERROR/AndroidRuntime(3802):     at android.os.Looper.loop(Looper.java:123)
ERROR/AndroidRuntime(3802):     at android.app.ActivityThread.main(ActivityThread.java:3728)
ERROR/AndroidRuntime(3802):     at java.lang.reflect.Method.invokeNative(Native Method)
ERROR/AndroidRuntime(3802):     at java.lang.reflect.Method.invoke(Method.java:507)
ERROR/AndroidRuntime(3802):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:864)
ERROR/AndroidRuntime(3802):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:622)
ERROR/AndroidRuntime(3802):     at dalvik.system.NativeStart.main(Native Method)

java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor
http://rtaki.blogspot.jp/2010/12/java...

  • bitmap が immutable (変えられない) ことが原因。copyしてmutable な Bitmap を新規に作成すればよい

補足:Animation API が使いづらい理由は? このAPIはどんな用途に向くのか?

Androidアプリに限らず言えることだが,

良いUIを実現するためには,ほどよい視覚効果(=エフェクト)が欠かせない。


また,ゲーム系のアプリを開発する場合であれば特に,

画面上の要素(キャラクターとか)の見え方がリアルタイムに変わっていくような視覚効果が,処理の中で大きな比重を占める。

そこで,「リアルタイムな視覚効果」を画面上で実現するための手段が必要になる。


Android SDKには,もとからアニメーションや視覚エフェクトを実現するためのAPIが存在する。


移動,回転,拡大/縮小,フェードイン/フェードアウトなどがあり,

AnimationSetを使ってエフェクトを合成することも可能だ。

Androidアプリで“アニメーション”するための基礎知識
http://www.atmarkit.co.jp/fsmart/arti...


ただしAnimation APIには,意外と使いづらい点もある。

  • View単位でのエフェクト適用なので,それより細かい粒度での視覚効果は不可能。
  • 時間的に連続して複数のエフェクトを適用させること」が,きわめて困難。


このAPIの使い道は,

  • ボタンを押したら,押したボタンが拡大しながらフェードアウトして消えた。

みたいな,「一発もの」のエフェクトに向いている。


エフェクトの適用対象もView単位なので,ピクセル単位でのきめ細やかな変化は付けられない。


Animation APIを,時間的に連続して複数回適用してみるためには,下記のようなライブラリの案がある。

Androidで,複数のAnimationを「順番に」実行するためのライブラリ (XMLを使わずに「連続した動きの変化」を指定し,逐次実行するDSL)
http://language-and-engineering.hatenablog.jp/entry/20120416/AndroidAnimation...

まあ悪くない。

だが,どうも見た目が想定外にブレたり,微調整が大変だったりする。(特に移動や回転が絡む場合)

主な理由は,setFillBefore() とか setFillAfter() が思い通りに機能してくれず,連続したアニメーションの間でうまく「補間」が実行されないためだ。

How can I animate a view in Android and have it stay in the new position/size?
http://stackoverflow.com/questions/33...
you will have to write an onAnimationEnd handler, and in there, manually adjust the "real" (pre-transformation) bounds of your view to match the end result of the scale animation.


Animation fillAfter(true) and click locations
http://www.androiddiscuss.com/1-andro...
If you want the effects of the animation to be permanent, you need to
add a listener to the animation, find out when the animation ends, and
then take steps to make the changes that the animation simulates.

So, for example, if you are sliding a button off of the screen, when the
animation ends, you could make the visibility of that button be GONE, or
remove it from its parent container, or something.

したがってAnimation APIは,時間的に連続・継続したアニメーションには,余り向かないのである。

定期的な間隔で繰り返し動かし続けることなど,もってのほか。

また,OpenGLで実現できるような,本物の自由自在な「アニメーション」は実現できない。

Animation APIは,Viewのような大きな単位で,一回限り実行されるようなエフェクト向きだ。