ヒャッハーなアプリを作ったら賞をもらった話

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

この記事は、クソアプリ Advent Calendar の22日目の記事です。

一昨年にハッカソンデビューして、今年で3年目になりました。もうすぐ4年目になるけど。
今年は5つのハッカソンに参加し、3つ賞をいただくという謎の好成績…。

そして、なんと!
そのノリで Mashup Awards 11 で「クルマde賞」をいただきました!フゥー!!
本当にありがとうございます!

で、そのアプリの紹介とか中のプログラムとか個人が思うハッカソンのコツ?とか
なんかその辺を大雑把に紹介しようと思います。主にアドベントカレンダーのために。

やりたいこと

  • 受賞アプリの紹介
  • 中のプログラムを一部解説
  • ほめられたい(願望)

アプリ紹介

今回、受賞したアプリは
「クルマと道の未来を描く もっと!しずみちinfo」ハッカソンにて開発しました…

しずみちひゃはーMAPⅡ!!!!!

スクリーンショット 2015-12-21 22.22.49.png

「クルマと道路等の情報を利用して、静岡市の未来を考えよう!」というハッカソンのテーマに沿った結果、
「某S市のAPIが生体コンピューターになって人類を支配し始める」とか
「謎の砂漠化によって荒廃した大地と共に荒れ果てた市民が肩パッドつけて暴れまわる」とか
そういった未来 想定して作成された「自身の危険を察知して回避するサーチ&アウェイアプリ」です。

まず、メインであるマップ画面。
このように砂漠化しています。

スクリーンショット 2015-12-21 22.24.45.png

大通り以外は砂で埋まってしまっていますが、ちゃんと実マップデータを元に作成しています。

そのマップに対して、このような感じでレーダーが動いています。

スクリーンショット 2015-12-21 22.20.25.png

なんだか危なそうな地点が検知されて表示されていますね。

スクリーンショット 2015-12-21 22.20.54.png

スクリーンショット 2015-12-21 22.21.19.png

まず、ひでぶポイント。

hidebu.png

これは、静岡市が提供している「シズオカ型オープンデータ」より
「交通事故多発地帯」を取得して、マップ上に表示しています。

次に、あべしポイント。

abeshi 23.11.57.png

同じく「シズオカ型オープンデータ」より「急な飛び出し多発地帯」を取得&表示しています。

そして、プリウス。

madcar.png

こちらは、トヨタIT開発センターが提供している「クルマ情報API」より
危険運転しているクルマを判定して、そのクルマの位置情報を元に表示しています。

ちなみに、モヒカンはアプリユーザーです。

そんな感じで、こんなに危険がいっぱいの世の中なのですが、
もし、それらの地点に近づいてしまった場合、なんと戦闘が始まってしまいます!

スクリーンショット 2015-12-21 22.21.50.png

※ 私は静岡県も好きですが、山梨県も大好きです。

ああ、なんて恐ろしい!
こんなことにならないために、全力で逃げましょう!!

