MotionLayoutでFAB Speed dialを実現する
こんにちはtkyです。
前回 、MotionLayoutでアニメーションさせる(Android) - ticktakclockの日記にてMotionLayoutについて学習しました。
今回はこのMotionLayoutを利用してFABメニューにおけるスピードダイアル(Speed dial)を実現してみたいと思います。
これですね。
実際のコードは前回のGitHubと同じです。 Sample9
をご確認ください。
レイアウト
まずはlayout.xmlにパーツを定義していきます。ツリー的にはこのようにになります。
1つViewを設置(@+id/viewのこと)していますが、これは背景をつけるために配置しています。Gif上で赤色の背景にあたる部分です。
layout/motion_09.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:showPaths="true" android:id="@+id/motionLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_09"> <View android:id="@+id/view" android:layout_width="wrap_content" android:layout_height="wrap_content" tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteY="0dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@android:drawable/ic_menu_add" app:fabCustomSize="48dp" tools:layout_editor_absoluteX="339dp" tools:layout_editor_absoluteY="659dp" /> <TextView android:id="@+id/sub_text1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="camera" tools:layout_editor_absoluteX="200dp" tools:layout_editor_absoluteY="553dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#3F51B5" android:clickable="true" android:src="@android:drawable/ic_menu_camera" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="332dp" tools:layout_editor_absoluteY="560dp" /> <TextView android:id="@+id/sub_text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="call phone" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="714dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#03A9F4" android:clickable="true" android:src="@android:drawable/ic_menu_call" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="363dp" tools:layout_editor_absoluteY="723dp" /> <TextView android:id="@+id/sub_text3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="search" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#FF9800" android:clickable="true" android:src="@android:drawable/ic_menu_search" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> <TextView android:id="@+id/sub_text4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="share social" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#4CAF50" android:clickable="true" android:src="@android:drawable/ic_menu_share" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> <TextView android:id="@+id/sub_text5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="setting" tools:layout_editor_absoluteX="297dp" tools:layout_editor_absoluteY="698dp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/sub_btn5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="#9C27B0" android:clickable="true" android:src="@android:drawable/ic_menu_manage" app:fabCustomSize="32dp" tools:layout_editor_absoluteX="368dp" tools:layout_editor_absoluteY="691dp" /> </androidx.constraintlayout.motion.widget.MotionLayout>
量は多いですが、1つ1つはただのButtonとTextViewです。パーツを定義したので xml/scene_09.xml
に開始/終了の成約を設定してどのようにアニメーションするかを定義していきましょう。
トリガー、開始成約、終了成約の順にポイントを解説していきます。
今回すべて1つのxml内に記述していますが、開始と終了成約については別xmlで定義したほうが可読性が上がるので本番投入する際は分けると良いのではないかと思われます。
トリガー
やりたいことは「FABボタンが押された時にメニューを開く/閉じる」です。 Transitionにはfabがクリックされた時に成約をトグルできるようにOnClickを配置します。
<Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end" motion:duration="200"> <OnClick motion:targetId="@id/fab" motion:clickAction="toggle" /> </Transition>
開始
@startにはメニューが閉じている状態の成約を指定していきます。
- 背景の@viewは特にすることはないので透過させておきます
- メニューの各ボタンも表示する必要はないので
android:visibility="invisible"
を指定して非表示状態とします - アニメーション自体は上下のみで、メニューのFAB(@sub_btn1~@sub_btn5)の下の成約を
motion:layout_constraintBottom_toBottomOf="parent"
で親につけておきます
<ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/view"> <Layout android:layout_width="0dp" android:layout_height="0dp" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="#00FFFFFF" /> </Constraint> <Constraint android:id="@id/fab"> <Layout android:layout_width="48dp" android:layout_height="48dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintBottom_toBottomOf="parent" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="0" /> </Constraint> <Constraint android:id="@id/sub_btn1" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/fab" motion:layout_constraintEnd_toEndOf="@id/fab" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text1" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="@id/sub_btn1" motion:layout_constraintTop_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_btn2" android:visibility="invisible"> <Layout android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn1" motion:layout_constraintEnd_toEndOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text2" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="@id/sub_btn2" motion:layout_constraintTop_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_btn3" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn2" motion:layout_constraintEnd_toEndOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text3" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="@id/sub_btn3" motion:layout_constraintTop_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_btn4" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn3" motion:layout_constraintEnd_toEndOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text4" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="@id/sub_btn4" motion:layout_constraintTop_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_btn5" android:visibility="invisible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn4" motion:layout_constraintEnd_toEndOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="parent" /> </Constraint> <Constraint android:id="@id/sub_text5" android:visibility="invisible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn5" motion:layout_constraintBottom_toBottomOf="@id/sub_btn5" motion:layout_constraintTop_toTopOf="@id/sub_btn5" /> </Constraint> </ConstraintSet>
終了
@endにはメニューが開いている状態の成約を指定していきます。
- 背景はViewのBackgroundColor属性を変更するためにCustomAttributeで色を指定します
- メニューのボタンたちは今度は表示する必要があるので
android:visibility="visible"
を指定して表示状態とします - メニューの各FABは例えば@sub_btn5(一番上のFAB)では
motion:layout_constraintBottom_toTopOf="@id/sub_btn4"
というように1つ下のFABの上に配置するように指定します
<ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/view"> <Layout android:layout_width="0dp" android:layout_height="0dp" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="#5CEC9D9D" /> </Constraint> <Constraint android:id="@id/fab"> <Layout android:layout_width="48dp" android:layout_height="48dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintBottom_toBottomOf="parent" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="45" /> </Constraint> <Constraint android:id="@id/sub_btn1" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/fab" motion:layout_constraintEnd_toEndOf="@id/fab" motion:layout_constraintBottom_toTopOf="@id/fab" /> </Constraint> <Constraint android:id="@id/sub_text1" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn1" motion:layout_constraintBottom_toBottomOf="@id/sub_btn1" motion:layout_constraintTop_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_btn2" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn1" motion:layout_constraintEnd_toEndOf="@id/sub_btn1" motion:layout_constraintBottom_toTopOf="@id/sub_btn1" /> </Constraint> <Constraint android:id="@id/sub_text2" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn2" motion:layout_constraintBottom_toBottomOf="@id/sub_btn2" motion:layout_constraintTop_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_btn3" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn2" motion:layout_constraintEnd_toEndOf="@id/sub_btn2" motion:layout_constraintBottom_toTopOf="@id/sub_btn2" /> </Constraint> <Constraint android:id="@id/sub_text3" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn3" motion:layout_constraintBottom_toBottomOf="@id/sub_btn3" motion:layout_constraintTop_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_btn4" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn3" motion:layout_constraintEnd_toEndOf="@id/sub_btn3" motion:layout_constraintBottom_toTopOf="@id/sub_btn3" /> </Constraint> <Constraint android:id="@id/sub_text4" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn4" motion:layout_constraintBottom_toBottomOf="@id/sub_btn4" motion:layout_constraintTop_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_btn5" android:visibility="visible"> <Layout android:layout_width="32dp" android:layout_height="32dp" android:layout_marginBottom="8dp" motion:layout_constraintStart_toStartOf="@id/sub_btn4" motion:layout_constraintEnd_toEndOf="@id/sub_btn4" motion:layout_constraintBottom_toTopOf="@id/sub_btn4" /> </Constraint> <Constraint android:id="@id/sub_text5" android:visibility="visible"> <Layout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" motion:layout_constraintEnd_toStartOf="@id/sub_btn5" motion:layout_constraintBottom_toBottomOf="@id/sub_btn5" motion:layout_constraintTop_toTopOf="@id/sub_btn5" /> </Constraint> </ConstraintSet>
全体
長いのでここを見てください。
https://github.com/ticktakclock/MotionLayoutSamaple/blob/master/app/src/main/res/xml/scene_09.xml
まとめ
MotionLayoutを使ってxmlのみでアニメーションを実現しました。
MotionLayoutのポイントはボタンクリック時のアニメーションも含めてxmlで定義して実現できるのが良いかな、と思います。
このようにすることでボタンクリック時に本当にやりたい処理のみktファイルに記述することができて全体的に見通しの良いコードになりそうです。
次はCoordinatorLayoutと組み合わせてMotionLayoutアニメーションを実装してみようと思います。
次回
参考:ありがとうございます!
GitHub Actionsのワークフロー個人的逆引き
こんにちはtkyです。
GitHub Actionsを使い始めて1ヶ月位が経過したのでやったことなどを忘れないようにまとめていこうと思います。
思いつき次第更新してこうと思いますが、大体ドキュメントに書いてあるので基本ドキュメント参考にするのが良いのかと思われます。
とりあえずパッと試してみたい人へ
GitHubのactionタブからポチポチするだけでとりあえずなにかは作れるので雰囲気を理解したい場合一度ブラウザ上で動かしてみると良いです。
逆引き辞典
できる限りコピペで使えるようにしています。
作業ディレクトを指定したい
cd foo && ( sample.sh args1)
みたいなことやりたくないですよね。
. ├── .github │ └── workflows │ └── pr-check.yml └── foo └── sample.sh
steps: - name: exec sample.sh working-directory: ./foo run: sample.sh args1
ワークフローを実行するコンテナを指定したい
通常ubuntu-latest上で動きますが、コンテナを指定したいときがある・・・?はず・・・?
jobs: samplejob: name: sample job runs-on: ubuntu-latest container: image: node:10.16.0-alpine # Nodeコンテナ上でsteps実行する steps: - name: Checkout uses: actions/checkout@v1 - run: npm install
PRのIssueにコメントする
github apiを使います。tokenは secrets.GITHUB_TOKEN
に格納されているので便利です。
if: failure()
で直前のstepが失敗していたとき(=exit status 0以外)に実行という条件分岐が可能になります。
APIたたいたあとに&& exit 1
で失敗に落として次のsuccess()に進まないようにしますが、もっといいやり方があれば教えて下さい。。。
テストにコケたときにIssueにコメントする
steps: - name: Checkout uses: actions/checkout@v1 - name: uint test run: .exec_test.sh # 各プロジェクトのテストを実行してください - name: post result if test failure if: failure() run: | curl \ -X POST \ ${{ github.event.pull_request.comments_url }} \ -H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ -H 'content-type: application/json' \ -d "{ \"body\": \"👮<200d>♂️UnitTestでエラーが発生しています🚧内容を確認してください👮<200d>♂️\" }" \ && exit 1 - name: post result if test success if: success() run: | curl \ -X POST \ ${{ github.event.pull_request.comments_url }} \ -H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ -H 'content-type: application/json' \ -d "{ \"body\": \"おめでとうございます🎉UnitTestは無事通過しました🥳\" }" \
こんな感じで出てきます
どんな環境変数が使えるのか
ワークフロー上の${{ secrets.github_token }}
(小文字でもいけます)などどんなものが使えるのか
ざっくり3つくらいリストアップ。
変数 | 説明 |
---|---|
${{ secrets.github_token }} | トークン |
${{ secrets.github_sha }} | コミットハッシュ |
${{ secrets. github_repository }} | オーナーとリポジトリ(ower/repo) |
${{ github.event.payload
}} にも色々と情報が入っています。GitHubAPIのEvent typeと同じものが入るようです。
PRの時に入るイベントは以下を見ると良いです。
PRにコメントするときは ${{ github.event.pull_request.comments_url }}
にPOSTするとコメントできます。
カスタムアクションが必要になるケース
自分のリポジトリだけで使うプライベートなアクションの作成が必要になるケースは、自分で使ってみて以下のような感じでした。
- 特殊なDocker環境を使っている
- shellをぶん回しながら色々と細かいことをやりたい
- yamlのRUNでかけなくもないけどごちゃるので・・・
DBも絡めたテストがしたい
大体のケースで当てはまるであろうケース。以下のように書くと localhost:port
でアクセスできるようになる
samplejob: name: sample job runs-on: ubuntu-latest services: mysql: image: mysql:5.7 # dockerhubのイメージを指定する env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: testdb MYSQL_USER: docker MYSQL_PASSWORD: docker TZ: Asia/Tokyo steps: - name: Checkout uses: actions/checkout@v1 # you can access to localhost:3306
以下のように実行コンテナを指定するとdocker-composeと同じような状況下で動作させることができます。
この時コンテナ間アクセスでは mysql:3066
のようにサービス名でアクセスできるようになります。
samplejob: name: sample job runs-on: ubuntu-latest container: image: php:7.2-fpm # 実行コンテナを指定する services: mysql: image: mysql:5.7 # dockerhubのイメージを指定する env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: testdb MYSQL_USER: docker MYSQL_PASSWORD: docker steps: - name: Checkout uses: actions/checkout@v1 # you can access to mysql:3306
その他わかったこと
躓いたところなど記載していきます。
github actionはpublicのみ対応だった
どういうことかというと
カスタムアクションで ticktakclock/actions-test@v1
のようなリポジトリをPrivateで作ると、他のリポジトリでこのアクションにアクセスできません(当たり前ですよね)
会社でGithubを使っていて、会社用のアクションを共通で利用できるようにリポジトリを作ってもPrivateなのでアクセスできない事象になるので困る・・・
同一Organization内のリポジトリはアクセスできるようにしてほしい・・・と思いながらも今はリポジトリごとに個別でコピペして入れてます。
ワークフローはgithubにデプロイしてみるしか確認できない
どうしようもない気がしますがなにか手がないか・・・ローカルで確認した後にgit push したいのですが。。。
僕はテスト用にリポジトリ作ってそっちにPushしまくって確認した後 コミットをきれいにしてから開発リポジトリに導入しました。
※Slack等にイベント通知しているので最初は開発リポジトリにpushしまくってタイムラインが一瞬荒れました・・・w
MotionLayoutでアニメーションさせる(Android)
こんにちはtkyです。
今日はかねてから気になっていたAndroidでにおけるconstraintlayout2.0のβ3版で追加されているMotionLayoutを使ってアニメーションを試してみたいと思います。
まだベータ版(2019/12/06執筆現在)であり、2.0が正式になった時にどうなるかは不明ですので予めご了承ください。
(2020/10/24 追記)
constraintlayout2.0のstableが出ているのでプロジェクト内のライブラリバージョンを更新しました。
一応すべてのサンプルはちゃんと動いているようでした。
(2020/10/24 追記)
今回試したリポジトリはこちらになります。
MotionLayout is 何
まずは公式ドキュメントを見に行きましょう。
冒頭部分を日訳でピックアップすると
- MotionLayoutはConstraintLayoutのサブクラスでライブラリバージョン2.0から追加されています。
MotionScenes
内に定義したconstraint(以降、制約)間の遷移をサポートします。- API level 18 (JellyBean MR2)以降で利用可能です。
- MotionLayoutはMotionSceneファイルが必要で、 MotionScene内トップレベルに
LayoutDescription
を含みます- StateSet (Optional) ・・・システムの状態(selected, focused, enabled, 等)
- ConstraintSet ・・・制約群
- Transition ・・・制約か状態間の遷移
※StateSetについてはよくわからず、詳しく知っている方がいれば教えていただきたいです。
android.util.StateSet
のことであっているんでしょうか・・・
また2019/12/06現在はベータ3となっており、以下のようにapp.gradleに追記することで利用可能となります。
dependencies { // 中略 implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3' // 中略 }
どうやって学習するか
公式にサンプルがあるのでこれで使い方を学習するのが一番手っ取り早いです。 私のリポジトリもこのサンプルを写経しながら色々と試していきました。
ざっくり解説
以下のような簡単なアニメーションの実現を考えます。
MotionLayoutを使ったアニメーションはやることを列挙したらきりこんな感じかと思いますが、2〜4は 1つのxml内に定義することになります。
- アニメーションさせたいレイアウトを
MotionLayout
タグで囲う - 遷移の仕方(Transition)を決める
- 遷移前の制約(ConstraintSet)を決める
- 遷移後の制約(ConstraintSet)を決める
アニメーションさせたいレイアウトを MotionLayout
タグで囲う
アニメーションさせたい場合、
これを
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/button" android:background="@color/colorAccent" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
こうする。
tools:layout_editor_absoluteXとtools:layout_editor_absoluteYは記述しても無視されます。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/motionLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_01" tools:showPaths="true" > <View android:id="@+id/button" android:layout_width="128dp" android:layout_height="128dp" android:background="@color/colorAccent" android:text="Button" tools:layout_editor_absoluteX="267dp" tools:layout_editor_absoluteY="333dp" /> </androidx.constraintlayout.motion.widget.MotionLayout>
前述の通りMotionLayoutはConstraintLayoutのサブクラスなのでConstraintLayoutを囲う必要はありません。
android:id="@+id/button"
また、今まで app:layout_constraintXXXX_toYYYYOf="parent"
のようにlayout側で定義していた制約は不要となります。
app:layoutDescription="@xml/scene_01"
layout側で定義するidはMotionSenceに定義するトリガーや制約で使用する為layout側に定義する必要があります。
このscene_01.xmlに遷移の仕方と制約を定義していきます。先に示しておきます。
<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end" motion:duration="1000"> <OnSwipe motion:touchAnchorId="@id/button" motion:touchAnchorSide="right" motion:dragDirection="dragRight" /> </Transition> <ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginEnd="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> </Constraint> </ConstraintSet> </MotionScene>
遷移の仕方を決める
上記のscene_01.xmlでやっていることは、
- 特定のIDに対して
- スワイプ/クリックした時の
- 開始ConstraintSetと終了 ConstrainntSetを定義した
<Transition>
タグをMotionSence内に設置する
です。遷移の仕方の部分は以下のところになります。
<Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end" motion:duration="1000"> <OnSwipe motion:touchAnchorId="@id/button" motion:touchAnchorSide="right" motion:dragDirection="dragRight" /> </Transition>
遷移前/遷移後の制約(ConstraintSet)を決める
以下の部分ですね。書きっぷりはおおよそConstraintLayoutと一緒です。
これと
<ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> </Constraint> </ConstraintSet>
これです
<ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginEnd="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> </Constraint> </ConstraintSet>
scene.xmlが長くなる問題
上記のようにConstraintSetを書いていると必然的にsence.xmlが長くなって見通しが悪くなりますが、xmlを分けて次のようにも記載することができます。
制約部分はいつもどおり、layout/xmlに記載してしまって motion:constraintSetStart="@layout/motion_01_start"
のように参照するだけにすると、役割も明確になって良いかもしれません。
layout/scene_01_start.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/button" android:background="@color/colorAccent" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
layout/scene_01_end.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/button" android:background="@color/colorAccent" android:layout_width="64dp" android:layout_height="64dp" android:layout_marginEnd="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto"> <Transition motion:constraintSetStart="@layout/motion_01_start" motion:constraintSetEnd="@layout/motion_01_end" motion:duration="1000"> <OnSwipe motion:touchAnchorId="@+id/button" motion:touchAnchorSide="right" motion:dragDirection="dragRight" /> </Transition> </MotionScene>
CustomAttributeについて
CostomAttributeは例えば backgroundColor
や ratation
といった viewの セッターを呼んだりできます。
ドキュメントに書いてありますが motion:attributeName="BackgroundColor"
とした場合 setBackgroundColor
を呼ぶことになるので、パスカルケースで記述するところがポイントですね。
また、customAttributeValueは int/float/boolean/stringが指定できて、色は"@color/colorAccent"のようにリソースIDで定義する感じです。
以下のように複数指定することで『左から右に赤から緑に変化しながら90度回転する』みたいなアニメーションが実現できることになります。
<ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginStart="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="@color/colorAccent" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="0" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/button"> <Layout android:layout_width="64dp" android:layout_height="64dp" android:layout_marginEnd="8dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintEnd_toEndOf="parent" /> <CustomAttribute motion:attributeName="BackgroundColor" motion:customColorValue="@color/colorPrimary" /> <CustomAttribute motion:attributeName="Rotation" motion:customFloatValue="90" /> </Constraint> </ConstraintSet>
KeyFrameSetについて
今まで指定してきたアニメーションは開始地点と終了地点のみ定義してきました。
A ----------------------> B
KeyFrameSetを使用すると、このAとBの間に中継地点Pを設けをその間(A->P, P->B)の移動方法、を定義できるようになります。
A ---------->P-----------> B
コードで説明すると
KeyPosition以外にはKeyAttribute、KeyCycle、KeyTimeCycle、KeyTriggerを指定することができますが、 まだKeyPositionしか理解していないのでKeyPositionで説明したいと思います。
この場合、『アニメーションのちょうど真ん中に来た時にY軸に25%移動(マイナスがついているので上に移動します)』となります。
<Transition motion:constraintSetStart="@id/start" motion:constraintSetEnd="@id/end" motion:duration="1000"> <OnSwipe motion:touchAnchorId="@id/button" motion:touchAnchorSide="right" motion:dragDirection="dragRight" /> <KeyFrameSet> <KeyPosition motion:keyPositionType="pathRelative" motion:percentY="-0.25" motion:framePosition="50" motion:motionTarget="@id/button"/> </KeyFrameSet> </Transition>
先程のCustomAttributeと組み合わせるとこのようになります。
デバッグについて
xmlでMotionLayoutのattributeに tools:showPaths="true"
を追加すると AndroidStudioのデザインビューで --- でアニメーションが描く線が表示されます。
しかし実機やエミュレータでは線は描かれません。
コード上で motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH)
とすると出てくるようになります。
val motionLayout = findViewById<MotionLayout>(R.id.motionLayout) motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH)
まとめ
アニメーション自体は昔から存在する仕組みですが、MotionLayoutを使うことでコードを書かないで、xmlで挙動を定義できるようになりました。
これによりViewのレイアウトと業務ロジックを分離して管理できるので見通しが良くなりそうです。
次はもう少し実践向きなレイアウトとアニメーションを試してみたいと思います。
次回Part1
次回Part2
参考記事
理解の助けになりました。ありがとうございます。
Developers.IO 2019に行ってきました
こんにちは、tkyです。
本日はClassmethodさんのDevelopers.IO 2019に参加してきたレポートブログとなります。 とは言え本家ブログ側に各レポートブログがあるのでこちらではサマリと感想などを綴ることにします。
Developers.IOについてはこちら dev.classmethod.jp
各セッションのレポートはこちら dev.classmethod.jp
ハッシュタグは #cmdevio
のようでした。
twitter.com
ブルボン総選挙もやってました。
濃密なスケジュールの中見てきたセッションはこちらになります。
時間 | Room | タイトル | 発表者 |
---|---|---|---|
10:30~11:15 | Room2 | 認証の標準的な方法は分かった。では認可はどう管理するんだい? | 都元ダイスケ 氏 |
10:30~11:15 | Room H | claspではじめるサーバーレス開発 Google Apps Scriptで簡単自動化 | 武田隆志 氏 |
12:24~13:30 | 昼食休憩してました | ||
13:45~14:30 | Room3 | サーバーレスの基本とCI/CD構築 & 運用 〜システムは動いてからが本番だ〜 | 藤井元貴 氏 |
14:45~15:30 | Room3 | エンジニアも知っておくと得する、デザイン組織とデザイナーマネジメント | ベイジ 枌谷力 氏 |
15:45~15:30 | Room3 | サービスを爆速で立ち上げるためのSaaSの活用 | 諏訪悠紀 氏 |
16:45~15:30 | Room5 | 障害に備えたアーキテクチャを考える「あのとき何が起こった!?」 | 坂巻一義 氏 / 吉井亮 氏 |
17:45~15:30 | Room5 | Amazon ECSを活用したAWS運用自動化サービスの裏側を包み隠さず解説 | 伊藤祥 氏 |
18:45~ | Room11 | 懇親会 |
認証の標準的な方法は分かった。では認可はどう管理するんだい?
公式レポート
感想
通常APIを設計するときにほぼ必ず考慮するであろうアクセス制御、 例えば「一般ユーザ」「管理者ユーザ」で取得できるデータの内容が異なったり、 更新できるデータの範囲が異なったりしますが、実際そのような要件に対して設計アプローチしていくのか、という話となります。
ISO 10181-3で Access Control Framework
という名称で標準化されている模様で、この考え方に基づきながら解説されていて非常にすんなりと理解できた気がします。
理解しやすくするために以下のワードが出てきました。
- 操作する人・・・ユーザ
- 操作されるもの・・・・リソース
- 何かの操作・・・アクション
そして上記の『「一般ユーザ」「管理者ユーザ」で取得できるデータの内容が異なったり』これがまさにアンチパターンとなりうる状況だったようです。 前提としてユースケースが異なるのでそもそも同じアクションで管理しては ~ダメ~ (複雑になるので推奨しない)そうです。 別々のAPIで設計することで、余計な条件分岐もなくなりテストしやすくなりそうです。 確かに、不具合が発生したときにその状況を再現しづらくなったり、原因特定に時間がかかるう要因になりかねないですし、なるほどと思いました。私の中ではここが一番収穫でした。
P34に書いてありますが、重要です。 - ユースケースとアクションをちゃんと定義して - 誰が何にアクセスできるのかきっちり決める
アクションがわかったところで、何を以てそのAPIを叩いてOK/NGなのか、 ですが、いろいろあるんですよね。
- ユーザが持つ権限(どのAPIと叩いて良いのか)
- オーナー権限
- 役職、リーダー
アクセス権は ユーザ
自身に紐づくものと その役職(Role)
に紐づくものがあって、これまでの話はユーザ側のものでした。
ここでユーザには関係ない役職という軸も出てきます。もうそろそろ脳のキャパシティを超えてきました。
で、このあたりをじっくり観察するとAWSのIAMによるアクセス制御がまさにこのことだそうで、きれいに締めくくられ 後味の良いスッキリとした気持ちでセッションが終了しました。
claspではじめるサーバーレス開発 Google Apps Scriptで簡単自動化
公式レポート
感想
自社でGASおじさんと化しているtkyですが、clasp自体知らなかったため、知識を得るために聴講。 GASができることと、デメリットを解消するためのclaspについて、 クラスメソッド内のGASの活用事例などが聴けました。 ※claspについては既知の人だと「聞いたことある内容だ」という感じの初級者向けセッションとなりそうです。
GAS、Google Chrome上で開発できす素早さとスプレッドシートとの連携が強力で便利なのですが、 - JS 1.6である - コードの差分管理ができない - コードフォーマッタなど活用できない
と言ったデメリットが挙げられる中でclaspというCLIツール(Nodeモジュール)の存在を知って心のなかで大きく盛り上がりました。 TSでかけるのが大きいかもしれませんね。ローカル開発となるのでgitも使えるので楽だと思いました。
メリットしか無いのでは?とも思いますが、セッションでもありました通り - 手軽さが減る - 非開発者には敷居が上がる
などもありますし、エクセルの標準関数でできることも多いので、目的に合わせてGASやclaspを利用していくのが良いなと改めて感じました。
活用事例としては以下のようなケースでGASで定常業務にかかる負担を減らしているようです。なるほど!! 1. スプレッドシートをJSONにしてS3にアップロード 2. GAとMackerel連携。リアルタイムユーザを定期的に送信して可視化する 3. Backlogチケットをスプレッドシート連携(定例時に転機するのに活用) 4. Gsuiteのグループ作成(メーリングリスト)Admin SDK Directory APIを叩いてグループ一括作成 5. Gsuiteのグループ棚卸し、グループ情報をスプレッドシートに出力
クラスメソッド内でのGASの活用事例もしれてとても良いセッションでした。
サーバーレスの基本とCI/CD構築 & 運用 〜システムは動いてからが本番だ〜
公式レポート
感想
入場ギリギリになってしまい立ち見となりました・・・ CI/CDで考慮すべき項目やAWS構成について理解できるセッションとなっていました。
CI/CDにおいて重要なのはセッションにもありましたが以下ですね。 - いつでも動く安心感を手に入れる - デグレに気づける - テストコードでは見つけられない不具合の回避 - オペレーションミスによるデプロイミスの回避
人的ミスをそもそもなくす、仮に発生しても水際回避したり、早期発見できるようになるのがCI/CDの強みだと思います。
テストコードでは見つけられない不具合
についてはピンと来なかったのですが、
AWSサービスを使うにあたり以下のような事象がそれに該当するようですね。なるほど。
- CloudFunctionのスペルミス
- IAMの権限不備
- DBの権限不足
デプロイ時に気をつけるべきポイントとしてのIAMアクセスキーの流出についても興味深かったです。 実際にやってみないとこういった知見にたどり着けないと思うのですごいなぁと思います。(AssumeRoleという機能を考えているAWSもすごい・・・)
最後に監視の話、何を、どのくらい検知したら、いつ、誰にどんな内容を通知するのか、 これをきっちり考えないと、精神的に摩耗してしまいますし、オオカミ少年になってしまいますね。
サービスもリリースして終わりではなく、リリースしてもやることは多くいかに素早く改善していくか、 やはりリリースしてからがやはり本番だ、というタイトル通りのセッションだったと思います。
エンジニアも知っておくと得する、デザイン組織とデザイナーマネジメント
クラスメソッドさんと一緒にお仕事をしている会社さんのセッションでした。 baigie.me
公式レポート
レポートブログはないので資料のみです。
https://baigie.me/download/pdf/devio_191101.pdf
感想
本セッションではデザイナーという職種の細かいジャンルとデザイナーのタイプの紹介がメインとなります。
セッション時、「デザイナー職の人と一緒に仕事をしている/したことがあるか」という質問が上がり 結構な数の手が上がっていました。やはりデザイナーと関わっているエンジニアが日々増えているのだろうと、感じています。 そんな中、エンジニア+デザイナーを含む組織をマネジメントする必要も出てくるのは、そのとおりですね・・・
デザイナータイプについてはエンジニアにも通ずるというか全ジャンルに通ずるところがあると思いました。 そしてお互いが相容れない存在で「混ぜるな危険」の意味も理解できましたw
- 挑戦的⇔保守的
- 感覚的⇔論理的
上記の軸からみる分布 - 理想実現型(先駆者) - 成果追求型(データドリブン) - 共同作業型(調和) - 実務遂行型(ドキュメント命)
MBTI診断を大きく4分割すると上記のタイプに分かれそうだな、という印象を受けました。
皆さんはどんなタイプでしたか?
サービスを爆速で立ち上げるためのSaaSの活用
公式レポート
感想
Auth0、stripe、 CircleCI を使ってサービスを立ち上げた話となります。
それぞれ、認証、決済、CI/CDを司り、これらを使って約2ヶ月でDevelopers.IO Dafeができたとのことで驚きのスピード感(と技術力!!)
stripeについてはテスト用のクレカを利用できたり、いざという時の返金も行えるようになっているということで、まじで便利じゃん(語彙力)と思いました。
本セッションでも伝えていたことですが『プロダクトにとって最も大切なもの(コア機能)はなにか』が重要だという印象を受けました。 実際認証の仕組みやDBなど自前で用意しようとすると車輪の再発明にもなりますし、運用コストがかかってきます。 本来のサービス、プロダクトのあり方を考えて適材適所でSaaSを利用していく判断も必要になってきますね。
最後に、エンジニアさんは実際にDevelopers.IO Cafe内で開発しているらしく、超羨ましい・・・と思った次第です。
障害に備えたアーキテクチャを考える「あのとき何が起こった!?」
公式レポート
感想
2019年8月23日 東京リージョン障害を通じて、どのような対策を取ると良いのか、 実際に稼働率を極限まで高めようとした際の構成及びコスト試算などを共有するセッションで、とても興味深かったです。
障害発生時の実際の調査内容についてもセッション内で共有がありました、こちらも合わせて見るとより流れが把握できそうです。
障害発生時、どのようなシステム構成を取ると良いのかについては Well-Archicted Framework 信頼性
が参考になるそうです。
システム設計運用の大局的な考え方や、設計原則のベストプラクティス、システムの最適化度合いを評価する視点をもてるようになるための資料で、
ちょうど11/1当日?くらいに日本語翻訳版が出たようです。
ドキュメントがいたれりつくせりですごいっすね、AWS・・・
正直このあたりから自分の知識量を超えていて理解が追いつきませんでした。
で、その上で稼働率を極限まで高めるために、99.0%、99.9%、99.99%のようにしていくとどういった構成になるのかをまとめていました。 ここからわかることは、落ちないような構成を取るためには相応のコストが掛かるということ、 また構成だけでは限界があるので、落ちたあとにいかに早く復旧できるかが稼働率を高めるポイントにもなるというところが印象深かったです。
Amazon ECSを活用したAWS運用自動化サービスの裏側を包み隠さず解説
公式レポート
感想
AWS運用自動化サービス「opswitch」のシステム構成や開発環境などの紹介セッションとなります。 こんなところまで見せてしまって良いんですか、、、というくらい濃い内容でした。 運用の自動化に対する課題の解説と、運用を自動化するサービスの説明に分かれています。
昼一のセッションにもありましたが、リリースして終了ではなく、リリースしてからが本番であり、運用は決して逃れることのできない必要作業になりますが、 やはり自動化して極力ヒューマンエラーを避けていきたいところ。
そこでopswitchという運用自動化サービスが登場します。 EC2/RDSインスタンスの起動・停止、バックアップなどのタスクを組み合わせてジョブ実行できる機能などが魅力的です。
内部のアーキテクチャについては、大きく3システムで構成されていて各システムで扱っている技術が異なっているところが面白いなと思います。 また、コア部分のシステムについてはそれぞれ必要なリソース(CPU,メモリ)や役割などが異なるため関心事別にクラスターを分けて構成しているとのことで勉強になります。
かなりディープな話だけあって知識的に置いてけぼりをくらいながらも知らない領域を知る機会を得られてとても良かったです。
懇親会
最後は再演セッションがいくつかあるなか、並行で懇親会がスタートしました。 食事が美味しい・・・ 登壇されていた方をうまく見つけることができずでしたが、数名の方とコミュニケーションを取ることができました。
おわりに
最近AWS等インフラの構築/設計に関わる機会が増えており、自分の知らなかった情報を手に入れることのできた 貴重な体験でした。
Koinを使って依存解決(DI)する
こんにちは、tkyです。
今回は
今までDI(Dependency Injection)ライブラリはDagger2だけしか使ったことがなかったのですが、他のDIライブラリも使ってみたくて Koin
を使ってみました。
公式見ながら作業しましたが、想像以上に簡単にDIできたのでびっくり。。。
以下のような2つの数字を足し算するだけのアプリでお試し実装してみました。
作成したものはGitHubに置いてありますのでご確認ください。
アプリ自体はMVVMを採用し、DataBindingを活用していますが、Koinに特化して記載するため、Bindingについては触れません。 github.com
環境情報
- Android Studio 3.5
- Kotlin 1.3.50
- Koin 2.0.1
Koin
Kotlin用の軽量な依存注入フレームワーク(日本語訳)です。2019/09/23時点の最新は v2.0.1
となっています。
使い方
Koin自体は超らくちんで、App
にKoin使う宣言して、モジュールの宣言をするだけです。
app.gradle
implementation 'org.koin:koin-android:2.0.1' implementation 'org.koin:koin-android-scope:2.0.1' implementation 'org.koin:koin-android-viewmodel:2.0.1'
App.kt
import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin class App : Application() { override fun onCreate() { super.onCreate() // ↓これだけ!DSLでかけるのがいいですね! startKoin { androidContext(this@App) modules(myModule) } } }
modules()
に依存を解決したいモジュールの宣言をしていきます。
今回は KoinInjector.kt
ファイルを作って、その中に変数を定義して見ました。
Calculator
、 CalculateService
、 MainViewModel
については後述します。
import org.koin.android.viewmodel.dsl.viewModel import org.koin.dsl.module // ちゃんと定数とわかるような命名規則のほうが良いです。 val myModule = module { single { Calculator() } single { CalculateService(get()) } viewModel { MainViewModel(get()) } // ViewModel用のモジュール宣言 }
後出しになりますが、サンプルアプリはクラス間で以下のような依存関係にあります。
Calculator
クラスは他のクラスに依存しない計算ロジックのクラスです。
myModule
には single { Calculator() }
のようにして依存解決します。
class Calculator { fun sum(a: Int, b: Int): Int = a + b }
CalcurateService
クラスは計算に関する処理をまとめるためのサービスクラスのような扱いで一枚かませました。DIしたかったし😅
実際、 CalcurateService
はRepository(DataSource)層、 Calculator
は Dao層のような関係で理解すると良いかもしれません。
Calculator
に依存していますが、Calculator
のインスタンスはコンストラクタから注入します。
myModule
には single { CalculateService(get()) }
のようにして依存解決します。
class CalculateService(private val calculator: Calculator) { fun sum(a: Int, b: Int): Int = calculator.sum(a, b) }
MainViewModel
は CalculateService
に依存しています。
これもコンストラクタからインスタンスを注入します。
myModule
には viewModel { MainViewModel(get()) }
のようにして依存解決します。
import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel class MainViewModel(private val calculateService: CalculateService) : ViewModel() { private var _result: MutableLiveData<String> = MutableLiveData() val result: LiveData<String> get() = _result fun calculate(a: String?, b: String?) { val numA = if (a.isNullOrEmpty()) 0 else a.toInt() val numB = if (b.isNullOrEmpty()) 0 else b.toInt() val result = calculateService.sum(numA, numB).toString() _result.value = result } }
MainActivity
は MainViewModel
に依存しています。
MainActivity
はAndroidクラスにつき、コンストラクタから注入できません。
その代わりKoinが遅延初期化の仕組みを用意してくれています。
import android.os.Bundle import androidx.appcompat.app.AppCompatActivity // import org.koin.android.ext.android.get import org.koin.android.ext.android.inject class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 関数内でもこのように依存注入できます。 // val viewModel: MainViewModel = get() } }
これで依存解決されたMainViewModelのインスタンスが出来上がりです!
まとめ
コードを貼り付けただけでしたが、やることは
- App.ktに以下を書く
startKoin {
androidContext(this@App)
modules(myModule)
}
- モジュール(myModule)の依存を解決する
val myModule = module { single { Calculator() } single { CalculateService(get()) } viewModel { MainViewModel(get()) } }
- ActivityでDIする
class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by inject() override fun onCreate(savedInstanceState: Bundle?) { ・・・略・・・ } }
たった3つ!って思うとやってみたくなりません?
公式にもGet Startedがあるので見てみてください。
WebRTCとWebSocketで画面共有(ビデオチャット)アプリを作る
こんにちはtkyです。
表題の通り、WebRTCとWebSocketの勉強を兼ねて、webアプリを作ってみました。
作り方を記載しているわけではなく、成果物に対しての説明となるブログですので予めご了承願います。
作ったもの
画面共有(ビデオチャット)アプリを作成しました。 音声に対応させていないので画面共有としています。理由は後述。
- 画面を複数人で共有し
- 参加者のアクションを画面上で共有できる(いいね機能と呼称しています)
※ローカル環境+Chromeでしか動作(確認)しませんのであしからず!
コードレビューや、プレゼン発表などでみんなのリアクションが励みになるような思いを込めて作ってみました!
作ったものについてはgithubにアップしています。
技術要素
技術及び環境情報です。
環境
- Macbook Pro (v10.14.6, 13inch 2.7GHz Intel Core i7 16GB)
- Google Chrome (v76.0.3809.100)
- Visual Studio Code (v1.37.1)
技術
- React (v16.9.0)
- Material UI (v4.4.0)
- Node (v10.14.1)
- WebSocket (socket.io v2.2.0)
- WebRTC
構成
サーバ側とクライアント側(Reactアプリのこと)に分かれます。 全体は以下のような構成になります。
WebRTCでP2P通信する情報をやりとりするためにnode.jsで情報交換用のサーバ(シグナリングサーバというようです)を構築します。
シーケンス
WebRTCで通信するためにシグナリングサーバとどのような情報を交換する必要があるのかシーケンスにしてみました。 流れとしては - 通信を開始したい側がofferを出す - offerを受け取った側がAnswerを出す - この時点でWebRTCが経路情報を算出する(Ice Candidate) - 経路情報が取得できたら自分の経路情報を出す(クライアント毎に自身の経路情報を交換する) - 経路が確立できたらP2P通信が始まる
サーバ側
上記のシーケンスを守れるようにサーバ側は以下を実装します。
サーバの実装は /server/app.js
です。サーバ側は以上です。
- socketを部屋に紐付ける
- メッセージを部屋に紐づくsoket全員に配信する
// offerとanswerは SDP(SessionDescriptionProtocol)というプロトコルとして処理します socket.on('SEND_SDP', function(data) { data.sdp.id = socket.id; if (data.target) { socket.to(data.target).emit('RECEIVE_SDP', data.sdp); } else { socket.broadcast.to(socket.roomname).emit('RECEIVE_SDP', data.sdp); } }); // Ice Candidateを配信する socket.on('SEND_CANDIDATE', function(data) { if (data.target) { data.ice.id = socket.id; socket.to(data.target).emit('RECEIVE_CANDIDATE', data.ice); } else { console.log('candidate need target id'); } });
上記のようなコードをやりとりしたいメッセージごとに以下の事に注意しながら定義するだけです。
- 部屋関係なく自分も含め全員 (今回使わない)
- 部屋関係なく自分以外全員 (今回使わない)
- 部屋に属する自分も含め全員 (いいねリアクション(機能説明にて後述)のやり取り)
- 部屋に属する自分以外全員(オファー要求)
- 特定の人(オファー自体)
クライアント側
クライアント側は結構手間で、Offerの送受信、Answerの送受信、Ice Candidateの送受信があります。
加えて、P2PができたときのStream(動画情報です)の取り扱いなどやることが結構多くて、
実際に作った MultVideoChat.js
が結構肥大化してしまいました。もう少しうまく分離できれば良かったのですが・・・
reactプロジェクト自体は create-react-app
で作成しました。
あとは自分用のprettierでフォーマッティングしています。
使い方
サーバ起動とクライアント起動は別ターミナル上で行ってください。
$npm i $node server/app.js // サーバ起動 $npm start // クライアント起動
画面構成
Material-UI: A popular React UI framework を採用しました。自分自身がAndroiderであるのと極力自分でレイアウトするコストを払わないようにするためです。 Material UIを使うこと自体もある程度慣れる必要がありますが、ここは頑張りポイントです。
大きく2つの画面で構成されています。App.jsにて、部屋情報のstateにより表示を切り替えています。
エントランス画面
部屋名を入力して入室する画面です。赤枠の領域が src/Entrance.js
の実装範囲になります。
ビデオチャット画面
src/MultiVideoChat.js
に大多数の実装が入っています。 src/Room.js
でラップしています。
メニュー
src/CustomDrawer.js
で自分の映像情報に対する操作UIを設置しています。
FABでも良かったのですが、操作の関心事がそれぞれ異なり、FABではうまく表現しきれないと判断してドロワーメニューにしました。
機能
- チャットルーム機能
WebSocket接続後、サーバに対して SEND_ENTER
のsocket通信を行います。
socket.join(roomname)
で指定した名前の部屋に入ることを実現しています。
その部屋に新しく入ったsocket idに対してofferを出すための呼び出しメッセージを通知します。
RECEIVE_CALL
を受け取ったsocketは指定socket idにOffer要求をしていきます。
// クライアント側 this.socket.on('RECEIVE_CONNECTED', data => { // socket情報をstateに持っておく this.setState({ socketId: data.id }); // 部屋に入る this.socket.emit('SEND_ENTER', this.state.room); }); // サーバ側 socket.on('SEND_ENTER', function(roomname) { socket.join(roomname); console.log('id=' + socket.id + ' enter room:' + roomname); socket.roomname = roomname; socket.broadcast .to(socket.roomname) .emit('RECEIVE_CALL', { id: socket.id }); });
- 複数人会話機能
上記の部屋を作ることで複数人が同じ部屋に属することができます。 また、先に記載したシーケンスを実装することで複数人のP2P通信を実現します。
- 全画面表示機能
メニューでFull Screenを押下した時、その接続streamを全画面で表示します。
Material UIのDialogを使います。fullscreen
プロパティをつかいました。
自分のstream情報を全画面用のvideoタグに設定する
<Dialog maxWidth="xl" fullScreen open={this.state.fullScreenId !== ''} onClose={this.onFullScreen} > <Like socket={this.socket}> <video style={fullScreenStyle} autoPlay="1" playsInline ref={video => { const fullScreen = this.videos[this.state.fullScreenId] || this.video; if (video && fullScreen) { video.srcObject = fullScreen.srcObject; } }} /> </Like> <IconButton style={closeButtonStyle} color="primary" onClick={this.onFullScreen} > <CloseIcon /> </IconButton> </Dialog>
- いいね機能
全画面表示中、画面をクリックすると、「良いね!!」を表すハートマークを表示する、盛り上がり機能をつけました。 実際これがやりたいがためにビデオチャットアプリを作ったと言っても過言ではありません。 盛り上がりが共有できて、みんなが幸せになれば良いなーと思います。
シーケンスの実装
クライアント側では先に示したシーケンスを実装することになります。 送信・受信はWebSocketの話になります。
- Offerの送信
- Offerの受信〜Answerの送信
- Answerの受信
- Ice Candidateイベント〜Ice Candidate送信
- Ice Candidateの受信
- streamイベント
ポイントのみ、抽出して記載します。
Offerの送信
async makeOffer(id) { // 接続先ごとにpeer(=RTCPeerConnection)を確立していきます。 const peer = this.prepareNewConnection(id); const stream = this.video.srcObject; stream.getTracks().forEach(track => { // peerに送信したいstream情報を設定します。 peer.addTrack(track, stream) }); const offer = await peer.createOffer(); await peer.setLocalDescription(offer); // websocket offer送信 this.sendSdp(id, peer.localDescription); }
Offerの受信〜Answerの送信
async onOffer(sdp) { // 相手側のsocket idでRTCPeerConnectionを確立する const peer = this.prepareNewConnection(sdp.id); const stream = this.video.srcObject; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); // 受け取ったofferデータをRTCPeerConnectionクラスにセットします。 const offer = new RTCSessionDescription(sdp); await peer.setRemoteDescription(offer); // 受け取ったOfferに対しAnswerを返答します。 this.makeAnswer(sdp.id); } async makeAnswer(id) { // 接続先のsocket id ごとにpeerをstateで管理しています。 const peer = this.state.peers[id]; const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); this.sendSdp(id, peer.localDescription); }
Answerの送信
async onAnswer(sdp) { // 接続先のsocket id ごとにpeerをstateで管理しています。 const peer = this.state.peers[sdp.id]; if (!peer) return; // 受け取ったofferデータを相手socketのRTCPeerConnectionクラスにセットします。 const answer = new RTCSessionDescription(sdp); await peer.setRemoteDescription(answer); }
Ice Candidateイベント〜Ice Candidateの送信
prepareNewConnection(id) { const config = { iceServers: [] }; const peer = new RTCPeerConnection(config); // 中略 peer.onicecandidate = event => { this.onIceCandidate(id, event.candidate); }; // 中略 return peer; } onIceCandidate(id, icecandidate) { if (icecandidate) { // Trickle ICE this.sendIceCandidate(id, icecandidate); } else { // Vanilla ICE console.log('empty ice event'); } }
Ice Candidateには2種類あります。どのタイミングでIceを送るかの違いです。
- Vanilla ICE
- すべてのイベント受信後、Ice Candidateを送信する
- Trickle ICE
- イベントを受け取り次第逐次Ice Candidateを送信する
- 逐次接続情報を送ることでVanillaよりも早く接続確立できる事がある
Ice Candidateの受信
onReceiveCandidate(ice) { const peer = this.state.peers[ice.id]; if (!peer) return; // 受け取ったiceを相手socketのRTCPeerConnectionクラスにセットします。 const candidate = new RTCIceCandidate(ice); peer.addIceCandidate(candidate); }
streamイベント
接続が確立できると、streamが降ってきます。streamイベントが来ていたらP2Pの接続自体は成功しています。
prepareNewConnection(id) { const config = { iceServers: [] }; const peer = new RTCPeerConnection(config); // バージョンによりpeer.onAddStream()らしいです。最新のchromeではpeer.ontrak()を使います。 peer.ontrack = event => { this.onAddStream(id, event.streams[0]); }; // 中略 return peer; }
本アプリではwebカメラと画面共有の2パターンの共有方法を持っていますが、リソースを切り替えるたびに通信を確立するようにしています。 streamを切り替えようとすると、peerの接続が切れてしまい、うまく映像の共有ができなかったためなのですが、やり方が悪いだけかもしれません。
音声ミュートについて
前提として、音声が流れている時、自身のブラウザにも音声が流れてしまいます。 上記が理由で音声配信は可能でしたが諦めた背景があります。 動画ミュートも音声ミュートも以下のようにしてミュートできますが、 自分のブラウザから出る自分の声をミュートしようとすると相手側も結局ミュートされてしまい、意味がなくなってしまいます・・・
const videoTrack = this.video.srcObject.getVideoTracks()[0]; // audioの場合getAudioTracks() videoTrack.enabled = !videoTrack.enabled;
考えうる対策としては以下が検討できましたが、どんどん仕様が膨らんでいくので今回は一旦ここまでとしました。
- 配信Streamと表示Streamを分ける
- 配信Stream(映像・音声)、表示Stream(映像)にすれば切り替えられそう
- 自分のStreamは表示しない
- 元も子もないですね・・・
終わりに
- 初めてReact Hooks関連の実装をしてみて、useState()の使い方が少し理解できた
- WebRTCの一通りの実装することでビデオチャットの基礎が理解できた
- 今回はlocalhost上でwebsocketのシグナリングサーバを実装したが、firebaseを使っても良いかもしれないと感じた
- websocket.ioを使ったが比較的容易に実装できた(気がする)のでまた挑戦してみたい
以上です。
Fuchsiaをビルドしてエミュレータ起動する
こんにちは、tkyです。
Fuchsiaのビルドをしてみたいと思います。
以下のサイトを見ながらやったこととなります。 結構時間かかります。3時間くらい・・・?
実施端末情報
macOS Mojave (10.14.5) MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports) プロセッサ 2.7 GHz Intel Core i7 メモリ16 GB 2133 MHz LPDDR3
Fuchsiaをビルドするソースコード郡を落としてきます。
$ curl -s "https://fuchsia.googlesource.com/fuchsia/+/master/scripts/bootstrap?format=TEXT" | base64 --decode | bash
20分くらいかかりました・・・ このあと pathを追加するように促されるのでされるがままに追加します。
{username}
はログイン名が入ります。
export PATH=/Users/{username}/fuchsia/.jiri_root/bin:$PATH
この状態で jiri
, fx
, cipd
コマンドが使えるようになります。
Jiriは開発ツールのようです。Fuchsiaのコードをチェックアウトしたりビルドしたりするためのコマンドがいろいろと用意されている模様です。
ビルドについては以下の通り。
$ xcode-select --install $ fx set core.x64 --with //bundles:kitchen_sink $ fx build
2時間強かかりました・・・
ビルドできたのでいざ起動!!!
$ fx run ・・・省略・・・ [00008.390] 01281:01283> devcoordinator: fallback driver 'usb_composite' is avai [00008.390] 01281:01283> devcoordinator: driver 'usb_composite' added [00008.390] 01281:01283> devcoordinator: fallback driver 'intel_disp' is availab [00008.390] 01281:01283> devcoordinator: driver 'intel_disp' added [00008.391] 02907:02930> devhost[proxy] bind driver '/system/driver/virtual_audi [00008.408] 02907:02930> devhost[proxy] bind driver '/system/driver/virtual_came [00009.599] 06866:06887> [00009.595941][1186734803][0][netstack] INFO: netstack. [00009.623] 06866:06887> [00009.621660][1186734803][0][netstack] WARNING: main.g [00013.611] 05656:05671> [ERROR:garnet/bin/sysmgr/app.cc(183)] Could not load pa [00014.980] 05656:05671> [ERROR:garnet/bin/sysmgr/app.cc(190)] Singleton fuchsia [00017.406] 06866:06887> [00017.405761][1186734803][0][netstack] WARNING: netsta [00018.418] 11088:11101> wlanstack [I]: Starting
動いたっぽい!!?やった! 🎉ここからどうしたら良いんだ・・・・
ls
は反応しませんでしたが、 pwd
は反応した。Linuxコマンドは少しだけ準備があるみたいです。
ls /boot/bin/sh: 1: Cannot create child process: -1 (ZX_ERR_INTERNAL): failed to re $ ls /boot/bin/sh: 2: Cannot create child process: -1 (ZX_ERR_INTERNAL): failed to re $ pwd / $
ドキュメント読み進めるとわかりますが、グラフィックを有効にして起動するオプションもありました。
QEMUはエミュレータのことです。それ以上のことはよく知らない状況です。
-m sets QEMU's memory size in MB. ・・・QEMUのメモリ容量を設定する -g enables graphics (see below). ・・・GUIを有効にする -N enables networking (see below). ・・・ネットワーク接続を有効にする -k enables KVM acceleration on Linux. ・・・KVMを有効にする -h help ・・・ヘルプ
改めて、この設定で起動してみます。一度シャットダウンしてもう一度起動します。
$ dm shutdown ・・・シャットダウンする時はdmコマンドを使います。 [01923.365] 04981:05009> minfs: Unmounted [01923.377] 10107:10120> thread 'main' panicked at 'log_listener: not able to wr [01923.377] 10107:10120> note: Run with `RUST_BACKTRACE=1` environment variable [01923.445] 04398:04413> Allocation Info: [01923.446] 04398:04413> Allocated 0 blobs (0 MB) in 0 ms [01923.446] 04398:04413> Writeback Info: [01923.446] 04398:04413> (Client) Wrote 0 MB of data and 0 MB of merkle trees [01923.446] 04398:04413> (Client) Enqueued writeback in 0 ms, made merkle tree [01923.446] 04398:04413> (Writeback Thread) Wrote 0 MB of data in 0 ms [01923.446] 04398:04413> Lookup Info: [01923.446] 04398:04413> Opened 1257 blobs (738 MB) [01923.446] 04398:04413> Verified 339 blobs (153 MB data, 2 MB merkle) [01923.446] 04398:04413> Spent 1012 ms reading 5 MB from disk, 7140 ms verifyi [01923.464] 04398:04413> blobfs: Unmounted [01923.469] 01281:01283> devcoordinator: Successfully waited for VFS exit comple $fx run -g
CUIが出てきました。クリックするとマウスが持っていかれるので ctrl + alt + gでマウスを奪い返します。
画面上部に表示されている <0] debug [1] [2] [3]
という表記はタブを表していて、 opttion(⌥) + tab でタブ切り替えが可能となります。
また、-Nの起動オプションも試してみます。
$ fx run -N WARNING: Please opt in or out of fx metrics collection. You will receive this warning until an option is selected. To check what data we collect, run `fx metrics` To opt in or out, run `fx metrics <enable|disable> Creating disk image...done To use qemu with networking on macOS, install the tun/tap driver:
tun/tapドライバを入れろというのでぐぐって入れます。このコマンドで行ける模様。
$ brew cask install tuntap ・・・略・・・ installer: Package name is TunTap Installer package installer: Installing at base path / installer: The install failed (エラーによってインストールできませんでした。ソフトウェアの製造元に問い合わせてください。) Follow the instructions here: https://github.com/Homebrew/homebrew-cask#reporting-bugs
っておいぃぃぃ!!!入らないんかーい! ということで-Nオプションはちょっと断念・・・解決できた人いたら教えてください・・・ 😇