20200908のAndroidに関する記事は9件です。

お爺さまにAndroid持たせて失敗した話

始まりはおよそ1年前

携帯(スマートフォン)を持たせたいなと思いました

理由はなんと...
事故りやがった!! ← 言い方

単独事故だったんですけどね、
それでも十分こっちには心配するお話しです...

で、その時何が困ったかっていうと、
電話がかかってきた時、向こうもテンパってたとは思うんですけど、
場所を言わない!!

「◯◯号線沿いのなんちゃら〜」

正直こちらからすると
免許取得したてな上に
元から地理には弱かったおかげでさっぱりでした。

その時は親の協力もありなんとかなったんですが、
これを期に運転の制限やスマホを持たせる事で

  • 頭を働かせる事(多少小難しくてもなんとかやっていけるだろうと思ってました)
  • 居場所を追跡できる事

この辺が改善してくれるかなと思い、
Android購入しました

で、まぁ追跡アプリはLife360を使いました
無料で、家族グループができるので!

事故を期に、運転を近所にするだとか、ドライブレコーダーをつけるだとか、安全装置のついた車に乗るとか決めておきました。

本人がまだ乗る気でいたのに驚きですが、買い物とかを考えると仕方ないのかなと思う反面、人様にやられると孫である私にまで被害が出る可能性があることを考えているのかは謎
つい最近免許更新の手前の高齢者がやる奴を受けてきた模様
点数ギリギリだったため、1,2年以内にはやめさせる方向に持っていく途中

持たせてからおよそ1ヶ月

変化はあった

まぁ、いろいろと聞かれました。
充電や、電話の仕方、Life360で他の家族の居場所をみる方法
いろいろ触る事で多少は元気でいられるのかな...なんてね

ちょっとした異変も感じてはいた

時々電話がかかってくるものの無言だったので押し間違いだろうと思いました

持たせて9ヶ月程

いつまでも電話がかかってくる

Androidってお年寄り用に大きい画面にしたり、電話やメールのショートカットがデフォルトで表示されるじゃないですか.
当然ショートカットに家族を登録してたんですが、
どうやら携帯に鍵をつけていたのがポケットで反応してたようで、
そこから電話がかかってきているのではと予想しました.

改善しよう

電話のかけ方もわかってるみたいなので、不要だろうという事で
ショートカット全部削除しましたw

ついこの間

ん...ん〜〜?????

ここまで言ってこなかったんですが、MVNO(昔の楽天モバイル)と契約していたんです。
で、手続きがネットで済むという事もあり、(親が)携帯料金を負担していたんですが、
電話料金がえげつない

とある1日の通話代がなんと...
¥35,780-

目を疑いました....
ちなみに通話時間は14時間54分
ほとんど15時間ですね

実はあった落とし穴(?)

私はiphoneだったのであまり気にしていなかったというか
感づいてたんですが、大丈夫だろうと放っておいてたんです。

Androidって電話中にホーム画面に戻れるじゃないですか?
そこから電話に戻るにはスワイプしてコントロールパネルから電話の履歴(?)を見つける必要があるんですが、
その操作を理解させるのがめちゃくちゃ難しい

という事もありほぼ半日電話がかけっぱなしになっていたようです

改善策

  • 現在のケースはストラップがつけれるようなソフトタイプのケース
    • 手帳型にする事で、間違って鍵が触れる事を防ぐ
  • Androidをやめる
    • 先ほど言った通り電話中にホーム画面戻っちゃうので、iphoneにするのか
    • あるいはガラケーに戻るのか

結果

ってことで

AndroidのSIMを解約してガラケーに移行する予定です

居場所に関してはGPSでどうにかなるのか.
あるいは他の方法を探すのか...

できればお年寄りのためのAndroidの簡単モード(?)を
操作がもっともーっっとわかりやすいのを作ってくれることを祈りますが、
おそらく次の世代のおじいちゃんおばあちゃんはスマホ扱える年代が増えてくるでしょうから、需要は少ないのかもしれませんね。

という事でAndroid持たせて失敗した話でした.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]RecyclerView のアニメーションの時間を調整する

はじめに

RecyclerView では ItemAnimator をセットすることでアニメーションを表示できるようになります。ItemAnimator は次のようなライブラリがあって好みのアニメーションに簡単に切り替えられるようになってます。

例えば ItemAnimators の ScaleUpAnimator を使ってアニメーションを実装すると次のような感じで表示されます。

    val scaleUpAnimator = ScaleUpAnimator()
    recycler_view.also { view ->
        // 省略 : Adapter や LayoutManager をセットアップする
        view.itemAnimator = scaleUpAnimator
    }

image.gif

調整方法

こういったアニメーションをもう少し早くまたは遅くしたいときがあると思います。そういったときは setAddDuration、setChangeDuration、setMoveDuration、setRemoveDuration を設定することで簡単に時間を調節できます。

