iOS7の立体的な壁紙をAndroidのライブ壁紙で作ってみた

2013/6/13 11680hit

iOS7の立体的に見える壁紙が素敵だったのでAndroidのライブ壁紙で再現してみました。
ソースコード付きで解説します。


昨日アップした動画は動く向きが逆だったので修正版に変更しました。

仕組みとしてはライブ壁紙になっていて、ジャイロセンサーの値を元に写真の位置をずらして立体的にしています。

特殊な点としては画面を描画するスレッドは別スレッドにしてループ処理内で一定時間ごとに描画させています。
onSensorChanged内で描画処理を行うと言うのも考えたのですが、描画処理がそこそこ重いので描画が終わる前に次のonSensorChangedがキューイングされてしまうことが有り
キューがどんどんたまると次第にセンサーが取ってくる値と実際の描画がずれてくると言う状態になってしまったので、描画処理を別スレッドに分けてonSensorChangedで集計した結果を元に一定時間ごと描画処理を行うという仕組みとしました。
Androidというとメインスレッド以外で画面を描画出来ないという印象がありますがSurfaceViewはスレッド関係なく描画することができます。

一定時間ごとの描画はタイマーではなくスレッドのスリープを使っています。 
あと取得してきた値に4かけて大きめに画像が動くようにしています。
これはデモのために分かりやすくするのが目的なのでより控えめにしたい場合はこの数を減らすといいです。

ライブ壁紙を作る時はActivityではなくWallpaperServiceクラスを継承して画面を作ります。


public class FlatWallPaper extends WallpaperService {


onCreateEngine()をOverrideしてEngineクラスを返してやります。
実際の処理はEngineクラスを継承した中で行います。

@Override
public Engine onCreateEngine() {
Engine engine = new MyEngine();
return engine;
}

ライブ壁紙は標準ではDebuggerにAttachしないため、デバッグしたい時はandroid.os.Debug.waitForDebugger()を呼んでやる必要があります。

@Override
public void onCreate() {
super.onCreate();
// Debugを行う時は下のコメントを有効にする
// android.os.Debug.waitForDebugger();
}

Engineクラスを継承して独自の処理を行います。
また、センサー処理のイベントを処理するためにSensorEventListenerを実装します。
この部分はおこのみで別クラスにしたり匿名クラスにすることもできます。 

// センサーの値を取得するためにSensorEventListenerを実装します。
private class MyEngine extends Engine implements SensorEventListener {
private SensorManager sensorManager;

private int centerX; // 画像を中心に位置させる場合の横軸
private int centerY; // 画像を中心に位置させる場合の縦軸

写真はResourcesからInflateして使っています。
なので、画像はResourcesに保存した写真の決め打ち。
実際にアプリとして配布するなら端末内の写真を選択できるようにした方がいいと思います。

final Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.img); // 壁紙に表示する画像

冒頭で説明したように描画は別スレッドで行なっています

private Thread drawThread; // 描画を行うスレッドは別スレッドとする

別スレッドで編集した値を参照する場合、volatileを指定してコンパイラが最適化するのを抑制します。
コストを下げるためにsynchronizedにはせずにvolatileの値を書き込むときに従来の値に依存しない作りとしています。
不変であるBitmapを除くと描画処理で参照する変数は以下の3つだけです。

// 描画スレッドからも最新情報が取得できるようにvolatileで宣言する
volatile int imgX; // 画面を描画する横軸
volatile int imgY; // 画面を描画する縦軸
volatile boolean draw; // trueの間描画を続ける

中心時の画像の位置(左上の位置)は画面の中心から画像を半分左上に動かした位置となります。
この位置は毎回取得するのではなく、画面サイズが変更になる場合だけ一度処理を行なっています。

@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.onSurfaceChanged(holder, format, width, height);
centerX = (width/ 2) - (image.getWidth() / 2);
centerY = (height/ 2) - (image.getHeight() / 2);
}


