Xperia arcのカメラアプリを縦向きの画面に対応するためにしたこと

Xperia arcでカメラアプリを作っていて、端末を縦にしたときの対応をしようとしたらやたら苦労したのでメモしておく。

やりたかったことは、縦にしたときにプレビューが回転して、サイズも変えること。

端末を縦にするとなんかプレビューが回転せずに小さくなった。
そこで、Camera.Parametersで"portrait"をセットしてみたらException...

Android2.2以降はCamera.setDisplayOrientation()を使えばいいらしい。
サンプルコードをそのまま入れてみたらちゃんとカメラビューが縦になった。
のだけど端末を横に戻そうとするとまたもException...

Camara.startPreview()したままCamera.setDisplayOrientation()を呼ぶとダメらしい。
surfaceChanged()の最初でCamera.stopPreview()するようにしたらオッケーだった。

そのままBitmap取得しようとすると、横向きのまま取得されてしまうので、Matrixを使ってBitmapも回転してあげる。

public void onPreviewFrame(byte[] data, Camera camera) {
    Size size = mCamera.getParameters().getPreviewSize();
    int[] rgb = new int[(size.width * size.height)]; // ARGB8888の画素の配列
    ImageUtil.decodeYUV420SP(rgb, data, size.width, size.height);	
    Matrix matrix = new Matrix();
    matrix.postRotate(mOrientation);
    Bitmap originalBmp = Bitmap.createBitmap(rgb, size.width, size.height, Bitmap.Config.ARGB_8888);
    Bitmap bmp = Bitmap.createBitmap(originalBmp, 0, 0, size.width, size.height, matrix, true);		
    // 画像処理するタスクの生成
    AsyncTask imageProcessingTask = new ImageProcessingTask(view.getContext());
    imageProcessingTask.execute(bmp);
}

するとさらに大きな問題ががが。

撮影した画像をBitmapにして加工する処理をAsyncTaskでやるようにしているのだけど、処理中に端末の向きを変えると画像が保存されない...

ググってみるといろいろ見つかった。
画面の縦横切り替え時に元の画面を保存
起動時にDialogを表示させるActivityで、横向きLANDSCAPEから縦向きPORTRAITへ向きを変えたときに発生するエラーの対処方法

確かにActivityが破棄されている。
処理中にタイトルバーにプログレス表示しているのが消えるのも、ActivityのonDestory()が呼ばれてたからだったのか!

上の記事の通りAndroidManifest.xmlに、android:configChanges="keyboardHidden|orientation"を指定して、ActivityのonConfigurationChanged()をオーバーライドしたらActivityが再生成されなくなった。


以上をまとめると、Androidのカメラアプリを縦対応にするには、

  • AndroidManifest.xmlに、android:configChanges="keyboardHidden|orientation"を指定
  • ActivityのonConfigurationChanged()をオーバーライド
  • Camera.setDisplayOrientation()に向きに合わせてrotationを指定する
  • SurfaceViewを使っているなら、surfaceChanged()で呼ぶといい感じ
  • Camera.setDisplayOrientation()の前にCamera.stopPreview()を呼ぶ
  • 撮影した画像をBitmapとして使うなら、Matrix使って回転させてから使う

こんな感じか。疲れたー><

Enum Factory Pattern

Androidでカメラアプリを作っていて、画像に対するフィルタを設定値からインスタンス生成して適用したい。
そこでFactory patternでやっていたのですが、Enum factory patternというのを見つけて、こっちの方が全然シンプルだし柔軟そう!と思い書き換えてみました。

package com.cheesepie.filter;
    public enum Filters {
        MONOTONE ("monotone"),
        SEPIA ("sepia"),
        DEFAULT ("default");

        private Filters(String name) {
            this.name = name;
        }

        public String toString() {
            return name;
        }

        public IFilter get() {
            switch (this) {
            case MONOTONE:
                return new MonotoneFilter();
            case SEPIA:
                return new SepiaFilter();
            case DEFAULT:
            default:
                return new DefaultFilter();
            }
        }

        private String name;
    }
}

IFilterは各フィルタで実装しているインターフェース。

使う側はこんな感じ。

// Get preference
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(uiActivity);
String filterPreference = sharedPreferences.getString("filter", uiView.DEFAULT_FILTER);
// Set filter
IFilter filter = EnumUtil.valueOf(Filters, filterPreference);
synchronized (objLock) {
    return filter.doFilter(sourceBmp);
}

EnumUtilは自作のユーティリティで、数も多くないのでfor文でEnumをなめて返している。
フィルタを追加するときは、Filters.javaに2行追加するだけ。
これでだいぶシンプルに書けるようになった。

Xperia arcのカメラで撮ったデータをBitmapクラスで扱えるバイトデータに変換する