例えば ScaleUpAnimator の setAddDuration と setRemoveDuration を変更してみます。すると追加と削除のときのアニメーションの表示間隔が変わります。

    val scaleUpAnimator = ScaleUpAnimator().apply {
        // 追加時のアニメーション間隔
        addDuration = 2000 
        // 削除時のアニメーション時間
        removeDuration = 4000
    }

    recycler_view.also { view ->
        // 省略 : Adapter や LayoutManager をセットアップする
        view.itemAnimator = scaleUpAnimator
    }

image2.gif

おわりに

RecyclerView でのアニメーションの時間を調整するには次の特徴を理解する必要がある。

  • RecyclerView では ItemAnimation をセットすることでアニメーションを変更できる
  • RecyclerView でアニメーションの時間を調整する場合は ItemAniamtor の setAddDuration、setChangeDuration、setMoveDuration、setRemoveDuration で変更できる。

本記事の内容の動作確認をしたプロジェクトがこちらにあります。
詳細を知りたい方は以下を参照してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

その動画、Androidプラットフォームでサポートされてる?

AndroidでVideoViewExoPlayerを使って動画再生機能を作ったはいいものの、なぜか動画がうまく再生されない。。
あのバージョンや端末だと問題なく再生できるのに、なぜかこのバージョンや端末だと再生がうまくいかない。
何故か黒みが出たり、表示が崩れたり、再生に失敗して何も表示されない。
そんなとき、ログを見て実装が怪しくないか疑うのもいいですが、もう一つ別のアプローチを紹介したいと思います。

その動画、Androidプラットフォームでサポートされてる?

Androidでは全ての動画形式がサポートされているわけではありません。
例えばmovファイルなど、再生できない動画も存在します。
公式サイトでは以下のようなガイドラインが用意されています。

サポートされているメディア形式  |  Android デベロッパー  |  Android Developers

ガイドラインを確認して、そもそもその動画形式がちゃんとサポートされているのか確認するのも大事です。
そうしないといつまでも原因がわからず詰んでしまうからです。
(遭遇した感じ、現象が発生したりしなかったりなど、かなりトリッキーでした。。)

動画形式の調べ方

動画形式を調べる方法の一つとして、FFmpegを利用するというのがあります。
(他にはMediainfoを利用するという手もあります)
導入方法はこちら

動画形式を調べるコマンドは以下になります。

ffmpeg -i [入力ファイル名]

例えば、こちらの動画形式は以下のようになっています。

~ ffmpeg -i /Users/Hitoshi/Downloads/dizzy.mp4
ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
  built with Apple clang version 11.0.3 (clang-1103.0.32.62)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.1 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-libsoxr --enable-videotoolbox --disable-libjack --disable-indev=jack
  libavutil      56. 51.100 / 56. 51.100
  libavcodec     58. 91.100 / 58. 91.100
  libavformat    58. 45.100 / 58. 45.100
  libavdevice    58. 10.100 / 58. 10.100
  libavfilter     7. 85.100 /  7. 85.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  7.100 /  5.  7.100
  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/Hitoshi/Downloads/dizzy.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2009-10-25T14:18:33.000000Z
  Duration: 00:00:25.00, start: 0.000000, bitrate: 510 kb/s
    Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default)
    Metadata:
      creation_time   : 2009-10-25T14:18:33.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.
    Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)
    Metadata:
      creation_time   : 2009-10-25T14:18:33.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.
At least one output file must be specified

ざっくり中を紐解いていくと、まず音声が、

Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default)

となっていて、形式が「AAC-LC」っぽいのでAndroidでサポートしている形式だな、というのが分かります。
また、動画の方も、

Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)

となっていて、形式が「H.264 AVC Baseline Profile(BP)」っぽいのでこれもサポートしている形式だなというのが分かります。
他にも解像度やフレームレートなどもわかるので、これらの情報を照らし合わせていきながらサポートされているかどうか判断します。

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter×Firebse】非エンジニアがパワーリフター向けアプリをリリースした話【個人開発】

はじめに

最近、パワーリフター向けBIG3記録アプリ「LifterLog」をリリースしました。

iOS版はこちら
android版はこちら

以前、Swiftで開発したアプリをFlutterで完全リニューアルしたのですが、
今回の開発→リリースまでの過程や得られた知見を共有したいと思います。

特にこれからアプリ開発やFlutterを始めようとしている人の参考になれば幸いです。
lifterlog_screenshot_アートボード 1-01.png
※因みにパワリフターとは
パワーリフティング(バーベルを持ち上げ、その重さを競うスポーツ by Wikipedia)を行う選手のこと。

自己紹介

ITベンチャーに勤務する社会人2年目の非エンジニア。
筋トレと椎名林檎/東京事変とアメフトが好き。

プログラミング経験は3年ほど。
独学を中心にSwiftやReactを触っていて、今年の5月頃からFlutterを始めました。

開発背景

完全に自分が使いたかったからです笑。
既存のトレーニング記録系のアプリに自分が必要としている機能を満たしたものがなく、「無いなら自分で作るか〜」と勢いで始めました。

当初はSwiftで開発し、昨年リリースもしたのですが

  • androidでもリリースしたい。
  • Flutterに興味があった。

以上の理由でFlutterで完全リニューアルを始めました。

