おしおきアプリを作ったら静岡市から賞をもらった話

  • 13
    いいね
  • 0
    コメント

はじめに

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

ちなみに去年も22日目の担当でした!1年ぶり!!(ヒャッハーなアプリを作ったら賞をもらった話

今年は控えめに1つのハッカソンに参加したきりでした。
まぁ、例によってそこでも明るく楽しくクソってきたので、
そのアプリの紹介と中のコードを大雑把に紹介しようと思います。

もちろん、アドベントカレンダーのために。
そして、どういうわけかそれで静岡市賞をいただいたので、ドヤるために。

やりたいこと

  • 受賞アプリの紹介
  • 中のプログラムを一部解説
  • ドヤる

アプリ紹介

今回、我ら「おしおき倶楽部」チームがご紹介させていただきますアプリは、
「しずみちinfo クルマと道の未来を描く HACK DAYS 2016」ハッカソンにて開発いたしました…

おしおき(仮)!!!!!

スクリーンショット 2016-12-19 7.00.48.png

今年のテーマが何かはすっかり忘れてしまいましたが、
「(現世で)召されるなら、(このアプリで)召されないでみせようホトトギス」
というノリと勢いと若干のひねくれ感で作った、
その名の通りの「危険行為をする歩行者を容赦なく”おしおき”するアプリ」でございます。

主な画面は、こちらとなっております。

スクリーンショット 2016-12-22 4.08.37.png

スクリーンショット 2016-12-22 4.09.44.png

まず、この地図画面の中央でテクテクしているキャラクターは、
あなた様(ユーザー)でございます。

user_1.png

もちろん、あなた様の位置情報を取得して、
ウンザリさせるほどあなた様を画面中央に表示させていただくのでございます。ふふふ。

次に、こちらの顔色悪くテクテクしているキャラクターは、ゾンビでございます。

zombie_1.png

なにか恐ろしい災いの跡が見えますね?
このゾンビは、「しずみち Info API」からの「災害情報」を元に表示しております。

そして、こちらの道路を徘徊しているのが、__ UFO __となっております。

car_danger_2.png

うっかり近づいたら、さらわれてしまうかもしれませんね?
この UFO が徘徊している場所は、
「シズオカ型オープンデータ」からの「ヒヤリハットマップ」を元に表示しております。

そんな感じで、あらゆるところに危険が潜んでいる今の世の中なわけでございますが、
このアプリを使って危険地点を認識し、きちんと安全な行動を心がけるようにすれば、
平穏で、かつ幸せな未来が約束されるのでございます…!

まず、きちんと安全な行動ができた場合、あなた様には
耳障りが良く素敵で感動的な音楽が聞こえると共に、徳(しずおかポイント)が進呈されます。

スクリーンショット 2016-12-22 4.09.16.png

災い(ゾンビ)からの貴重なデレシーンが見られるのです。
もう、この時点で少し幸せになれますよね?
(災害発生地点が移動していいのかなどの細かいことは、目をつぶっていただけると幸いでございます)

この徳でございますが、貯めると
何かしらのクーポンや何かしらのポイントと引き換えることが可能となっております。

スクリーンショット 2016-12-22 4.16.55.png

おや?何か不穏な選択肢も見えますね?

ですが、ご安心ください!
ここで交換できる「ゾンビの餌」とは、我ら「おしおき倶楽部」が抱える
マスコットキャラクター「オッシーちゃん」のご飯となっているのです!!

スクリーンショット 2016-12-22 4.10.29.png

あぁ、日々のストレスが消えていく…。

とても有用な徳の使い方でございますね。

さて、反対にこの危険地点をスルーするとどうなってしまうのでしょうか?

そう、待っているのはおしおきでございます。

スクリーンショット 2016-12-22 4.10.04.png

画像上でお伝えするのが難しいところもあるのですが、
実は2つのおしおきが実行されているのでございます。

まず、テキストとして表示されております通り、
あなた様のご尊父様やご尊母様に強制的にご連絡させていただきます。

スクリーンショット 2016-12-22 4.24.34.png

もちろん、年齢など関係ございません。
いい歳した大人だったとしても、
容赦なく自身の両親に叱られるという辱めを受けるのです。
あぁ、みっともない…。

そして、もう一つが、とてつもなく恥ずかしい音楽が大音量で流れるというものでございます。

発表時にデモで流した音楽は何て言いましたっけ?
…あぁ、そう「に○にぱ音頭」でしたね。
正直、発表していた私めも身の置き場所に困ってしまいましたが、
そういった所謂「イタイ系の音楽」というものが流れるのでございます。

通勤・通学で多くの人が歩く中で突如として大音量で鳴り響く、イタイ音楽。
あぁ、なんて恥ずかしい…!

そういった事態にならないためにも、オッシーちゃんを愛でるためにも!
交通ルールを守り、平穏で幸せな未来を「おしおき倶楽部」と共に迎えようじゃありませんか!!

 

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

プログラム解説

ちょっとうっかりアプリ紹介に熱が入ってしまいましたが、
アプリの中身事態はビックリするほど大したことないです。

例によって、一部だけ解説しちゃいます。

SupportMapFragment のレイアウトをカスタムする

GoogleMap の上にボタンを重ねるとかしようと思った場合、普通は
「SupportMapFragment とそのボタンのレイアウトを持ったフラグメントを重ねる」
なーんてことをすることが多いと思うんですけど、
正直、画面としては1つのものなので、1つのフラグメントにまとめたいとか思いませんか?

個人的な思想としては、別れてていいよねとは思うんですけど、
せっかくなんで試験的に1つにまとめる試みをしてみました。

その血と汗と涙と途中で意識なくなったときに垂らしたよだれの結晶が、以下になります。


public abstract class SimpleMapFragment extends Fragment
        implements OnMapReadyCallback, GoogleMapUtil.UseGoogleMapImpl {

    private boolean mStop;
    private GoogleMap mMap;
    private boolean mFirst;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mFirst = true;
    }

    @Override
    public void onStart() {
        super.onStart();

        mStop = false;

        final Fragment mapFragment = getChildFragmentManager().findFragmentById(R.id.fragment_map);

        if (mapFragment instanceof SupportMapFragment) {
            SupportMapFragment supportMapFragment = (SupportMapFragment) mapFragment;
            supportMapFragment.getMapAsync(this);
        }
    }

    @Override
    public void onStop() {
        mStop = true;

        final Fragment mapFragment = getChildFragmentManager().findFragmentById(R.id.fragment_map);

        if (mapFragment instanceof SupportMapFragment) {
            onMapDestroy();

            final SupportMapFragment supportMapFragment = (SupportMapFragment) mapFragment;
            supportMapFragment.getMapAsync(new OnMapReadyCallback() {
                @Override
                public void onMapReady(GoogleMap googleMap) {
                    // null を設定してると SupportMapFragment の方で
                    // ぬるぽになるので、空のコールバックを設定しておく
                }
            });
            mMap = null;
        }

        super.onStop();
    }

    @Override
    public final void onMapReady(GoogleMap googleMap) {
        if (mStop) {
            return;
        }

        mMap = googleMap;

        if (mFirst) {
            onMapReadyFirst();
            mFirst = false;
        }

        onMapReady();
    }

    @Override
    public GoogleMap getMap() {
        return mMap;
    }

    /**
     * onMapReady のタイミングで呼ばれる(初回のみ)
     */
    protected void onMapReadyFirst() {

    }

    /**
     * onMapReady のタイミングで呼ばれる
     */
    protected void onMapReady() {

    }

    /**
     * GoogleMap のインスタンスを破棄するタイミングで呼ばれる
     */
    protected void onMapDestroy() {

    }

}