public class Image {
	/**
	 * @param tempData
	 * @param width
	 * @param height
	 * @return
	 * @throws NullPointerException
	 * @throws IllegalArgumentException
	 */
	public static int[] decodeYUV(byte[] tempData, int width, int height) throws NullPointerException, IllegalArgumentException {
        int size = width * height;
        if (tempData == null) {
            throw new NullPointerException("buffer tempData is null");
        }
        if (tempData.length < size) {
            throw new IllegalArgumentException("buffer tempData is illegal");
        }

        int[] out = new int[size];

        int Y, Cr = 0, Cb = 0;
        for (int i = 0; i < height; i++) {
            int index = i * width;
            int jDiv2 = i >> 1;
            for (int i2 = 0; i2 < width; i2++) {
                Y = tempData[index];
                if (Y < 0) {
                    Y += 255;
                }
                if ((i2 & 0x1) != 1) {
                    int c0ff = size + jDiv2 * width + (i2 >> 1) * 2;
                    Cb = tempData[c0ff];
                    if (Cb < 0) {
                        Cb += 127;
                    } else {
                        Cb -= 128;
                    }
                    Cr = tempData[c0ff + 1];
                    if (Cr < 0) {
                        Cr += 127;
                    } else {
                        Cr -= 128;
                    }
                }

                // red
                int R = Y + Cr + (Cr >> 2) + (Cr >> 3) + (Cr >> 5);
                if (R < 0) {
                    R = 0;
                } else if (R > 255) {
                    R = 255;
                }
                // green
                int G = Y - (Cb >> 2) + (Cb >> 4) + (Cb >> 5) - (Cr >> 1) + (Cr >> 3) + (Cr >> 4) + (Cr >> 5);
                if (G < 0) {
                    G = 0;
                } else if (G > 255) {
                    G = 255;
                }
                // blue
                int B = Y + Cb + (Cb >> 1) + (Cb >> 2) + (Cb >> 6);
                if (B < 0) {
                    B = 0;
                } else if (B > 255) {
                    B = 255;
                }

                out[index] = 0xff000000 + (B << 16) + (G << 8 ) + R;
                index++;
            }
        }
        return out;
    }
}

Canvasの外側を丸く塗る

/**
  * @param canvas
  */
private void setBlackEdge(Canvas canvas) {
	float w = canvas.getWidth();
	float h = canvas.getHeight();
	int[] colors = new int[] { 0x00000000, 0x00000000, 0xFF000000 };
        RectF rect = new RectF(0, 0, w, h);
        
        // create a paint with a RadialGradient
        RadialGradient shader = new RadialGradient(w / 2, h / 2, w / 2, colors, null, TileMode.CLAMP);
        
        Paint paint = new Paint();        
        paint.setDither(true);
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setShader(shader);
        
        // paint the rectangle with said gradient
        canvas.drawRect(rect, paint);
}

Google Closure Library の goog.events.EventHandler & goog.events.EventTarget のシンプル版を作ってみた

かれこれ1年くらい Google Closure Library を仕事でガッツリ使ってます。
それまでは jQuery でよくね?と思ってたのですが、もはや Closure Library なしでブラウザの大規模UI作るなんて考えられない!というくらい馴染んでしまいました。
ソースも綺麗だし空いたときにソース読んでるだけでも勉強になります。さすが Google さん。

中でも goog.events.EventHandler & goog.events.EventTarget は DOM に依存せずオブジェクト間で簡単に通信ができて便利。
自宅で軽いサンプル作るときにも使いたいな、でも軽く使うには Closure Library はめんどくさすぎ。

ということで仕組みだけそのままにシンプルなものを作ってみました。

// Base object.
var util = {
    events: {}
};

/**
 * @param {*} opt_scope
 * @constructor
 */
util.events.EventHandler = function(opt_scope) {
    this.scope_ = opt_scope || null;
};

/**
 * @lends util.events.EventHandler
 */
util.events.EventHandler.prototype = {
    /**
     * @type {*}
     * @private
     */
    scope_: null,

    /**
     * @type {number}
     * @private
     */
    uidCounter_: 0,

    /**
     * Name for unique ID property.
     * @type {string}
     * @private
     */
    UID_PROPERTY_: '_uid_' + Math.floor(Math.random() * 2147483648).toString(36),

    /**
     * @return {string}
     */
    getUid: function() {
        return this.UID_PROPERTY_ + (this.uidCounter_++);
    },

    /**
     * @type {*} src
     * @type {string} type
     * @type {Function} listener
     * @type {!*} opt_scope
     */
    listen: function(src, type, listener, opt_scope) {
        if (!src.uid_) {
            src.uid_ = this.getUid();
        }
        var scope = opt_scope || (this.scope_ || listener);
        util.events.EventProxy.add(src, type, listener, scope);
    },

    /**
     * @type {*} src
     * @type {string} type
     * @type {Function} listener
     * @type {!*} opt_scope
     */
    unlisten: function(src, type, listener, opt_scope) {
        var scope = opt_scope || (this.scope_ || listener);
        return util.events.EventProxy.remove(src, type, listener, scope);
    }
};