自分が作りたいものを自分好みに作れることって、個人開発の素晴らしい美徳だと思います。

使用ツール・技術

  • Flutter
  • Firebase(Firestore / Firebase Authentication / Cloud Functions)
  • Git / Github(バージョン管理, issue管理)
  • Adobe XD(モック作成)
  • Adobe Photoshop(モック作成)
  • Adobe Illustrator(スクショ、アイコン作成)
  • Googleスプレッドシート(DB設計など)
  • Googleフォーム(アンケートや問い合わせフォームとして利用)
  • Slack(メモ、issue管理、アンケート確認等)

リリースまでの流れ

  1. 必要機能洗い出し
  2. UIUX設計
  3. DB設計
  4. スケジュール作成
  5. 開発
  6. リリース申請

ざっとこんな流れで仕事のない土日祝日を中心にコツコツと進めていきました。
以下、各過程でやったことを説明したいと思います。

1. 必要機能の洗い出し

まずアプリに必要となる機能を思いつく限り書き出し、その後本当に必要な機能のみをピックアップしてアプリ機能を固めました。(スプレッドシートにまとめた。)

機能をピックアップする際に意識したのは
「このアプリを一言で表すと?」
です。

今回なら、「BIG3のトレーニングボリュームが記録できるアプリ」。
こちらを念頭に機能を選定しました。

あれもこれも機能をつけようとすると結局何のためのアプリかわからなくなったり、開発がままならくなることも。。。
特に個人開発では必要最低限の機能で開発・リリースし、徐々にアップデートしていくことが大事だと思っています。

2. UIUX設計

自分がトレーニングしている状況を想定し、画面遷移等を決めAdobe XDにてモックを作成しました。
やはり、モックがあると開発がグッと楽になりますね。

3. DB設計

設計にあたっては主にこちらの記事を参考に致しました。
Cloud Firestoreの勘所 パート2 — データ設計
Firestore Database Design

必要な機能や拡張性を考慮しつつ慎重に。。。
ここが一番気を使ったところでしたが、同時に楽しいものでもありました。

4. スケジュール作成

スプレッドシートに開発する機能をまとめ、優先順位をつけざっくりとした開発日程を作成。
個人開発といえど、今回のように限られた開発時間でリリースまで最速でまでもっていくには特に必須な工程かと思っています。
地図のない旅行は無駄な回り道が増えますからね。(まぁ、往々にして予定通りにはいかなかったのですが。)

また、開発する機能ごとにGithubのissueを立て、完了次第closeするという流れで進めました。
関連する記事などを逐次コメントでき、後々振り返る時にも便利です。
また、バグ等が発生した場合も同様にissueを立て管理しています。

5. 開発

いよいよ開発スタートです。
Flutterの状態管理はprovider + ChangeNotifierを利用。
とにかくリリースすることを最優先に掲げて開発しました。

ここで大事なのは以下の3点かな、と思います。

1. なんか動くぞ!を許容する。
→最初から100%理解することは不可能だ割り切ってとりあえず動く状態を作り先に進まないと時間がいくらあっても足りません。
モチベが下がってしまう危険性もありますし、そのうち理解できる時は必ずくるのでとりあえず進むことを優先しましょう。

2. 妥協と追求の繰り返す。
→実装していく中でどうしても上手くいかない・思った通りに動かないことがでてくるかと思います。
そんな時は期限を決め、期限内に解決しなければ機能を簡潔にする・削る・代替するなどどうにかして前に進むことが大事!(もちろん、全力で調べることが前提ですが)
個人開発の良さはステークホルダーが存在せず、自分の思いのままに開発ができること。
リリースするために柔軟に対応してくことをお勧めします。

3. 公式ドキュメントを読もう。
→原理原則は全てここにある、といっても過言でないでしょう。とっつきにくいですがこちらも繰り返し読んでいればそのうちなんとなーくわかってきます笑。

Ck9_MOJWYAAR_Rm.jpg
この精神大事。

6. リリース

ストアのスクショやアイコン等はIllustratorで作成し、公式ドキュメントにそってリリース作業を行いました。
iOSリリース
androidリリース
審査に提出後、iOS・android共に約二日後に審査結果がでて、晴れてリリース!?
開発を決めてから約4ヶ月程度。
やっぱり、リリースされる瞬間は嬉しいです。?

7. 開発ではまった点

7_1. 本番/開発環境の切り替え

多分一番苦労した。。。
そもそもflavorってなんぞや?ってところからスタートし、あれこれ記事を読んで何とかできました。
特にiOS側の設定が複雑かつ、自分にほとんど知見のない領域だったのが要因。
こちらの記事を主に参考にいたしました。

Flutterで環境ごとにビルド設定を切り替える — iOS編
flutterで本番/ステージング/開発を切り替える
【Flutter】Flavorの設定~buildまで(Android編)
Creating flavors for Flutter

7_2. スクロールして画面外となったListView内のTextFieldの値がリセットされる。

