【Androidでゲーム的なアニメーションの実装】第2回.サークルセレクタ編
個人的な解釈が含まれております。
SpinachMediaは本ページの情報をあなたが利用することによって生ずるいかなる損害に対しても一切の責任を負いません。
前回(↓)に続いて第二回です。
【Androidでゲーム的なアニメーションの実装】第1回.テキストウィンドウ編 - SpinachMedia
このままちっちゃいモジュールをたくさん使って、豪華な画面のゲームを作ってやろうと思います。
何をつくろうかなぁ。
今回は最近流行っているUI?を実装してみます。
画面内のアイコンをタップすると、花びらが開くようにアイコンが円形に飛び出すUIですね。
なんていうUIなんでしょうか?
本章では勝手に「サークルセレクタ」と名づけて実装しています。
【Androidでゲーム的なアニメーションの実装】
第1回.サークルセレクタ編
勝手に名前をつけてしまいましたが…
「Path」や「matomemo」あたりで驚いたUIです。
以下にスクリーンショット。
左の画面の右下のアイコンをタップすると、
右の画面の様に「バラっと」開きます。
→
なんなんでしょうね。これ。
かっこいい。
いきなり実装結果
こんなかんじになりました。
アイコンは「ぴぽや倉庫」様から拝借しております。
ポイント
- 親ボタンをタップすると、子要素が円形に飛び出す。
- 一度バウンドする。
- 開ききるまでに、親要素が90度回転する。
- 子要素もどうやら回転している。
むむ…難しいな…。
今回の開発条件
- Android
- SurfaceView
開発方針
今回、独自に以下の4つのクラスを定義します。
・CircleSelecterAnimationクラス
メインクラス。
アニメーションを実行し、動的に座標などを操作する。
・CIrcleSelecterParentクラス
親ボタンクラス。
・CircleSelecterChildクラス
子ボタンクラス。
・AnimationTypeクラス
親ボタンの配置場所を設定するENUMクラス。
今回は左下に親ボタンを配置する実装のみ。
全体としてはこんな図でしょうか・・・?
わかりづらいな・・・。
今回使った小技
滑らかな動きを再現するにあたって、以下の様な考え方をしています。
①それぞれの子ボタンは、3種類の座標情報を持ちます。
- startRect・・・開く前の自分の座標
- endRect・・・一番開いた時の自分の座標
- viewRect・・・描画に利用する座標
②viewRectの位置を、startRect と endRect の間の直線で行き来させる。
③滑らかに行き来させる為に正弦波(Sin)を利用する。
「endRect」と「startRect」との差に対して、
正弦波の計算で導き出した「0~1」の少数を掛けて座標を決定する。
④各要素の配置は、正弦余弦を利用した円座標に配置する。
ソースコード
色々と省いてます。
完全版は画面最下部のGithubをご覧下さい。
MainActivity.java
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainView view = new MainView(this); setContentView(view); } }
MainView.java
public class MainView extends SurfaceView implements SurfaceHolder.Callback, Runnable { //変数は割愛します。 public MainView(Activity activity) { super(activity); holder = getHolder(); holder.addCallback(this); this.activity = activity; res = getContext().getResources(); paint = new Paint(); //サークルセレクターを初期化。 //サークルセレクターの表示位置を画面の左下に設定。 //コンストラクタはこのタイミングで動作させる必要がある。 //(SurafaceView開始後では、CircleSelecterAnimation内のHandlerと競合してしまい、インスタンス化できない。) cercleSelecterAnimation = new CircleSelecterAnimation( AnimationType.LeftBottom); } @Override public void run() { //画面サイズの取得 scWidth = getWidth(); scHeight = getHeight(); //メインのボタンを登録する。 cercleSelecterAnimation.addMainCircle(R.drawable.s_menu_parent, res); //メインボタンのサイズを登録する為に、画面サイズを渡す。 cercleSelecterAnimation.setMainCircleRect(scWidth, scHeight); //子要素を追加する。画面のスペースと、メモリの許す限りいくらでも追加可。 //3~5個を推奨。 cercleSelecterAnimation.addChildCircle(R.drawable.s_menu_child_01, res); cercleSelecterAnimation.addChildCircle(R.drawable.s_menu_child_02, res); cercleSelecterAnimation.addChildCircle(R.drawable.s_menu_child_03, res); cercleSelecterAnimation.addChildCircle(R.drawable.s_menu_child_04, res); while (thread != null) { try { canvas = holder.lockCanvas(); canvas.drawColor(Color.WHITE); // サークルボタンの子要素の描画 for(CircleSelecterChild circle:cercleSelecterAnimation.circleSelecterList){ canvas.drawBitmap(circle.image, circle.drawRect, circle.viewRect, paint); } // サークルボタンの親ボタン描画 canvas.drawBitmap(cercleSelecterAnimation.mainCircle.image, cercleSelecterAnimation.mainCircle.matrix, paint); } catch (Exception e) { } finally { } } } }
CircleSelecterAnimation.java
class CircleSelecterAnimation { /** * コンストラクタ。 * 親ボタンの表示位置を決める。 * @param type */ public CircleSelecterAnimation(AnimationType type) { this.type = type; circleSelecterList = new ArrayList<circleselecterchild>(); } /** * サークルセレクタのメインボタンを登録する。 * @param id * @param res */ public void addMainCircle(int id,Resources res){ mainCircle = new CircleSelecterParent(); mainCircle.image = BitmapFactory.decodeResource(res, id); mainCircle.drawRect = new Rect(0, 0, mainCircle.image.getWidth(), mainCircle.image.getHeight()); mainCircle.matrix = new Matrix(); mainCircle.matrix.postRotate(0); } /** * 画面サイズを渡してボタンの描画領域を決定する。 * addMainCircleの後に実行される必要がある。 * @param screenWidth * @param screenHeight */ public void setMainCircleRect(int screenWidth, int screenHeight) { this.screenWidth = screenWidth; this.screenHeight = screenHeight; switch(type){ case LeftBottom: mainCircle.viewRect = new Rect(0, screenHeight - mainCircle.image.getHeight(), mainCircle.image.getWidth(), screenHeight); mainCircle.matrix.setTranslate(0, screenHeight - mainCircle.image.getHeight()); break; case CenterBottom: //TODO 実装せねば break; case RightBottom: //TODO 実装せねば break; } } /** * サークルセレクタの子要素を追加する。 * 2~5個を推奨。 * 試しに50個やってみたらOutOfMemory. * @param id * @param res */ public void addChildCircle(int id,Resources res){ CircleSelecterChild circle = new CircleSelecterChild(); circle.image = BitmapFactory.decodeResource(res, id); circleSelecterList.add(circle); settingPlace(); } private void settingPlace(){ for(int i = 0;i < circleSelecterList.size();i++){ CircleSelecterChild circle = circleSelecterList.get(i); switch(type){ case LeftBottom: //初期位置の設定 circle.startRect = new Rect(0, screenHeight - circle.image.getHeight(), 0 + circle.image.getWidth(), circle.image.getHeight()); //描画位置の設定(初期位置) circle.viewRect = new Rect(0, screenHeight - circle.image.getHeight(), 0 + circle.image.getWidth(), screenHeight); //画像内の描画位置の決定 circle.drawRect = new Rect(0, 0, circle.image.getWidth(), circle.image.getHeight()); //リストの順番によって、座標を変える。 float rot = 90/(circleSelecterList.size() + 1); rot = rot * (i+1); float rad = (float) (rot * Math.PI / 180); double x = Math.sin(rad); double y = Math.cos(rad); //x = 0 ~ 1 の少数。 //画面幅 * x - ボタンの横幅 /2 - 親ボタンの横幅 / 2 int left = (int)((screenWidth*x - circle.image.getWidth()/2) - mainCircle.image.getWidth()/2); int top = (int)((-screenWidth*y) + screenHeight - circle.image.getHeight() / 2 + mainCircle.image.getWidth()/2); //最終的な位置を決定 circle.endRect = new Rect( left, top, left + circle.image.getWidth(), top + circle.image.getHeight() ); break; case CenterBottom: //TODO 実装せねば break; case RightBottom: //TODO 実装せねば break; } } } /** * 表示アニメーションハンドラーのキックメソッド * 実行時にハンドラーが起動済で無いことが条件 */ public void startOpenAnimation() { if(handlerIsRunning){ //例外でも投げるか・・・? }else{ handlerIsRunning = true; for(CircleSelecterChild circle:circleSelecterList){ circle.rot = 0; } mainCircle.selfRadious = 0; openAnimation.sendEmptyMessageDelayed(1, 5); } } /** * サークルセレクターアニメーションハンドラー * 0度から130度までの正弦の値を係数にしてアニメーションを行う。 */ private final Handler openAnimation = new Handler() { @Override public void dispatchMessage(Message msg) { if (msg.what == 1) { //全アニメーションが終わるとtrueになる。 Boolean handlerIsEnd = false; //メインボタンの回転角を追加 mainCircle.selfRadious++; //90度になるまで回転させ続ける。 if(mainCircle.selfRadious < 90){ //マトリックスに値を設定 mainCircle.matrix.postRotate( //回転角を1度追加 1, //回転の中心を設定。 //今描画している領域から、画像の真ん中の位置まで中心をずらす。 mainCircle.viewRect.left + mainCircle.image.getWidth()/2, mainCircle.viewRect.top + mainCircle.image.getHeight()/2 ); } //サークルセレクタの子要素に for(int i = 0;i < circleSelecterList.size();i++){ CircleSelecterChild circle = circleSelecterList.get(i); //ひとつ前のボタンとの30度分の差がついてからアニメーションをスタートする。 if(i > 0){ CircleSelecterChild beforeCircle = circleSelecterList.get(i-1); if(circle.rot == 0 && beforeCircle.rot - circle.rot < 30){ break; } } //ボタンの動きが正弦波の130度を超えた時点で動作を止める。 //(アニメーション的には目的地を一旦通りすぎる様に見える。) if(circle.rot >= 130){ handlerIsEnd = true; }else{ circle.rot++; float rad = (float) (circle.rot * Math.PI / 180); Rect startRect = circle.startRect; Rect endRect = circle.endRect; double x = Math.sin(rad); int left = startRect.left + (int)((endRect.left - startRect.left) * x); int top = startRect.top + (int)((endRect.top - startRect.top) * x); circle.viewRect = new Rect( left, top, left + circle.image.getWidth(), top + circle.image.getHeight() ); handlerIsEnd = false; } } //45度まで処理。 if (!handlerIsEnd) { sendEmptyMessageDelayed(1, 3); }else{ handlerIsRunning = false; isOpen = true; } } else { super.dispatchMessage(msg); } } }; }
上記のコードは色々と省いているので、以下のコードを参考にしてください。
spinachmedia/CircleSelecter · GitHub