/**
 * @constructor
 */
util.events.EventTarget = function() {
};

/**
 * @lends util.events.EventTarget
 */
util.events.EventTarget.prototype = {
    /**
     * @param {string} type
     * @param {*} data
     */
    dispatchEvent: function(type, data) {
        util.events.EventProxy.fireListener_(this, type, data);
    }
};

/**
 * @static
 */
util.events.EventProxy = {
    /**
     * @type {Object}
     * @private
     */
    listenerTree_: {},

    /**
     * @param {*} src
     * @param {string} type
     * @param {Function} listener
     * @param {*} scope
     * @return {boolean}
     */
    add: function(src, type, listener, scope) {
        var uid = src.uid_;
        if (uid === undefined) {
            return false;
        }

        if (!this.listenerTree_[uid]) {
            this.listenerTree_[uid] = {};
        }
        if (!this.listenerTree_[uid][type]) {
            this.listenerTree_[uid][type] = [];
        }
        this.listenerTree_[uid][type].push([listener, scope]);
        return true;
    },

    /**
     * @param {*} src
     * @param {string} type
     * @param {Function} listener
     * @param {*} scope
     * @return {boolean}
     */
    remove: function(src, type, listener, scope) {
        var result = false,
            uid = src.uid_;
        if (uid === undefined) {
            return result;
        }

        var listeners = this.listenerTree_[uid][type];
        for (var i = 0, len = listeners.length; i < len; i++) {
            var l = listeners[i];
            if (l[0] === listener && l[1] === scope) {
                this.listenerTree_[uid][type].splice(i, 1);
                result = true;
                break;
            }
        }
        return result;
    },

    /**
     * @param {*} src
     * @param {string} type
     * @return {Array<Function, *>}
     */
    get: function(src, type) {
        var uid = src.uid_;
        return this.listenerTree_[uid][type];
    },

    /**
     * @param {*} src
     * @param {string} type
     * @param {*} arg
     * @private
     */
    fireListener_: function(src, type, arg) {
        var listeners = this.get(src, type);
        for (var i = 0, len = listeners.length; i < len; i++) {
            var listener = listeners[i];
            listener[0].call(listener[1], arg);
        }
    }
};

addEventListener のオブジェクト登録版といった感じです。DOMイベントに依存してるわけではないので、イベント発火側は自由にイベント名を付けられます。
イベント受け取り/発火オブジェクトは、EventProxy クラスを介してやりとり。

もちろん Closure Library ではいろいろ他にもごにょごにょしてるけど、実験用ならこれくらいでいいかなー。

使い方は Closure Library と同じです。 簡単な QUnitテスト。

/**
 * @param {Function} childCtor
 * @param {Function} parentCtor
 */
function inherits(childCtor, parentCtor) {
    function tempCtor() {};
    tempCtor.prototype = parentCtor.prototype;
    childCtor.superClass_ = parentCtor.prototype;
    childCtor.prototype = new tempCtor();
    childCtor.prototype.constructor = childCtor;
}

var TargetClass = function() {
    util.events.EventTarget.call(this);
};
inherits(TargetClass, util.events.EventTarget);

var ListenerClass = function() {
    this.eh_ = new util.events.EventHandler();
};
ListenerClass.prototype = {
    count_: 0,
    addEvent: function(src, type, listener, opt_scope) {
        this.eh_.listen(src, type, listener, opt_scope);
    },
    removeEvent: function(src, type, listener, opt_scope) {
        this.eh_.unlisten(src, type, listener, opt_scope);
    },
    increment: function(args) {
        this.count_++;
    },
    getCount: function() {
        return this.count_;
    }
};

//
var target = new TargetClass();
var listener = new ListenerClass();

module("EventHandler/Target test", {
    setup: function() {
        listener.addEvent(target, 'foo', listener.increment, listener);
    },
    teardown: function() {
        listener.removeEvent(target, 'foo', listener.increment, listener);
    }
});

asyncTest("dispatch test", function() {
    expect(2);
    ok(true);
    target.dispatchEvent('foo');
    setTimeout(function() {
        var count = listener.getCount();
        equals(count, 1, "number");
        start();
    }, 13);
});

asyncTest("dispatched count test", 2, function() {
    expect(2);
    ok(true);
    var count = listener.getCount();
    equals(count, 1, "number");
    start();
});

DOMイベントに依存せずリスナを設定/発火できるので、DOMイベント張りまくって遅くなったりしたらこういう仕組みもありですね。

綺麗なクラス設計でメンテしやすいJSを書くには

シンプルなHTMLを書く。これに尽きる。
HTMLがシンプルならJSもシンプルにできる(CSSはちょっと別)。
WebアプリのクライアントサイドはHTMLがやっぱり基本なんだ。