トレーニングを記録する画面で入力した値が消える現状に遭遇。
最初はロジックエラーかと思いましたが、思い当たる節があり調べてみるとこちらがヒット。

[Flutter]スクロール可能なFormFieldの取り扱い

非表示にされたり、ListViewの要素が画面外にスクロールされたりした時、つまりInvisibleな状態になると、そのwidgetはdisposeされます。再び表示される際には初期化されるため、それまでの値が書き換えられてしまいます

あーあるある、こういうの。
SwiftでいうTableViewCellの再利用的な話かなぁ、と目星がついていたので検索もスムーズにできました。
このように、わからないことを他の言語に置き換えて考えてみる、というのはとても有効な考え方だなと改めて感じました。

7_4. 今後の展望

今回のリリースでは切り捨てた機能の実装、UIの見直しを直近の動きと捉えています。


今回切り捨てた機能

・スプラッシュ画面

・強制アップデート機能(Firebase Remote Config)

・ダークモード

・androidのGoogleログイン


ざっとこんなもんでしょうか。
リリースを最優先に考えた結果、実装が後回しになりましたが確実に今後必要となってくる機能ばかりなので確実に実装していきたい。


UIの見直し
ここもしっかり基礎から学んで反映させたい、切実に!
今は完全にノリと勢いに任せてます。苦笑。
学ぶにあたっては、ドキュメント等だけでなく、いろいろなアプリを触っていくことも大事にしたい。

8. 未解決問題(どなたかお力添えを。。。?)

今回、どうしても解決できなかった問題。
リリース環境のandroidでGoogleログインができない!

ログインしようとするとこんなエラーが。。。

(PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 12500: , null))

調べてみるといろいろ記事はでてきますがどれもすでに対処済み&効果なし。。。
サポートメールの設定、Firebaseにrelease用のSHA1のキーの入力、Google API ConsoleのOAuth同意画面の設定も全部やってるけど効果なし。
開発環境だと問題なく作動するのに。。。

参考記事
https://stackoverflow.com/questions/56188338/platformexception-platformexceptionsign-in-failed-com-google-android-gms-comm
https://github.com/flutter/flutter/issues/25640
https://qiita.com/hiraski/items/c5fb20da4a8862ec72ea

本当にここは妥協したくなかったのですがとりあえずメールとパスワード認証に代替して一時撤退。。。
同じような現象に遭遇して解決した方いたらどんな方法とったか教えてい頂けると嬉しいです...!

全くわからん!笑。

9. まとめ

以上、リリースまでの流れをまとめてみました。

やはり、個人開発で大切なのは

・必要最低限の機能に絞る。
・妥協と追求を繰り返しとにかく前進する。

ことかと思います。

まだまだ、できないこと・知らないことがたくさんありますが、やはり自分のアプリをリリースできるのは本当に楽しい。
これからも少しずつ問題を解消していき、学んでいきたいと思います。

にしても、Flutter、楽しいなぁ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]Retrofit + OkHttp + MoshiでGithub APIを使う

AndroidでAPIを実装する時の手法をまとめました。

ざっくり説明

  • アーキテクチャはシンプルなAndroid MVVM
  • API実装にはRetrofit OkHttp Moshiを使用
  • 言語はKotlin
  • Github Search APIでリポジトリを検索

ソースコード

こちらに置きました。
https://github.com/usk-lab/AndroidGithubApiSample

実装画面


[Screenshot_20200908-144710.png]()

単純に検索文字を打ち込み、検索結果を表示します。

API部分の実装

  • GithubInterface:APIの定義
  • GithubRepository:API処理の実行
  • GithubRetrofitProvider:Github用にRetrofitを初期化しGithubRepositoryに提供する
  • SearchResponse:レスポンスの定義

APIの定義

GithubInterface.kt
interface GithubInterface {

    @GET("/search/repositories")
    fun getSearchRepositories(@Query("q") query: String) : Call<SearchResponse>

}

検索結果の処理

MyViewModel.kt
    //検索結果
    private var _searchResult: MutableLiveData<Result<SearchResponse>> = MutableLiveData()
    val searchResult: LiveData<Result<SearchResponse>> get() = _searchResult
    ....
    //リポジトリを検索
    fun searchRepository(query: String) {
        repository.searchRepository(query).also { response ->
            if (response.isSuccessful) {
                this._searchResult.postValue(Result.success(response.body()!!))
            } else {
                this._searchResult.postValue(Result.failure(Throwable(response.errorBody()!!.toString())))
            }
        }
    }

GithubRepositoryで返却される型はResponse<***>ですが、扱いやすくResult<***>に変換しています。
また、LiveDataを使い、情報更新を通知しています。

検索結果の表示

MainActivity.kt
    private lateinit var viewModel: MyViewModel
    private lateinit var adapter: ArrayAdapter<String>

    ....

        viewModel.searchResult.observeForever { result ->
            result.onSuccess { response ->
                this.adapter.clear()
                this.adapter.addAll(response.items.map { it.fullName })
                this.adapter.notifyDataSetChanged()
            }
            result.onFailure {
                Timber.e(it.toString())
            }
        }

