某エバカメラタイムトライアル - ReDo

2010年7月 1日

某エバカメラタイムトライアル

Togetter -まとめ「iPhoneアプリ開発できる方を探してます。エヴァカメラと同等の仕様で納期が明日昼と急ですがよろしくお願いします。」
http://togetter.com/li/32751

が面白かったので、カメラの勉強がてらAndroidでやってみました。

ebacamera.jpg

  • オーバーレイ可能なプレビュー付きのリソース画像合成カメラアプリ、という仕様と元アプリはミリシラ解釈。
  • android-sdk-windows\platforms\android-1.6\samples\ApiDemos配下にCameraPreview.javaを参考にしてプレビューを作成します。
  • Activity#addContentView()を使ってLayoutファイルをいじるのをさぼってます。
  • Canvasを使ったoffScreenへの画像合成でカメラ撮影画像とリソースを合成してSDカードに保存します。
  • X06HT(2.1 Eclair)とNexus One(2.2 Froyo)では動作しました。800x480以外だと怪しいです。
  • カメラアプリ初開発でとりかかってから約3時間でした。カメラそのものより画像合成に苦労しました。カメラは簡単だけど楽しいということがよく分かりました。

package youten.redo.ebacamera;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;

// ----------------------------------------------------------------------

public class CameraPreview extends Activity {
	private static final String TAG = "CameraPreview";

	private Preview mPreview;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		// Hide the window title.
		requestWindowFeature(Window.FEATURE_NO_TITLE);

		// Create our Preview view and set it as the content of our activity.
		mPreview = new Preview(this);
		setContentView(mPreview);
		ImageView ebaImage = new ImageView(this);
		ebaImage.setImageResource(R.drawable.eba1);
		this.addContentView(ebaImage, new LinearLayout.LayoutParams
				(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
	}

	// key
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event)
	{
		if(keyCode == KeyEvent.KEYCODE_DPAD_CENTER)
		{
			Log.d(TAG, "onKeyDown KEYCODE_DPAD_CENTER");
			mPreview.takePictureAndSave();
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}


}

// ----------------------------------------------------------------------

class Preview extends SurfaceView implements SurfaceHolder.Callback {
	private static final String BASEPATH = Environment.getExternalStorageDirectory().toString() + "/" + "EBACAMERA";
	SurfaceHolder mHolder;
	Camera mCamera;

	int mHeight = 800;
	int mWidth  = 480;

	Preview(Context context) {
		super(context);

		// Install a SurfaceHolder.Callback so we get notified when the
		// underlying surface is created and destroyed.
		mHolder = getHolder();
		mHolder.addCallback(this);
		mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
	}

	public void surfaceCreated(SurfaceHolder holder) {
		// The Surface has been created, acquire the camera and tell it where
		// to draw.
		mCamera = Camera.open();
		try {
		   mCamera.setPreviewDisplay(holder);
		} catch (IOException exception) {
			mCamera.release();
			mCamera = null;
			// TODO: add more exception handling logic here
		}
	}

	public void surfaceDestroyed(SurfaceHolder holder) {
		// Surface will be destroyed when we return, so stop the preview.
		// Because the CameraDevice object is not a shared resource, it's very
		// important to release it when the activity is paused.
		mCamera.stopPreview();
		mCamera.release();
		mCamera = null;
	}

	public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
		// Now that the size is known, set up the camera parameters and begin
		// the preview.
		Camera.Parameters parameters = mCamera.getParameters();
		parameters.setPreviewSize(w, h);
		mCamera.setParameters(parameters);
		mCamera.startPreview();
		if(h > 0){ mHeight = h; }
		if(w > 0){ mWidth = w; }
	}

	/**
	 * 写真をとってSDカードに保存する。
	 */
	public void takePictureAndSave()
	{
		mCamera.takePicture(null, null, new Camera.PictureCallback()
		{
			public void onPictureTaken(byte[] data, Camera camera) {
				if(data != null)
				{
					try
					{
						BitmapFactory.Options options = new BitmapFactory.Options();
						options.inSampleSize = 2;
						Bitmap cameraBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
						Bitmap ebaBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.eba1);

						Bitmap offBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
						Canvas offScreen = new Canvas(offBitmap);
						offScreen.drawBitmap(cameraBitmap, null, new Rect(0,0,mWidth,mHeight), (Paint)null);
						offScreen.drawBitmap(ebaBitmap, 0, 0, (Paint)null);

						OutputStream os = new FileOutputStream(createFilePath());
						offBitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
						os.close();
					}
					catch(Exception e)
					{
						e.printStackTrace();
					}
				}
				mCamera.startPreview();
			}
		});
	}

	/**
	 * ファイル名を生成する。
	 * @return ファイルパス
	 */
	private static String createFilePath() {
		makeDirectoryIfNotExisted(BASEPATH);
		Date now = new Date(System.currentTimeMillis());
		StringBuffer sb = new StringBuffer(BASEPATH);
		sb.append("/");
		sb.append(1900+now.getYear());
		sb.append("-");
		sb.append(1+now.getMonth());
		sb.append("-");
		sb.append(now.getDate());
		sb.append("-");
		sb.append(now.getHours());
		sb.append("-");
		sb.append(now.getMinutes());
		sb.append("-");
		sb.append(now.getSeconds());
		sb.append(".jpg");
		return sb.toString();
	}

	/**
	 * ディレクトリがなければ作る。
	 * @param path ディレクトリパス
	 * @return oolean 成功時:true 失敗時:false
	 */
	public static boolean makeDirectoryIfNotExisted(String path)
	{
		boolean ret = false;
		File dir = new File(path);
		if(!dir.exists())
		{
			ret = dir.mkdirs();
		}
		else
		{
			ret = true;
		}
		return ret;
	}

}

余談:

こういうアプリの開発が企画として一括いくらなのかは分かりませんが、たとえばこういう「実質コード量として1kに満たず、実コーディング時間が半日を切る様な開発」があったとして、「1. 要件検討(目的の確認)」「2. 仕様検討と顧客の承認」「3. 実装」「4. テスト」「5. 顧客確認」「6. リリースとサポート」のうち、今回の件でGPLとして表に出ている部分は「3. 実装」の一部でしかない様に見えます。

商売としては「0. 契約処理」の様なリードタイムに絡む問題があったり、AppStoreの審査があったりして納期を守ることは大変困難だと思いますが、「動くコード」と「製品とサービス」の間に挟まっているギャップがあって、それを考えさせられる面白い話であって、鬼発注とか技術の安売りとかそういうあたりをあーだこーだ懸念する流れになると少し悲しいネタになってしまう気がします。

コメントする