play-services-maps のバージョンは 9.8.0 を想定しています。

SimpleMapFragment の使い方は、こんな感じです。

  1. res/values 配下に「ids.xml」を作成し、<item name="fragment_map" type="id"/>を定義
  2. SimpleMapFragment を継承したフラグメントを作成
  3. 子フラグメントのレイアウトをこんな感じ(※)で定義
  4. 定義したレイアウトをいつも通り onCreateView でインフレート
  5. あとは、マップのオブジェクト取得していじり倒したり、ビューに対してロジック書いたり

※こんな感じ


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@id/fragment_map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        class="com.google.android.gms.maps.SupportMapFragment"/>

    <Button
        android:id="@+id/button_pocket_money"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_margin="10dp"
        android:text="@string/label_button_pocket_money"
        android:background="@drawable/selector_shape_square_corner"/>

    <Button
        android:id="@+id/button_pet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_margin="10dp"
        android:text="@string/label_button_pet"
        android:background="@drawable/selector_shape_square_corner"/>

    <Button
        android:id="@+id/button_point"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_margin="10dp"
        android:text="@string/label_button_point"
        android:background="@drawable/selector_shape_square_corner"/>

</RelativeLayout>

こうすると、重ねたビューもマップ周りの処理も子フラグメントの方にまとめられました!
これはこれで使い勝手よかったので、もうちょっといろいろ改良していきたいなぁ…。

テクテクするマーカー

これ、わりとパワープレイです。(・ω<)
Handler の postDelayed メソッドを駆使して、アイコンを切り替えてました。


/**
 * ユーザーアイコン1に切り替える
 */