検索結果をobserveForeverで受け取っています。
onSuccessの場合は、adapterを更新します。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter/コード自動生成】FlutterGenを使ってみる。

こんにちは。最近はニホンザルにハマっているminako-phです。
※おすすめの猿山があったら教えてください

昨日、@wasabeef_jp さんが このようなTweetをされてらっしゃいました。

Flutter向けに画像リソースなどのコードを自動生成するためのツールです。

ちょうど iOS開発の時に使っていた R.swift みたいなツール無いかなーと探していたタイミングだったので、
おおお.....!!これは!!? となり早速試してみました。

何ができるのか

  • 自動生成により画像などのリソースファイルへアクセスできるようになる
  • 実装する時にコード補完が効くのでタイポしなくなる
  • 静的解析ができるので存在しないファイルの指定などによる実行時のエラーを防ぐ

この記事の概要

FlutterGenについては作者のわさびーふさんが日本語で詳しく書いてくださっているので
この記事ではシンプルにFlutterGenを導入するとこから、
コード自動生成によるコード補完を実際に体験するまでを試してみました。

FlutterGenのインストール

今回は Homebrew でインストールしました。

$ brew install FlutterGen/tap/fluttergen

インストールが終わったら使用できる事を確認してみましょう

$ fluttergen -h

今回はHomebrewを使用しましたが 他にも、
Dart Command-linebuild_runner が用意されています✔️

実際に使ってみる

コードの自動生成を実行する

今回は3種類の画像とフォントを用意しました。それぞれを assets/ 配下に配置します。

スクリーンショット_2020-09-08_1_29_10.png
pubspec.yaml に追加できたら、次にFlutterGenによるコードの自動生成を実行します。
パスはプロジェクトの pubspec.yaml へのパスを指定します。

$ fluttergen -c ./pubspec.yaml

すると一瞬でコードの生成が完了しました!
スクリーンショット_2020-09-08_0_59_35.png

実際にエディタで使用してみる

assets/images/ 配下の画像を指定したいので、Assets.images. と入力してみます。

すると以下のように、コード補完機能が使用できるようになっている事が確認できます?!
Sep-08-2020 01-51-32.gif

これでタイポを気にせずスピーディーに実装できるようになりました!
とっても快適 ....... ><✨

公式でも解説されていますが、対象ファイルがFlutterで対応してる画像フォーマットの場合、
Assets.images.warauEbifriedAssetImage classとして使え、実際に描画する際は
Assets.images.warauEbifried.image(...) とする事で Image class として
Assets.images.warauEbifried.pathString として使えます。

  • 普通に書くと ↓
// 文字列指定する為 タイポしやすい...
Image.asset(
  'assets/images/warau_ebifried.png',
  width: 150.0,
),
  • FlutterGenを使用して書くと ↓
// Image classとして使用する. Image classが持つパラメーターを使用できます
Assets.images.warauEbifried.image(
  width: 150.0,
),

// Stringとして使用する
Image.asset(
  Assets.images.warauEbifried.path,
  width: 150.0,
),

今回のようにディレクトリ名が assets/ もしくは asset/ の場合は冗長となる為 省略されますが、
Assets.directoryName.directoryName.fileName のように階層をそのまま使用できるようになっています。

フォントでも使ってみましょう。
以下のようにStringとしてコード補完ができることが確認できます?!
Sep-08-2020 02-14-42.gif
フォントの場合 FontFamily.notoSansJP のように指定が出来ます。

感想

ファイル名やディレクトリを追ってコピペして貼り付けて や 定数宣言して... みたいな
地味にコストのかかる作業が解消されとても快適になりました ><✨ 有り難いです ..

他にも出来ることなど詳しくは公式も是非ご覧ください!

参考

コード自動生成の FlutterGen を作りました。
Github|FlutterGen/flutter_gen
pub.dev|flutter_gen

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

localhostにスマホから繋げてみた!!(Mac編)

どうも、三町哲平です。

ローカル環境で開発している時に開発したアプリをブラウザで確認したい。
そういう時にlocalhostにアクセスするじゃないですか!

その時、自分のスマホで見るとどういう風なデザインになるのだろうという疑問が生まれますよね。
でも、パソコンから開いたlocalhostに別の機器でアクセスってどうやるんでしょう?
難しく思う方もいるかも知れませんが、実はすごい簡単です!

なので今回は、MacBookで起動したlocalhostにAndroidスマホとiPad、WindowsPCの3点からアクセスしてみます。

それでは、順を追っていきましょう!!

1. Wi-Fi環境を同じにする

まず最初にMacBook、Androidスマホ、iPad、WindowsPCを同じWi-Fi環境に接続してください。

今回は、Air-WiFi_0OXPYNに接続しています。

2. MacBookのIPアドレスを確認する。

MacBookの システム環境設定=>ネットワーク を開きます。

Air-WiFi_0OXPYN1に接続されていることを確認したら、
赤線部分のIPアドレスをメモやコピーしておきます。

今回の場合は、
192.168.43.160です。

3. localhostを起動する

今回は、ターミナルで、