SurfaceViewは複数スレッドから描画を行うことができますが、その代わりにスレッドセーフを自分で保証してやる必要があります。
そのため、SurfaceHolder.lockCanvas()を呼び、描画が終わったらunlockCanvasAndPost()で開放してやる必要があります。

Canvas canvas = sh.lockCanvas();
if (canvas == null) {
continue; // そういう時もあるさ
}
// 一度画面をクリアする 画像が明らかに巨大な場合は不要ではある
canvas.drawColor(0, Mode.CLEAR);
// 画像を描画する
canvas.drawBitmap(image, imgX, imgY, paint);
// 描画が終わったらとっととアンロック
sh.unlockCanvasAndPost(canvas);
sh.unlockCanvasAndPost(canvas);

CPUを休ませるためにスレッドを一時停止します。
ここでは10ミリ秒休憩させています。
この処理を入れないとCPUが100%で回り続けてバッテリーを消耗します。(しかしレスポンスは最高になります)
割り込みなどでsleepが途中で解除された場合InterruptedExceptionが発行されます。
しかしながら、今回の場合、問題とならないためcatch句では何も行なっていません。

try {
// 一時停止
Thread.sleep(10l);
} catch (InterruptedException e) {
}

ジャイロセンサーの値はX軸(画面の上下真ん中を通る線)Y軸(画面の左右真ん中を通る線)Z軸(画面の中心を垂直に貫く線)の順番で配列として渡ってきます。
X軸で回転した場合(端末を縦方向に回した場合)画像を上下させます。
Y軸で回転した場合(端末を首振させた場合)画像を左右させます。
Z軸で回転した場合(端末を回転させた場合)何も行いません。
ジャイロセンサーの値は角度センサーや磁気センサーと違い絶対的な角度ではなく、角速度(前回からの時間単位ごとの角度の変化)の値なので
値を加算していって実際の角度を求めます。
といっても、今回の場合絶対的な角度は必要ないので、ここ最近どういうふうに動いているかだけを求めています。

public void onSensorChanged(SensorEvent event) {
float[] values = event.values;
shiftX -= values[1]*SPEED;
shiftY -= values[0]*SPEED;
if (shiftX > MAX) {
shiftX = MAX;
}
if (shiftX < -MAX) {
shiftX = -MAX;
}
if (shiftY > MAX) {
shiftY = MAX;
}
if (shiftY < -MAX) {
shiftY = -MAX;
}
// 何も動かしていないときじんわりと中央に戻す
shiftX *= 0.999f;
shiftY *= 0.999f;

描画スレッドへはvolatileなimgXとimgYに値をセットして渡しています。
どちらもint型なのでatomic性が保証されます。

// 渡す時はintでまるめちゃう
imgX = (int) (centerX + shiftX);
imgY = (int) (centerY + shiftY);
}

描画スレッドへ渡す最後の1つのdraw変数は描画スレッドを終了させるのに使います。
drawの値をfalseにするとループが終了し描画スレッドが終了します。

// おつかれさまでした
@Override
public void onDestroy() {
sensorManager.unregisterListener(this);
draw = false;
}


ソース全容

package org.firspeed.flatwall;

import java.util.List;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.service.wallpaper.WallpaperService;
import android.view.SurfaceHolder;