private void changeUser1() {
    changeIcon(mUserMarker, R.drawable.user_1,
            new Runnable() {
                @Override
                public void run() {
                    changeUser2();
                }
            }, 1000);
}

/**
 * ユーザーアイコン2に切り替える
 */
private void changeUser2() {
    changeIcon(mUserMarker, R.drawable.user_2,
            new Runnable() {
                @Override
                public void run() {
                    changeUser1();
                }
            }, 1000);
}


/**
 * アイコンの切り替え
 * 
 * @param marker 対象のマーカー
 * @param resourceId 切り替える画像の ID
 * @param nextRunnable 次に実行する処理
 * @param delayMillis ディレイ時間(ミリ秒)
 */
private void changeIcon(final Marker marker, final int resourceId, final Runnable nextRunnable,
                        final long delayMillis) {
    if (isRemoving() || isDetached()) {
        return;
    }

    if (marker == null) {
        return;
    }

    marker.setIcon(BitmapDescriptorFactory.fromResource(resourceId));

    new Handler().postDelayed(nextRunnable, delayMillis);
}

フラグメントに実装していたので、
念のため、デタッチ後に処理しないようにしたことが気をつけたことですかね?一応。

 

あとの位置情報の衝突判定とかは、去年の「しずみちひゃはーMAPⅡ」と同じだし、
このアプリとしてのオリジナルはこれくらいかも…?

あれ、思ったより解説できること少ない(;・∀・)

おしおき倶楽部のチーム構成

そんな感じで、楽しくハックしてサクッと賞をいただいてきたわけですが、
今回、チーム構成がわりと特殊(?)な感じだったので、ちょっとそれも紹介しておきます。

おしおき倶楽部チームは、総勢7名のチームだったわけですが、
そのうち、なんと4名が学生(しかも、うち3名が高校生!)のチームでした。

原案からしてすでに「おしおきアプリ」だったのに、よく一緒にやろうと思ったな…
とか密かに思ったりしたのですが、それは置いといて。

そんな学生陣はコーディング経験がまったくない!

まったくない!

状態だったわけです。

とはいえ、「じゃあ、ちょっと寝てて!」とは言えないし言いたくもなかったので、
今できないなら、今始めればいいじゃない!今でしょ!!
ということで、実際にコーディングも含めて以下のように仕事を振ってみたのです。

  • 女子高生A  → Android アプリのコーディング(オッシーくんを愛でる画面の作成)
  • 女子高生B  → 画面遷移・画面デザイン・オッシーくんデザインを考える
  • 男子高生C  → 徳の交換画面(Web、サーバー側でのポイント管理)
  • 男子大学生D → 発表資料作成、イタイ音楽探し

私は基本的にAちゃんさんにコーディング教えながら、
Bちゃんさんと画面について話し合ってたのですが…。
もうね、学生ズの活躍がハンパなかった。

まず、Aちゃんさんには軽く Android について説明した後に
「ボタンを押したら、それに対応したオッシーくんの画像に切り替わる」画面を作ってとお願いしてたんですよ。

そしたら!そしたらですよ!!

なんと、自分でいろいろ調べて画像を切り替えるだけじゃなく、
オッシーくんにアニメーションをつけてくれたんですよ!!!
初心者っすよ?今日その時はじめて Android のコーディングしたんですよ??

パネェ…。

いや、ホント大人でもアニメーション嫌いって言ってる人そこそこいるのに、
チャレンジした上にちゃんとハッキングタイム中に完成させる、その飲み込みの良さがヤバかったです。

Bちゃんなんかは、このアプリの要件伝えて
「どういう画面が必要か、どういうパーツが必要かを考えながら画面遷移図を作る」
ことをお願いしたんですけど、それがこれですよ。

スクリーンショット 2016-12-22 4.26.22.png

マジパネェ…。

しかも、必要なパーツについては既存のアプリを参考に考えたというから、
もうホント感動で震えるレベルでした。

衝撃すぎて、ちょっとガチトーンでウチに入社しないか打診した。
残念ながら、流されたけど。w

Cくんもサーバー側のエンジニアの方に教えてもらって、自分の担当をやりきっていたし、
Dくんもやたらめったら気合の入ったパワポ作ってくれたし、
ものすごくメンバーに恵まれたチームでした!

ちなみに、おしおき内容は高校生ズがされたら嫌なことを挙げてもらいました。
親へのメールもイタイ音楽再生も、案が出た時点で本当に嫌そうだったので、とても楽しかったです。w

まとめ

  • 今年も受賞したよ!本当に本当にありがとうございます!
  • 学生ズ、マジパネェ!
  • 雑な敬語を考えるのにやたらめったら時間がかかったよ!

参考

この投稿は クソアプリ Advent Calendar 201622日目の記事です。