$ docker-compose up

を使用して、docker-composeで、localhostを起動します。
ポート番号は、3000番です。
スクリーンショット 2020-09-07 2.42.24.png

4. MacBookでlocalhost:3000にアクセス

スクリーンショット 2020-09-07 2.47.54.png

無事、ページが表示されました。

5. その他の機器で試す

※MacBookからは今回の場合、localhost:3000とブラウザのURLに入力することで、表示されますが、その他機器では、192.168.43.160:3000と入力します。

Androidスマホ

iPad

WindowsPC

キャプチャ.PNG

このように、同じWi-Fiに接続することにより、Macで起動したloclhostにAndroidスマホとiPad、WindowsPCからもアクセスすることが出来ました。

参考記事:localhostをスマホからアクセスする方法(同じWiFi環境の場合)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Flutter]AnimatedBuilderで実用的なアニメーション

記念すべき初投稿。

おなじみの枕詞になるが、Flutterは公式のドキュメントが大変充実している。
したがって、我ながら「この記事いる?」という感じではあるが、学習発表会の感覚で書き残しておく。

概要

Flutterアプリケーション上でアニメーションを書く方法は色々あるが、本記事ではAnimatedBuilderを使った最も単純なアニメーションレシピをまとめる。
本質的な理解よりも「早速Flutterアニメーションを体験できる」ことに重きをおく。

Flutter初学者の方は、あらかじめ公式の「Write your first Flutter app, part 1」あたりは(できればpart2も)修了しておくことをおすすめする。

「実用的」とは言いつつも、サンプルはContainerのwidthが広がったりもとに戻ったりするだけのシンプルなもの。
ある程度複雑なアニメーションも、基本的にはこうしたシンプルなアニメーションの組み合わせで実現できる。

animateボタン押下でContainerのwidthが変化
sample
完成サンプル+αはコチラ

対象読者

  • Flutter初学者(?)
  • StatelessWidgetとStatefulWidgetの違いが分かっている

AnimatiedBuilderとは?

アニメーションを実装する際に繰り返し記述することになるお約束コードを端折るために用意された、StatefulWidgetのラッパー。シンプルな単一アニメーションから複数のシーケンシャルなアニメーションにまで幅広く使える実用性の高いアニメーション系Widgetである。
Flutterでアニメーションを描く際に最も使用頻度が高い、という方も少なくないのでは無いだろうか。しらんけど。

ざっくりレシピ

  1. TickerProviderMixinを組み込んだStatefulWidgetを用意する。
  2. 1.で用意したStatefulWidgetにAnimationControllerのインスタンスを持たせる。
  3. 2.で用意したAnimationControllerとTweenを用いてAnimationのインスタンスを生成する。
  4. WidgetツリーにAnimatedBuilderを組み込む。その際、3.で用意したAnimationインスタンスを渡す。
  5. 任意のタイミングでAnimationControllerを操作する。

ざっくり用語解説

TickerProviderMixin

AnimationControllerにvsync(≒画面のリフレッシュイベント)を提供するためのMixinオブジェクト。
実装上はほとんどお作法的に出てくるだけなので、ここでは本質的なことは扱わない。

Mixin

クラスの多重継承を避けつつ任意のクラスに汎用的な拡張を組み込むための仕組み。
ここでは詳しく扱わない。

AnimationController

アニメーションの開始、停止、リセットなどの操作や、アニメーション状態の管理、参照を媒介するオブジェクト。
「value」というプロパティを持っており、アニメーションの始点を0.0、終点を1.0とした場合の現在値を参照できる。

Tween

Animationインスタンスを生成するためのオブジェクト。
ちなみに、Tweenはin-betweeningの略とのこと。

Animation

実際にWidgetに動きを与えるための実数値を生成、返却するオブジェクト。
FlutterのAnimationクラスには、始点〜終点の値を指定したDurationとCurveに従ってなめらかに遷移するTween animationと、実際の物理現象をモデリングしたPhysics-based animationの二種類が存在するが、実際に使うことになるのはほぼほぼTweenの方になると思われる。

AnimationControllerも「value」というプロパティを持っているが(上記参照)、Animationインスタンスのvalueは、Tweenに設定した始点と終点の間で現在値を参照できる、というもの。

解説

レシピの内容を順を追って詳しく見ていこう。

1. TickerProviderMixinを組み込んだStatefulWidgetを用意する

AnimationControllerはStatefulWidgetのState内で宣言しライフサイクルを管理してやる必要があるので、そのためのStatefulWidgetを用意する。

width.dart
class WidthAnimationPage extends StatefulWidget {
  WidthAnimationPage({Key key}) : super(key: key);

  static const kRouteName = '/width';

  @override
  _WidthAnimationPageState createState() => _WidthAnimationPageState();
}

class _WidthAnimationPageState extends State<WidthAnimationPage> {

  AnimationController _controller;  // AnimationControllerを宣言

}

Controllerにvsyncを提供するため、StatefulWidgetにTickerProviderStateMixinを適用する。
単一のAnimationを組み込む場合はSingleTickerProviderStateMixin、複数のAnimationを組み込む場合はTickerProviderStateMixinを使用する。