public class FlatWallPaper extends WallpaperService {

@Override
public Engine onCreateEngine() {
Engine engine = new MyEngine();
return engine;
}

@Override
public void onCreate() {
super.onCreate();
// Debugを行う時は下のコメントを有効にする
// android.os.Debug.waitForDebugger();
}

// センサーの値を取得するためにSensorEventListenerを実装します。
private class MyEngine extends Engine implements SensorEventListener {
private SensorManager sensorManager;

private int centerX; // 画像を中心に位置させる場合の横軸
private int centerY; // 画像を中心に位置させる場合の縦軸
final Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.img); // 壁紙に表示する画像
private Thread drawThread; // 描画を行うスレッドは別スレッドとする

// 描画スレッドからも最新情報が取得できるようにvolatileで宣言する
volatile int imgX; // 画面を描画する横軸
volatile int imgY; // 画面を描画する縦軸
volatile boolean draw; // trueの間描画を続ける
/**
* 描画開始時、センサーを有効にして描画スレッドを開始
*/
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_GYROSCOPE);
sensorManager.registerListener(this, sensors.get(0), SensorManager.SENSOR_DELAY_GAME);
drawThread = new Drawer();
draw = true;
drawThread.start();
}

/**
* 画面のサイズが変更になった場合などは画像の中心位置を計測し直す
*/
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.onSurfaceChanged(holder, format, width, height);
centerX = (width/ 2) - (image.getWidth() / 2);
centerY = (height/ 2) - (image.getHeight() / 2);
}

/**
* 描画スレッド
* @author kenz
*/
private class Drawer extends Thread {
@Override
public void run() {
super.run();
int preX = 0;
int preY = 0;
Paint paint = new Paint();
SurfaceHolder sh = getSurfaceHolder();
while (draw) {
// 描画処理は重たいので位置が動いていなければ再描画しない
if (preX != imgX || preY != imgY) {
Canvas canvas = sh.lockCanvas();
if (canvas == null) {
continue; // そういう時もあるさ
}
// 一度画面をクリアする 画像が明らかに巨大な場合は不要ではある
canvas.drawColor(0, Mode.CLEAR);
// 画像を描画する
canvas.drawBitmap(image, imgX, imgY, paint);
// 描画が終わったらとっととアンロック
sh.unlockCanvasAndPost(canvas);
// 現在の位置を記録する
preX = imgX;
preY = imgY;
}
try {
// 一時停止
Thread.sleep(10l);
} catch (InterruptedException e) {
}
}
}
}

@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
super.onSurfaceDestroyed(holder);
draw = false; // 描画停止
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}

// ジャイロセンサーの値を加算していく
private float shiftX;
private float shiftY;
private static final float SPEED = 4f; // 速度を調整
private static final float MAX = 400f; // 最大のズレ幅
@Override
public void onSensorChanged(SensorEvent event) {
float[] values = event.values;
shiftX -= values[1]*SPEED;
shiftY -= values[0]*SPEED;
if (shiftX > MAX) {
shiftX = MAX;
}
if (shiftX < -MAX) {
shiftX = -MAX;
}
if (shiftY > MAX) {
shiftY = MAX;
}
if (shiftY < -MAX) {
shiftY = -MAX;
}
// 何も動かしていないときじんわりと中央に戻す
shiftX *= 0.999f;
shiftY *= 0.999f;
// 渡す時はintでまるめちゃう
imgX = (int) (centerX + shiftX);
imgY = (int) (centerY + shiftY);
}
// おつかれさまでした
@Override
public void onDestroy() {
sensorManager.unregisterListener(this);
draw = false;
}
}

}

前:iOS7の動く壁紙をAndroidで作ってみた 次:Google I/O 2013 MakerFaireで遊んできました

関連キーワード

[iPhone][Android][Java][モバイル][IT]

コメント

名前:mmasaki@HelloNavi|投稿日:2013/11/02 11:14

初めまして。
「iOS7の立体的な壁紙をAndroidのライブ壁紙で作ってみた」には大変興味があったので、早速sourceを利用させていただいたところ、どうしてもエラーが発生し、eclipseで実行できません。
もし可能であれば、すべてのsourceを公開していただけませんでしょうか。
ご検討よろしくお願いいたします。

名前:kenz|投稿日:2013/11/04 22:00

実行するには別途XMLやリソースを用意する必要があります。
このアプリに関してはきちんと写真を選ぶことが出来る形でマーケットに公開しようかと思います。

コメントを投稿する

名前URI
コメント