- 投稿日:2020-07-11T20:50:42+09:00
ExoPlayerのUI変更方法
以下のように、 ExoPlayer は特別な変更をしなくても良い感じの UI にしてくれるのですが、
これらの UI をカスタマイズする事も出来ます。
動画再生UIの変更
ExoPlayer の UI変更 はちょっと特殊で、
com.google.android.exoplayer2.ui.PlayerView
のapp:player_layout_id
に動画再生画面のレイアウトIDを指定します。<com.google.android.exoplayer2.ui.PlayerView ~~~ app:player_layout_id="{layout_id}" />UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、PlayerView (ExoPlayer library)のOverriding the layout file
の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。ちなみに、デフォルトでは
exo_simple_player_view.xml
というレイアウトが表示されております。
res_id 意味 型 exo_content_frame
再生中のメディアやアルバムアートを表示するフレームです。 AspectRatioFrameLayout
exo_buffering
プレイヤーがバッファリングしている時に表示される View 。 View
exo_subtitles
字幕の View です。 SubtitleView
exo_shutter
ビデオを非表示にするときに表示される View 。応用的な方法として、再生開始時は動画再生画面を隠すことで、ビデオ再生時のちらつきをなくす事が出来ます。 View
exo_artwork
アルバムのアートワークを表示します。 ImageView
exo_error_message
エラーが起きたときに表示されるテキスト。 TextView
exo_controller_placeholder
インフレートされたPlayerControlViewを置き換えられます。 exo_controller
が存在する時は無視されます。View
exo_controller
すでにインフレートされているPlayerControlView。 PlayerControlView exo_ad_overlay
広告のUIを表示するためのオーバーレイです。 FrameLayout exo_overlay
プレイヤーの上部に表示されるオーバーレイです。 FrameLayout (
exo_controller_placeholder
とexo_controller
の違いはいまいちわかっていませんので、詳しい方は教えて頂けるとうれしいです。。。 )例
動画の読み込み中に Lottieアニメーション が表示されるようにします。
アニメーションは LottieSample に良い感じの物が落ちていたので、こちらを使わせて頂きます。
https://lottiefiles.com/1055-world-locations事前準備
読み込み中に
exo_buffering
を表示させるには、 Layout に以下の設定が必要です。<com.google.android.exoplayer2.ui.PlayerView ... app:show_buffering="always" ... />UI変更
プレイヤーのUI
exo_buffering
を画面全体を覆う半透明の黒い Layout の上に先ほどの LottieAnimation を表示する様にします。my_movie_view.xml<?xml version="1.0" encoding="utf-8"?> <com.google.android.exoplayer2.ui.AspectRatioFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/exo_content_frame" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/exo_error_message" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@id/exo_controller_placeholder" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/exo_buffering" android:layout_width="0dp" android:layout_height="0dp" android:background="#9E000000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <com.airbnb.lottie.LottieAnimationView android:layout_width="300dp" android:layout_height="300dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:lottie_autoPlay="true" app:lottie_loop="true" app:lottie_rawRes="@raw/loading" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>UI指定
activity_main.xml
で UI の レイアウトID を指定します。activity_main.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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view" android:layout_width="0dp" android:layout_height="0dp" android:background="#00000000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:player_layout_id="@layout/my_movie_view" app:show_buffering="always" /> </androidx.constraintlayout.widget.ConstraintLayout>完成
こんな感じになります。
コントロールバーの変更
コントロールバーも動画再生UIの変更と同じような感じで、
com.google.android.exoplayer2.ui.PlayerView
のapp:controller_layout_id
にコントロールバーの レイアウトID を指定します。<com.google.android.exoplayer2.ui.PlayerView ~~~ app:controller_layout_id="{layout_id}" />動画再生UI と同じく、 UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、PlayerControlView (ExoPlayer library)のOverriding the layout file
の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。ちなみに、デフォルトでは
exo_playback_control_view.xml
というレイアウトが表示されております。
res_id 意味 型 exo_play
再生ボタン View
exo_pause
一時停止ボタン View
exo_rew
巻き戻しボタン View
exo_ffwd
早送りボタン View
exo_prev
前に戻るボタン View
exo_next
次に進むボタン View
exo_repeat_toggle
リピートボタン drawableのID指定によって、トグルの画像を変更します。 exo_controls_repeat_off
、exo_controls_repeat_one
、exo_controls_repeat_all
ImageView
exo_shuffle
シャッフルボタン drawableのID指定によって、トグルの画像を変更します。 exo_controls_shuffle_off
、exo_controls_shuffle_on
ImageView
exo_vr
VRモードボタン View
exo_position
再生位置を表すテキスト。 TextView exo_duration
動画自体の長さを表すテキスト。 TextView exo_progress_placeholder
インフレートされたDefaultTimeBarを置き換えられます。 exo_progress
が存在する時は無視されます。View
exo_progress
再生中に更新され、シークできるプログレスバー。 DefaultTimeBarが表示されています。 TimeBar (
exo_progress_placeholder
とexo_progress
の違いはいまいちわかっていませんので、詳しいから教えて頂けるとうれしいです。。。 )プログレスバーについて
ExoPlayerのプログレスバー、一見SeekBarを使っていそうに見えますが、実は違うんです・・・。
public class DefaultTimeBar extends View implements TimeBar { ... }普通の Viewクラス ですね・・・。じゃあどうやってバーを描画しているの?と思ってコードを追っていると、毎回表示位置を計算して Canvas に描画している事がわかりました。
@Override public void onDraw(Canvas canvas) { canvas.save(); drawTimeBar(canvas); // タイムバーをCanvasへ描画 drawPlayhead(canvas); // プレイヘッドをCanvasへ描画 canvas.restore(); }これの何が痛いかというと、プログレスバーの背景を画像にしたり、角丸にしたり等が容易に出来ないのです。
DefaultTimeBar側で背景色の変更やプレイヘッドの Drawable変更 のインターフェースは設けられているのですが、背景変更は存在しません。。。
こうなった経緯
以下の Commit を確認すると、元々はSeekBarで実装されていたことがわかります。
では、なぜわざわざ Canvas への描画方式へ変更したのかと言いますと、この変更がリリースされたのが、
v2.4.0
になっており、この時のリリースノートには以下の様に記述されています。New time bar view, including support for displaying ad break markers.
広告の再生位置表示を行う様にしたという内容です。 YouTube のように広告再生位置をバー上に表示する、という内容ですね。
このようなバー上に何かを描画する場合は、 SeekBar でやるより、 Canvas へ描画した方が楽なので、このようにしたのだと推測出来ます。プログレスバーの背景変更の解決方
しばらく、どうすればいいのだ・・・?と途方に暮れていましたが、 TimeBar を実装した、 SeekBar のサブクラスを作成すれば良いという事に気づきました。
以下の様なクラスを作ると行けます。SeekTimeBar.ktclass SeekTimeBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : SeekBar(context, attrs), TimeBar { private val listeners: CopyOnWriteArraySet<TimeBar.OnScrubListener> = CopyOnWriteArraySet() private var isSeeking = false init { setLayerType(View.LAYER_TYPE_SOFTWARE, null) setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { listeners.forEach { it.onScrubMove(this@SeekTimeBar, progress.toLong()) } } override fun onStartTrackingTouch(seekBar: SeekBar?) { isSeeking = true listeners.forEach { it.onScrubStart(this@SeekTimeBar, progress.toLong()) } } override fun onStopTrackingTouch(seekBar: SeekBar?) { isSeeking = false listeners.forEach { it.onScrubStop(this@SeekTimeBar, progress.toLong(), false) } } }) } override fun setKeyCountIncrement(count: Int) { progress += count } override fun setBufferedPosition(bufferedPosition: Long) { secondaryProgress = bufferedPosition.toInt() } override fun addListener(listener: TimeBar.OnScrubListener?) { listeners.add(listener) } override fun setDuration(duration: Long) { max = duration.toInt() } override fun removeListener(listener: TimeBar.OnScrubListener?) { listeners.remove(listener) } override fun setKeyTimeIncrement(time: Long) { progress += time.toInt() } override fun setPosition(position: Long) { //シーク中は何もしない(ちらつき防止) if (isSeeking) return progress = position.toInt() } override fun setAdGroupTimesMs(adGroupTimesMs: LongArray?, playedAdGroups: BooleanArray?, adGroupCount: Int) { } override fun verifyDrawable(who: Drawable): Boolean { return super.verifyDrawable(who) || who == progressDrawable } }あとは、 XML側 で
android:progressDrawable
やandroid:thumb
を指定すれば背景変更やプレイヘッド変更が可能になります。
(広告再生位置の表示は未対応です。それを実装したい場合は少々めんどくさい・・・。)例
長らくプログレスバーについて説明していましたが、ここではコントロールバーの変更の例を示します。
再生ボタンと停止ボタンを前面に表示するだけのコントロールバー(バー・・・?)を実装します。事前準備
今回はコントロールバーを全画面表示したいので、
my_movie_view.xml
のexo_controller_placeholder
のサイズを0dp
に変更します。my_movie_view.xml<View android:id="@id/exo_controller_placeholder" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />UI変更
コントロールバーのUI
背景を半透明にして、再生ボタンと停止ボタンを中央に表示します。
ややこしいですが、再生中は停止ボタンを表示するようにし、停止中は再生ボタンを表示するようにしています。(YouTubeと同じ方式)my_controller_view.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" android:background="#BE000000"> <ImageButton android:id="@+id/exo_pause" android:layout_width="200dp" android:layout_height="200dp" android:background="@null" android:scaleType="fitXY" android:src="@drawable/exo_controls_pause" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageButton android:id="@+id/exo_play" android:layout_width="200dp" android:layout_height="200dp" android:background="@null" android:scaleType="fitXY" android:src="@drawable/exo_controls_play" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_goneMarginStart="25dp" /> </androidx.constraintlayout.widget.ConstraintLayout>UI指定
activity_main.xml
で UI の レイアウトID を指定します。activity_main.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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view" android:layout_width="0dp" android:layout_height="0dp" android:background="#00000000" app:controller_layout_id="@layout/my_controller_view" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:player_layout_id="@layout/my_movie_view" app:show_buffering="always" /> </androidx.constraintlayout.widget.ConstraintLayout>完成
こんな感じになります。
最後に
ExoPlayer の UI変更 についてまとめました。
UI変更方法自体はちょっと複雑ですが、マスターすればいろんな動画再生の UI を実現出来るような気がします。
(もっと楽に UI変更できて欲しいという気持ちはありますが・・・。 )今回、 UI変更 について再調査している時に、まだ自分の把握していない設定がありましたので、次回触るときにはマスターしておきたいですね・・・。
参考
- 投稿日:2020-07-11T17:22:57+09:00
Bitriseで32分かかっていたAndroidのCI/CDをGitHub Actionsに移行して15分にした話
はじめに
これは自分が内定者バイトとして参画している OPENREC.tv(オープンレック) のタスクとして取り組んだ、CIの高速化に関しての知見を残すために書くブログです。
CIのある環境でコードを書いた経験はありますが、CI自体の構築は3ヶ月前に初めて個人Repositoryで行った程度でした。
そのときに書いた記事Github ActionsでマルチモジュールAndroidプロジェクトのCI環境を整えよう(ビルド/Slack通知/Danger/ktlint)も是非読んでみてください。
Bitirse運用での課題
workflow実行時間が長い
一番の問題がworkflow実行時間が長すぎることです。
OPENREC.tvはサービスを開始してから約5年経過しており、コードの量もかなり増えてきたため1度の実行で30~40分程かかっていました。
PRレビューの修正後も35分待ち、再度レビュー指摘があったりktlintFormat忘れをしようものなら再度35分CIを待たなければなりませんでした。
workflow実行j感がが長いせいでなかなかマージができずに作業が進まないことや、hotfixも出すのに時間がかかる問題がありました。
ボトルネック
上図を見ると、
Android Lint
Unit Test
でかなり時間がかかっていることがわかります。また、これら2つよりは短いものの、
android-build
も他と比べると時間がかかっています。これらはプロダクトが成長していくにつれてさらに肥大化していくため、今後も実行時間が長くなっていくことが想像できます。
そのため、
- Android Lint(ついでにktlint, detektも)
- Unit Test
- Build
の実行時間を短くするアプローチではなく
これらを並列で実行することでworkflow全体の実行時間を短縮する アプローチで改善していくことにしました。
Bitriseで試みた解決方法(失敗)
そもそもBitriseには、他のサービスと違ってjobを並列で回すことはできません。
しかし、 build-router-startを使うことで、
workflow
の並列化ができます。ex) Unit Test と lintを並列で回す場合
※画像は
PR
やpush
で左のbuild-test
workflowが起動し、Bitrise Start Build
で右のlint
workflowをtriggerさせます。lintが完了するのを
Bitrise Wait for Build
stepで待つことで、見かけ場は並列実行ができているように見えます。しかし、
- Bitirseのworkflow並列実行可能数を圧迫してしまうため実質的な高速化にはつながらない
- lint reportをマージするのが困難
この2つの理由から
build-router-start
での並列実行は断念しました。Bitirseのworkflow並列実行可能数を圧迫してしまうため実質的な高速化にはつながらない
この方法はあくまでも workflow 単位での並列化のため、同時並列実行可能枠が1である無料プランではそもそも使えませんし、有料プランでも並列実行可能枠に応じた課金となるため、この方法ではworkflowを圧迫してしまい、結局高速化には繋がりませんでした。
※4枠あったとして、30分を4枠で同時に実行するのと、15分だけど2枠使うworkflowを4枠で実行するのでは実行時間としてはほぼ同じ(むしろinstall missing Android SDK等で実行時間は伸びる)
lint reportをマージするのが困難
workflow間でデータの共有をする方法は主に2つあると思っています。
- 環境変数を用いる
- Cacheを用いる
前者に関しては、 公式ドキュメント(環境変数のサイズ制限を増やす)より、1つあたり20KB、合計120KBという制限があります。
multi moduleでは各moduleでreportが生成されますし、ktlint以外にもAndroid Lint, detekt, test reportも環境変数で渡そうとするとサイズ制限にひっかかるため難しいので断念。
後者のCacheに関しては、少しややこしいので図解します。
キャッシュは、ブランチ単位で保持されます。
今回やりたいこととしては、
「
lint report
をbuild-test
workflowに共有したい」のですが、
lint reportがブランチ単位で保持されるため、任意のPRでのみ使用するlint reportをキャッシュで持ってしまうと、タイミングが悪ければ同じブランチのPRでそのキャッシュが適用されてしまう可能性があります。
【具体例】hoge branchで初めてPRを出した時にlint結果をDangerで指摘する場合
解決策は、キャッシュするディレクトリ名を変えたり、できなくはないのですがBitriseの設計思想としておそらく上記のユースケースは想定されていないため、無理やり実装することになりそうです。
結論
結局、上記の理由からBitriseで並列実行するのは諦め、他のCIツールに乗り換えることにしました。
BitriseはGUIで簡単にworkflowが構築できる反面、プロジェクトが大きくなるにつれ辛みもでてくるなーという感じです。
Github ActionsでのCI構築
Github ActionsでマルチモジュールAndroidプロジェクトのCI環境を整えよう(ビルド/Slack通知/Danger/ktlint) にも書いたので、詳しい構文の説明はしません。
jobの構成はこんな感じです。
時間のかかるlint, build, testを並列化し、それらのjobが終わった後にreportを元にdangerでPRにコメントしてSlackに通知する形にしました。
deployのjobはdangerと並列しても良いですし、workflowを分けても良いと思います。マルチモジュール配下でのjobの並列化
初めての慣れないymlでのCI構築というのと、マルチモジュールでつまづいた点もあったので共有です。
lint結果、test結果のupload
Bitirseでも少し触れましたが、job間は環境がクリアされるためlintやtest結果をjob間でやりとりする必要があります。
Cacheを使うと、上述したような状況になる可能性があります。
そこで使うのが アーティファクトです。
Artifacts allow you to persist data after a job has completed, and share that data with another job in the same workflow
より、同一workflowのjob間で共有することができます。
- name: Upload Hoge uses: actions/upload-artifact@v2 with: name: hoge path: hoge/hogehoge.txtしかし、multi module環境下では、各moduleでreportは生成され得ます。
upload actionは上記のように、ファイルかディレクトリを指定してuploadします。
multi moduleで、各moduleのディレクトリで良いのですが、moduleが数十もあるととてもじゃないですがメンテナンスができません。
そこで、conference-app-2020(DroidKaigi2020) を参考にし、各moduleのreport結果を1つのディレクトリにまとめてuploadすることですっきり書くことができました。
Dangerfile
でlint reportのディレクトリを指定している場合はDir.glob("**/checkstyle.xml").each do |xml|
のように任意のディレクトリのreportを集めてあげるように修正する必要があるので注意です。upload, download での注意点
upload-artifactは、ファイル単位でjob間でデータを渡すことができます。
しかし、download-artifactでは、ディレクトリ単位でデータを受け取るため、注意が必要です。例えば
build job
でapkをuploadし、deploy job
でdownloadするとき、このように書くと意図せぬ結果になります。- uses: actions/upload-artifact@v2 with: name: generated development apk path: ./app/build/outputs/apk/hoge/debug/app-hoge-debug.apkここまではok
- uses: actions/download-artifact@v2 with: name: generated development apk path: ./app/build/outputs/apk/hoge/debug/app-hoge-debug.apkこのようにdownloadしようとすると、見かけ上はdownloadできているようにlogが残るのですが、
deploy時にバイナリが破損していてdeployできない問題に遭遇しました。download-artifactのドキュメントを見ると、
Basic (download to the current working directory):
or
Download to a specific directory:と、基本的にはディレクトリのパスを指定してのdownloadをするひつようがありました。
そのため、- uses: actions/upload-artifact@v2 with: name: generated development apk path: ./app/build/outputs/apk/hogeのようにupload時もディレクトリで指定して、downloadするときも同様のディレクトリを指定することで解決しました。
定期実行
schedule構文を使います。
on: # default branchの最新commitに対して規定の時刻でworkflowを実行する schedule: - cron: '0 14 * * 1-5'土日を抜いた23時にCIを回すよう設定できました。
時間に関しては、デフォルトはUTCです。
他の記事で
envにTZ: 'Asia/Tokyo'
を指定するとみたのですが、TimeZoneの変更がうまくいかず、 UTC時間で記載しています。(わかる方いたら教えてください)
cron:
のところ、spaceが2つなので気をつけてください。めっちゃハマりました。tag push
tag構文を使います。
tagにバージョンを指定することでリリースビルドが走り、deployするようなことをする際には、
on: push: tags: - ver*このように書けます。
無駄なworkflowをキャンセルする
Github Actionsは従量課金のため、同一PRで既に走っているworkflowは自動でキャンセルするようにしないとかなりコストがかかってしまいます。
ここに関してはまだデファクトスタンダードなものはないみたいですが、現段階で一番スターの多いCancel Workflow Actionを使ってみました。
- name: Cancel Previous Runs uses: styfle/cancel-workflow-action@0.4.0 with: workflow_id: 1111111 access_token: ${{ secrets.GITHUB_TOKEN }}private repositoryの場合は
https://api.github.com/repos/:org/:repo/actions/workflows
をrepo
の読み取りができるアクセストークンを添えてcurlで叩くとworkflowのidが取得できます。このactionを使うことで、新しくcommitをpushした際に前回分commitのworkflowは自動でキャンセルされるので無駄な課金がされなくなり安心ですね。
おわりに
全てのstepを直列実行していたBitriseからGithub Actionsに移行し、時間のかかるstepを並列で実行したことにより、
1回あたりのworkflow実行時間が30~40分 → 15分に短縮できました?
これでCIを待つ時間が半分になり快適な開発に貢献できたと思うと、最高ですね。
UI実装や機能開発などの普段のAndroid開発と違い、経験が浅かったため少しのバグや機能追加でも結構詰まりましたが、苦労してやった分かなり力が着いたと思います。
まだ色々Github Actionsで効率化できそうなので、今後も個人でちょくちょく触って記事を書いていくのでよろしくお願いします。
- 投稿日:2020-07-11T00:52:43+09:00
Xamarin.Forms ガワネイティブアプリで、C# から JavaScript へのリクエスト~レスポンスを Task<T> にする
メモ書きなので雑。
シナリオ
- Xamarin.Forms + WebView(HTML, JavaScript な SPA) のガワネイティブアプリである
- Android アプリの BACK キーの有効無効を、WebView 内のページ状態によって切り替えたい
- データ編集中(未保存)なので Back キーで history.back やアプリ終了されたら困る、とか
流れ
1. [Forms側]
Action<TaskCompletionSource<bool>> OnRequestIsEnableBackKey { get; set; }
を生やす2. [Forms側] WebViewEx(WebViewを拡張したもの)に
Task<bool> IsEnableBackKeyAsync()
も生やす。実装は次のように。public Task<bool> IsEnableBackKeyAsync() { var comp = new TaskCompletionSource<bool>(); if (OnRequestIsEnableBackKey != null) { OnRequestIsEnableBackKey.Invoke(comp); } else { comp.SetResult(false); } return comp.Task; }3. [Android側] WebViewRenderer を拡張した MyWebViewRenderer を作る。
4. Xamarin.Forms の WebView で JavaScript 連携を行う(with iOS/Android共通化) - Qiita を参考に JavaScriptHandler も作る。コンストラクタで Control に加え
e.NewElement as WebViewEx
も渡す。引数の名前はWebViewEx outer
とする。AddJavascriptInterface の第2引数はGawaApp
とでもしておく。5.JavaScriptHandler のコンストラクタで OnRequestIsEnableBackKey を受信するコードを書く。
private TaskCompletionSource<bool> isEnableBackKeyComp = null; public JavaScriptHandler(Android.Webkit.WebView webView, WebViewEx outer) { outer.OnRequestIsEnableBackKey = comp => { isEnableBackKeyComp = comp; // メインスレッドから呼ばないとエラー webView.Post(() => { webView.EvaluateJavascript("window.requestIsEnableBackKey()", null); // webView.LoadUrl("javascript:window.requestIsEnableBackKey();"); ←これでもOKっぽい }); }; }ここまでの処理で、Forms 側で
await webViewEx.IsEnableBackKeyAsync()
が呼び出されたら、JavaScript のwindow.requestIsEnableBackKey()
関数が呼び出される。6.JavaScriptHandler 内に onResultCanExitApp を生やす。JavaScript側 から結果が通知されるメソッドである。次のように。
[Export] [Android.Webkit.JavascriptInterface] public void onResultIsEnableBackKey(bool value) { isEnableBackKeyComp?.SetResult(value); isEnableBackKeyComp = null; // one shot }JavaScript 側で
GawaApp.onResultIsEnableBackKey(true or false)
を呼び出すと、このメソッドがコールバックされる。
TaskCompletionSource である isEnableBackKeyComp に値を設定すれば、Forms 側のawait webViewEx.IsEnableBackKeyAsync()
の結果が返される。7.JavaScript 側はきっとこんな感じ
window.requestIsEnableBackKey = () => { // 何かの処理 GawaApp.onResultIsEnableBackKey(true or false); }途中まで実装したけど、面倒になって(JavaScript→C# に単方向の方がシンプルでいいやと思って)やめちゃったので、アイデアだけ残しておきます。
- 投稿日:2020-07-11T00:20:40+09:00
Androidのヒンジ角度センサに関しての補足
はじめに
- ヒンジ角度センサに関する記事をすでに投稿しましたが、いくつか積み残しのタスクがありました。
- ということで、今回は積み残しのそれぞれに対応してみます。
2番目のヒンジ角度センサの値を取得する
こんな感じで変更を加えました。
- Before
val hingeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE) sensorManager.registerListener(this,hingeSensor,SensorManager.SENSOR_DELAY_NORMAL)
- After
val hingeSensors = sensorManager.getSensorList(Sensor.TYPE_HINGE_ANGLE) for (hingeSensor in hingeSensors) { sensorManager.registerListener(this, hingeSensor, SensorManager.SENSOR_DELAY_NORMAL) }
- なんてことはありませんでした。複数のセンサがあるのだから、それぞれを登録する、というただそれだけですね。
ただし、onSensorChangedで値を得て、何かをする際に意識すべきこととして、
値を上げてきたセンサはどちらのセンサか?という点があります。前回の記事のログより、センサの情報は以下の様になるので、区別する方法はnameくらいしかなさそうです。このため、センサの値を集める時は、nameもセットにする必要がありそうです。
{Sensor name="Goldfish hinge sensor0 (in degrees)", vendor="The Android Open Source Project", version=1, type=36, maxRange=360.0, resolution=1.0, power=3.0, minDelay=10000}, {Sensor name="Goldfish hinge sensor1 (in degrees)", vendor="The Android Open Source Project", version=1, type=36, maxRange=360.0, resolution=1.0, power=3.0, minDelay=10000},Postureを扱う
※JetpackのWindow Managerは2020/7/10時点ではリリースされたばかりですのでご注意ください。
現時点の端末の折りたたみの状態(Posture)を取得するためには、DeviceState.getPosture()を使います。
上記のAPIを使う部分がわかりづらかったので、WindowManagerの使い方含めてサンプル実装してみました。
ボタンをタップするとその時点での端末のPostureの値を取得する様な処理になります。WindowManagerの利用設定(dependencyを設定すれば良い)
dependencies { implementation "androidx.window:window:1.0.0-alpha01" }WindowManagerの取得
class MainActivity : AppCompatActivity(), SensorEventListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(findViewById(R.id.toolbar)) val wm = WindowManager(this, null) // ... (略) }WindowManagerコンストラクタの第二引数であるWindowBackendは端末の状態などを取得しようとした時に、特殊な処理を入れる場合に指定します。
デフォルトはnullでよいです。DeviceState.getPosture()の呼び出し
findViewById<Button>(R.id.getPosture).setOnClickListener { view -> val posture = wm.deviceState.posture Log.d("POSTURE_TEST", posture.toString()) }例えばヒンジの角度を変え、開いている状態なら3、半開きなら2、閉じているなら1などとpostureの値を取得できます。
Postureの変更を検知して処理を実行する
Postureの変更を検知する場合は、WindowManager#registerDeviceStateChangeCallbackにExecutorと変更検知の際のコールバックを登録してあげればよいです。
例えば、登録ボタンタップ時に変更の検知を開始して、解除ボタンタップ時に検知を終了する様な処理については以下の通りです。
実装の際は、registerを複数回実行を避ける様にしてください。val mainThreadExecutor = Executor { Handler(Looper.getMainLooper()).post(it) } val deviceStateChangeCallback = Consumer<DeviceState> { Log.d("CHANGE_POSTURE", it.posture.toString()) } findViewById<Button>(R.id.registerChangePosture).setOnClickListener { view -> Log.d("CHANGE_POSTURE", "register") wm.registerDeviceStateChangeCallback(mainThreadExecutor, deviceStateChangeCallback) } findViewById<Button>(R.id.unregisterChangePosture).setOnClickListener { view -> Log.d("CHANGE_POSTURE", "unregister") wm.unregisterDeviceStateChangeCallback(deviceStateChangeCallback) }参考
- WindowManagerについては公式のサンプルを一部参考にしました。