width.dart
class _WidthAnimationPageState extends State<WidthAnimationPage>
    with SingleTickerProviderStateMixin {

2. StatefulWidgetにAnimationControllerのインスタンスを持たせる

initState()でControllerの生成、dispose()でControllerの破棄を行う。
AnimationControllerによるアニメーションのスタート・ストップ等の操作も、このStatefulWidgetで行うことになる。

width.dart
class _WidthAnimationPageState extends State<WidthAnimationPage>
    with SingleTickerProviderStateMixin {

  AnimationController _controller;  // AnimationControllerを宣言

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 500),  // アニメーションにかける時間の指定
      vsync: this,  // [SingleTickerProviderStateMixin]を組み込んだStatefulWidget自身を指定。
    );
  }

  @override
  void dispose() {
    _controller.dispose();  // Controllerの破棄を忘れずに
    super.dispose();
  }
}

AnimationControllerの生成・破棄まででStatefulWidget全体は以下のような感じになる。(一部省略)

width.dart
class WidthAnimationPage extends StatefulWidget {
  ...
  @override
  _WidthAnimationPageState createState() => _WidthAnimationPageState();
}

class _WidthAnimationPageState extends State<WidthAnimationPage>
    with SingleTickerProviderStateMixin {  // StateにTicerの組み込み

  // AnimationControllerの宣言
  AnimationController _controller;

  @override
  void initState() {
    super.initState();

    // AnimationControllerのインスタンス生成
    _controller = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
  }

  @override
  void dispose() {
    // AnimationControllerの破棄
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
    );
  }
}

3. AnimationControllerとTweenを用いてAnimationインスタンスを生成する

AnimationControllerの下準備が終わったら、実際に時間ごとに変化する値を返すAnimationオブジェクトを生成する。
アニメーションパーツは再利用性などを鑑みて予め別Widgetに切り出しておくといいだろう。
イニシャライザでAnimationインスタンスを生成しておけばスッキリ書ける。
今回はContainerのwidthに設定するdouble値を100⇔300で行ったり来たりさせたいので、以下のようにTweenのbeginには100、endには300を設定しておく。

width.dart
class WidthAnimation extends StatelessWidget {
  WidthAnimation({
    Key key,
    this.controller,
  }) : _width = Tween<double>(begin: 100.0, end: 300.0).animate(controller),
       super(key: key);

  final AnimationController controller;
  final Animation<double> _width;

  ...
}

Tweenのanimate()メソッドに任意のAnimationControllerを渡しておくと、戻り値としてAnimationが返される。
そして、そのアニメーションの開始、終了、停止等は引数に渡したController経由で操作できるというスンポーである。

Animationインスタンス生成のタイミングだが、今回のように単一のアニメーションパーツを単一のAnimationControllerで操作するだけのシンプルな構成ならば、予め親のStatefulWidgetで生成して渡すだけでもいい。

width.dart
class WidthAnimation extends StatelessWidget {
  WidthAnimation({
    Key key,
    this.width, // 今回のケースでは生成済みAnimationインスタンスをもらうのでも良い
  }) : super(key: key);

  final Animation<double> width;

  ...
}

実運用上は、Animationの仕様をどのレイヤーで確定させたいかによって使い分けるといいだろう。

4. WidgetツリーにAnimatedBuilderを組み込む

アニメーションパーツとして切り出したWidthAnimationにAnimatedBuilderを組み込む。

本来、アニメーションを含む全ての画面更新にはStatefulWidgetのSetState()ないしはそれに相当するトリガーが必要だが、そのあたりのことはAnimatedBuilderがよしなにやってくれるので、気にしなくて良い。そしてそれがまさにFlutterアニメーションにAnimatedBuilderを利用する主要な利点の一つである。

直接アニメーションするWidgetの生成関数をbuilderに渡し、その子Widget(アニメーションしない)をchildに渡す。
WidthAnimationの全体は以下のようになる。

width.dart
class WidthAnimation extends StatelessWidget {
  WidthAnimation({
    Key key,
    this.controller,
  }) : _width = Tween<double>(begin: 100.0, end: 300.0).animate(controller),
       super(key: key);

  final AnimationController controller;
  final Animation<double> _width;

  Widget _animationBuilder(BuildContext context, Widget child) {
    return Container(
      width: _width.value,  // valueが変化する度にbuilderが実行され、アニメーションが実現する
      height: 100.0,
      alignment: Alignment.center,
      color: Colors.red,
      child: child, // 静的な子Widget(Text)はBuilderの親Widget(WidthAnimation)でキャッシュする
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _animationBuilder,
      animation: controller,  // AnimationControllerでもAnimationでもよい
      child: Text('width',
        style: Theme.of(context).primaryTextTheme.headline6,
      ),
    );
  }
}

Flutterにおけるbuilder型Widgetの定形的な書き方で、簡単に言えば「動的なWidgetにぶら下がる静的な子WidgetをBuilderの親Widgetでキャッシュする」というテクニックなのだが、具体的にどのような効果があるのかついての説明を始めてしまうと壮大な脱線になってしまうので本記事では扱わない。気が向いたらそのうち別記事で扱いたい。