...というアプリだったとさ。ヤレヤレ ┐(´ー`)┌ マイッタネ

プログラム解説

とまあ、確実にクレイジーなアプリなんですが、これをハッカソン2日間で作ってきました。
ぶっちゃけ、ハッカソンを乗り切ればいいと思っていたので、それなりにロークオリティです。(・ω<)

レーダー部分

これは一番最初にパパッと浮かんだアイデアでした。
画像を2枚重ねて、針(?)の画像だけ回転させればいいじゃん!

ということで、デザイナーさんにお願いして以下のような画像を作ってもらいました。

ore_bg.png ore_hari.png

要は、

  • 真ん中に穴が空いた画像
  • 真ん中に針とエフェクトだけ書いて↑の画像と同じサイズの画像

を用意してもらった感じです。

なんで穴が空いている画像と針(?)の画像が同じサイズかというと、
回転時の中心点の調整に悩みたくなかったからです。

画像サイズがデッカくなるので、アプリのリソースとしては良くないのですが、
デモするだけなら、これで十分です。

ちなみにこのアプリ、Nexus 9 でしか動きません。
マルチ対応なんてハッカソンには不要です。

もちろん、画面サイズぴったりにしているので、鬼のような画像サイズです。w

で、針(?)を自身の中心で360度回転させたコードがコチラ。

RotateAnimation ra = new RotateAnimation(
            0,
            -360,
            Animation.RELATIVE_TO_SELF,
            0.5f,
            Animation.RELATIVE_TO_SELF,
            0.5f);
    ra.setDuration((long) (6f * 1000));
    ra.setInterpolator(new LinearInterpolator());
    ra.setRepeatCount(Animation.INFINITE);
    ra.setAnimationListener(RadarFragment.this);

    // Start the animation
    hari.startAnimation(ra);

針(?)が上に来たら(一周したら)更新処理を入れたかったので、
Animation.AnimationListener の onAnimationRepeat でコールバックしてました。

マップ部分(主に砂漠化)

さて、メインのマップ部分ですが、コイツ本当にロークオリティ

本当はタイル表示に対応して、どんなにスクロールさせても一面の砂漠!
というふうにしたかったのですが、さすがに時間が足りないので断念。

じゃあ、どうしたかというと
ペライチの砂漠画像をデザイナーさんに用意していただいて、GroundOverlay で上に重ねただけです。

// マップのオーバーレイ(静岡駅中心)
GroundOverlayOptions options = new GroundOverlayOptions()
        .position(new LatLng(34.9715229, 138.3891491), 6500)
        .image(BitmapDescriptorFactory.fromResource(R.drawable.map3000))
        .anchor(0.5f, 0.5f);
mMap.addGroundOverlay(options);

position メソッドの第二引数 width の単位が m(メートル)で調整に地味に苦戦しました。
とはいえ、GroundOverlay した画像の上に、交差点の名称とか出してくれるグーグルマップの仕様のおかげで、
一気にそれっぽく見えるようにできました!すげー!!

ただし、固定画像なので範囲外までスクロールすると普通のマップが表示されます。
でも、そんなの関係ねぇ!(デモだけに

クルマ位置とか危険地帯とかの分析は、サーバーサイドの人に丸投げしました。
フロント側が苦心することじゃないからね。仕方ないね。

こちらは、それら分析情報を JSON で受け取ります。

サーバー側とHTTP 通信するので、ここで AsyncTask の登場です。
秘伝のソースのように継ぎ足された結果、このときのオレオレカスタム基底 AsyncTask はこんな感じです。

public abstract class BaseAsyncTask<Param, Progress, Result> extends
        AsyncTask<Param, Progress, Result> {

    private static String TAG = "BaseAsyncTask";

    public interface Callback<Result> {
        /**
         * 例外発生
         *
         * @param t 発生した例外
         */
        void onException(Throwable t);

        /**
         * 非同期処理完了
         *
         * @param result 処理結果
         */
        void onPostExecute(Result result);

        /**
         * 非同期処理の後処理
         */
        void onFinally();
    }

    public enum CallbackMethod {
        EXCEPTION,
        POST_EXECUTE,
        FINALLY,
    }

    private WeakReference<Context> contextReference;
    private WeakReference<Callback<Result>> callbackReference;
    private Throwable throwable;
    private Result result;

    /**
     * コンストラクタ
     *
     * @param context コンテキスト
     */
    public BaseAsyncTask(Context context) {
        this.contextReference = new WeakReference<>(context);
        this.callbackReference = null;
    }

    /**
     * コンストラクタ
     *
     * @param context コンテキスト
     * @param callback コールバック処理
     */
    public BaseAsyncTask(Context context, Callback<Result> callback) {
        this.contextReference = new WeakReference<>(context);
        this.callbackReference = new WeakReference<>(callback);
    }

    @Override
    protected final void onPreExecute() {
        try {
            preExecute();
        } catch (Throwable t) {
            throwable = t;
        }
    }

    @SafeVarargs
    @Override
    protected final Result doInBackground(final Param... params) {
        Result result = null;

        try {
            result = doInBackground(params[0]);
        } catch (Throwable t) {
            throwable = t;
        }

        return result;
    }

    @SafeVarargs
    @Override
    protected final void onProgressUpdate(final Progress... values) {
        try {
            progressUpdate(values[0]);
        } catch (Throwable t) {
            throwable = t;
        }
    }

    @Override
    protected final void onPostExecute(final Result result) {
        if (throwable == null) {
            postExecute(result);
        } else {
            exception(throwable);
        }

        // 後処理
        finallyExecute();
    }

    /**
     * 非同期処理の前処理
     */
    protected void preExecute() {

    }

    /**
     * 非同期処理の進捗処理
     *
     * @param value 進捗処理に渡される値
     */
    protected void progressUpdate(final Progress value) {

    }

    /**
     * コールバック呼び出し処理
     *
     * @param callbackMethod コールバックメソッド enum
     */
    protected void callback(final CallbackMethod callbackMethod) {
        Callback<Result> callback = getCallback();
        if (callback == null) {
            return;
        }

        // コールバック呼び出し
        onCallback(callback, callbackMethod);
    }

    /**
     * コールバック呼び出し
     *
     * @param callback コールバック
     * @param callbackMethod コールバックメソッド enum
     */
    private void onCallback(final Callback<Result> callback, final CallbackMethod callbackMethod) {
        switch (callbackMethod) {
            case EXCEPTION:
                callback.onException(throwable);
                break;
            case POST_EXECUTE:
                callback.onPostExecute(result);
                break;
            case FINALLY:
                callback.onFinally();
                break;
            default:
                break;
        }
    }

    /**
     * コンテキストの取得
     *
     * @return コンテキスト
     */
    protected final Context getContext() {
        Context context = contextReference.get();
        if (context == null) {
            Log.w(TAG, "Context is null.");
        }

        return context;
    }

    /**
     * コールバックの取得
     *
     * @return コールバック
     */
    protected final Callback<Result> getCallback() {
        if (callbackReference == null) {
            Log.w(TAG, "Callback is not set at initialization.");
            return null;
        }

        Callback<Result> callback = callbackReference.get();
        if (callback == null) {
            Log.w(TAG, "Callback is null.");
        }

        return callback;
    }

    /**
     * リソースファイルから文字列を取得
     *
     * @param id リソース ID
     * @return リソース ID にひもづく文字列
     */
    protected final String getString(final int id) {
        Context context = getContext();
        if (context == null) {
            Log.w(TAG, "String resource can't get.(id=" + String.valueOf(id) + ")");
            return null;
        }

        Resources res = context.getResources();
        return res.getString(id);
    }

    /**
     * リソースファイルから文字列を取得
     *
     * @param id リソース ID
     * @param formatArgs フォーマットに埋め込む引数
     * @return リソース ID にひもづく文字列
     */
    protected final String getString(final int id, final Object... formatArgs) {
        Context context = getContext();
        if (context == null) {
            Log.w(TAG, "String resource can't get.(id=" + String.valueOf(id) + ")");
            return null;
        }

        Resources res = context.getResources();
        return res.getString(id, formatArgs);
    }

    /**
     * 処理中に例外発生
     *
     * @param t 発生した例外
     */
    private void exception(final Throwable t) {
        callback(CallbackMethod.EXCEPTION);
    }

    /**
     * 非同期処理完了
     *
     * @param result 非同期処理の結果
     */
    private void postExecute(final Result result) {
        this.result = result;
        callback(CallbackMethod.POST_EXECUTE);
    }

    /**
     * 非同期処理の後処理
     */
    private void finallyExecute() {
        callback(CallbackMethod.FINALLY);
    }

    /**
     * 非同期処理
     *
     * @param param パラメータ
     * @return 処理結果
     * @throws Throwable 発生した例外
     */
    protected abstract Result doInBackground(final Param param) throws Throwable;

}

勝手に使ってもいいですが、責任は取りませんので、ご了承ください。(`・ω・´)キリッ
ちなみに、今なおバージョンアップしています。
最近大型バージョンアップ(?)したので、もうこのころの面影はありません…。・゚・(ノ∀`)・゚・。

これを HTTP で GET な感じにカスタマイズし、さらにレスポンスの JSON をパースするのですが、
ライブラリ使うのめんどくさかったので、Map<String, Object> にしてくれるメソッドを作りました。

/**
 * JSON文字列(配列)を JSON文字列(オブジェクト)に変換
 *
 * @param jsonArrayString JSON文字列(配列)
 * @return JSON文字列(オブジェクト)
 */
protected final String jsonArrayStringToJsonObjectString(final String jsonArrayString) {
    if (TextUtils.isEmpty(jsonArrayString)) {
        return jsonArrayString;
    }

    if (TextUtils.indexOf(jsonArrayString, '{') == 0) {
        return jsonArrayString;
    } else if (TextUtils.indexOf(jsonArrayString, '[') == 0) {
        StringBuilder sb = new StringBuilder(jsonArrayString);
        sb.insert(0, "{\"" + JSON_ARRAY_KEY + "\":");
        sb.append("}");
        return sb.toString();
    } else {
        return jsonArrayString;
    }
}

/**
 * JSON文字列 を Map に変換
 *
 * @param jsonString JSON文字列
 * @return JSON文字列 から生成した Map
 * @throws JSONException JSONのパース失敗
 */
protected final Map<String, Object> jsonStringToMap(String jsonString) throws JSONException {
    JSONObject jsonObject = new JSONObject(jsonString);
    return jsonObjectToMap(jsonObject);
}

/**
 * JSONオブジェクト を Map に変換
 *
 * @param jsonObject JSONオブジェクト
 * @return JSONオブジェクト から生成した Map
 * @throws JSONException JSONのパース失敗
 */
protected final Map<String, Object> jsonObjectToMap(JSONObject jsonObject) throws JSONException {
    Map<String, Object> map = new HashMap<>();

    Iterator<String> jsonKeys = jsonObject.keys();
    while (jsonKeys.hasNext()) {
        String jsonKey = jsonKeys.next();
        Object jsonValue = jsonObject.get(jsonKey);

        if (jsonValue instanceof JSONObject) {
            // JSONオブジェクトなら再帰
            map.put(jsonKey, jsonObjectToMap((JSONObject) jsonValue));
        } else if (jsonValue instanceof JSONArray) {
            // JSON配列オブジェクトなら、リストを作成
            map.put(jsonKey, jsonArrayToList((JSONArray) jsonValue));
        } else {
            // プリミティブなら、そのまま Map に設定
            map.put(jsonKey, jsonValue);
        }
    }

    return map;
}

/**
 * JSON配列オブジェクト を List に変換
 *
 * @param jsonArray JSON配列オブジェクト
 * @return JSON配列オブジェクトから生成した List
 * @throws JSONException JSONのパース失敗
 */
protected final List<Object> jsonArrayToList(JSONArray jsonArray) throws JSONException {
    List<Object> valueList = new ArrayList<>();

    for (int i = 0; i < jsonArray.length(); i++) {
        JSONObject jsonObject = jsonArray.getJSONObject(i);
        if (jsonObject != null) {
            // JSONオブジェクトが取得できたら、Map にして追加
            valueList.add(jsonObjectToMap(jsonObject));
        } else {
            // JSONオブジェクトが取得できない=プリミティブとして、そのまま追加
            valueList.add(jsonArray.get(i));
        }
    }

    return valueList;
}

解析したらGPS座標と表示タイプ(クルマとか危険地帯とか)が
得られるようにしていただいたので、適宜マーカーにつっこんでいきます。

MarkerOptions options = new MarkerOptions()
    .position(new LatLng(hyahhaItem.getLat(), hyahhaItem.getLon()))
    .icon(BitmapDescriptorFactory.fromResource(hyahhaItem.getTypeResource());
hyahhaItemList.add(mMap.addMarker(options));

戦闘部分

ロジック的には、このアプリの中で一番気を使った気がする。ほぼコピペだけども。

まず、GPS座標の2点間の距離の算出ですが、ここからいただいてきました。
すごい有用なサイトだと個人的に思っている。ヒュベニの公式はちょっと勉強し直した。

で、ここで危険地帯と一定値以下の距離にいた場合、自動的に動画を再生します。
アクティビティの実装が、こちらです。

public class EncountActivity extends BaseMobileActivity
        implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {

    @Bind(R.id.videoView)
    VideoView mVideoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        fullScreen();
        noActionBar();
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_encount);

        mVideoView.setOnPreparedListener(this);
        mVideoView.setOnCompletionListener(this);
    }

    @Override
    protected void onResume() {
        super.onResume();

        mVideoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.movie_encount));
    }

    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        mVideoView.start();
    }

    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        finish();
    }
}

BaseMobileActivity は秘伝のアクティビティです。
Butter Knife の初期処理とフルスクリーン設定とかしか書いてないけど。

意外とこの動画の URI を見つけるのと、
onPrepared メソッドで start させることで自動的に再生させるっていうのにちょっと悩んだ。

まぁでも、デモ中は移動しないということで、
GPS座標の2点間の距離まわりのロジックはボツになったんだけどね!!

見極めダイジ、ゼッタイ。

ハッカソンのコツ(?)

ということで、ちょろちょろ書いてしまったけども、
個人的にハッカソンで気をつけることについて、こんな風に考えてます。

  • 100%成功するデモ専用アプリを作れ!

データは多少デモデータでも良い!
その辺は「今後に期待!」的なトークでカバーする!

  • 必要なロジックと不必要なロジックは切り分けろ!

そのアプリの主機能は作るべきだけど、作り込む必要はない!
時間はものすっごく限られているんだから、とにかくデモ以外のことは考えるな!
Androidの場合は、マルチスクリーン対応なんて考えたら終わるぞ!

  • 過去のコードは貯めてライブラリ化しておけ!

ハッカソンでは普段仕事でしないことをすることが多い!
そのため、ちょっと変わったロジックを作ることも多い!
この辺をユーティリティ等にしておくと、仕事でも以降のハッカソンでも大活躍する!
そして、スピード開発が可能に…!?

まとめ

  • 受賞したよ!本当にありがとうございます!
  • ハッカソンでは、デモ専用アプリに特化するのがオススメだよ!
  • 賞もらってしまったので、このアプリのプレゼンを会社でもしたよ!失笑の嵐だったよ!

参考