AnimatedBuilderのanimationにはAnimationControllerもしくはAnimationのインスタンスを渡す。
どちらを渡すべきかの判断基準だが、AnimationControllerが複数のAnimationをハンドリングする場合にはAnimatedBuilderにはAnimationを直接渡す方が間違いが少ないはず。それ以外ならばどちらでも良いだろう。
サンプルではAnimationControllerとAnimatedBuilderが1対1のシンプルな構成なのでどちらを渡しても問題ない。

これでWidthAnimationの準備ができたので、先に用意したWidthAnimationPageに組み込んでやればよい。

width.dart
class _WidthAnimationPageState extends State<WidthAnimationPage>
    with SingleTickerProviderStateMixin {

  AnimationController _controller;

  ... // 略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        ... // 略
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          SizedBox(height: 100.0), // topマージン用SizedBox()
          WidthAnimation(controller: _controller), // アニメーションパーツ
        ],
      ),
    );
  }
}

5. 任意のタイミングでAnimationControllerを操作する

今回はボタンを押す度にWidthAnimationのwidthが100⇔300を行ったり来たりする仕様なので、そのためのボタンを用意する。
シンプルにこんな感じでいいだろう。

widh.dart
CupertinoButton(
  onPressed: () {
    if (_controller.status == AnimationStatus.dismissed) {
      _controller.forward(); // width: 100 -> 300
    } else if (_controller.status == AnimationStatus.completed) {
      _controller.reverse(); // width: 300 -> 100
    }
  },
  color: Colors.green,
  child: Text('animate',
    style: Theme.of(context).primaryTextTheme.headline6,
  ),
),

こいつをWidthAnimationPageに組み込んでやる。

widh.dart
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Row(
            children: <Widget>[
              Spacer(),
              CupertinoButton(
                onPressed: () {
                  if (_controller.status == AnimationStatus.dismissed) {
                    _controller.forward();
                  } else if (_controller.status == AnimationStatus.completed) {
                    _controller.reverse();
                  }
                },
                color: Colors.green,
                child: Text('animate',
                  style: Theme.of(context).primaryTextTheme.headline6,
                ),
              ),
              Spacer(),
            ],
          ),
          SizedBox(height: 100.0),
          WidthAnimation(controller: _controller),
        ],
      ),

出来上がり。(見本&サンプル

おわりに

できるだけ、「今すぐわからなくてもできる」というような部分には言及しないように努めたつもりだが、それなりに散らかってしまっている気がしてならない。初投稿なので大目に見てほしい。というか、ご質問やご指摘を頂けたら嬉しいです。
継続的に記事を投稿しながら本記事もブラッシュアップしていく所存。

そのうち、「AnimatedBuilderとは何か」という根本の部分をより深く掘り下げた記事なども書いてみたい。

以上。

参考文献:

Flutter公式『Animations tutorial

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleMapの下からニュッと出るやつを実装してみた

はじめに

参考になったのはこちらの2記事でした。

実装したコードと説明

全量は こちら

ポイント

Google のロゴをスライドに合わせて移動させてます。
(GoogleMap風に..)
xmlではanchorも指定していますが、
anchorは若干マージンの幅がずれたりするので、
onSlideで計算した方が良い感じになります。

計算が手動になりますが、滑らかに動く分計算して実装できるといい感じです。

閉じているとき

広げたとき

MapsActivity.kt

        // CoordinatorLayout の 直下に配置している View を渡す
        bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheetLayout))
        bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
                override fun onSlide(bottomSheet: View, slideOffset: Float) {

                    Log.d("tag", "slideOffset=$slideOffset")

                    // 異常値を弾く
                    if (slideOffset > 1 || slideOffset < -1) {
                        return
                    }

                    val minHeight = 56 // peek_height
                    val slide = 300 - minHeight // 最大の高さから最低の高さを引いたのが移動幅
                    val defaultBottomMargin = 8 // 最低bottomマージン
                    val bottomMargin = (slide * slideOffset + minHeight + defaultBottomMargin).toInt()
                    mMap.setPadding(0 ,0, 0, dp2px(bottomMargin))
                }

                override fun onStateChanged(bottomSheet: View, newState: Int) {

                }
            })
        bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED

activity_maps.xml

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="72dp"
        android:layout_marginEnd="16dp"
        android:layout_gravity="top|end"
        android:orientation="vertical"
        app:layout_anchorGravity="top|end"
        app:layout_anchor="@id/bottomSheetLayout">

        <Button
            android:id="@+id/buttonMyHouse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自宅" />

        <Button
            android:id="@+id/buttonMyOffice"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="勤務先" />

    </LinearLayout>

最後に

iOSとandroidでの違いとか部品名が意外とわからない、あとGoogleのアプリ使ってるとできること(できそうなこと)、Googleが提供している部品かどうかがわかるのでやっぱり使い倒すの大事だなと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む