はじめに
Droidkaigi2019で話してきた内容はこちらです。(スライド)
GitHubのwikiにコードなどの内容を載せています。
Chromebook向けのAndroidアプリの開発と、ChromebookでのAndroidアプリの開発についてまとています。
それぞれ、主にCodelabsや参考サイトをまとめた内容となります。
ChromeOSとは
Linuxをベースに、Googleが開発しているOSで、ラップトップ(Chromebook)やデスクトップ(Chromebox)向けに提供されている。
簡単な歴史:
- 2009年:Google Chrome OSのオープンソース版である「Chromium OS」(クロミウム・オーエス)のソースコードを公開
- 2016年 Google I/O:Androidアプリのサポートを発表
- 2018年 Google I/O:Linuxのサポートを発表(現在も、beta状態)
AndroidからみたChromebookの特徴
ChromeOSとAndroidの差分について
ChromeOS -> Androidアプリに合わせている部分
Androidアプリ -> ChromeOSに合わせている部分
以下のページで、ChromeOS独自の対応について説明
- 04_ウィンドウ・レイアウトサポート
- 05_入力サポート
ChromeOSでのAndroidアプリのデバッグについて
以下で説明
- 01_ChromeOSのアプリデバッグ方法
- 02_ChromeOS上でのAndroidアプリの開発
Chromebook向けのアプリの対応として、マルチウィンドウ、フリーフォームレイアウトの対応について、複数サイズの画面の対応について記載。
ウィンドウ・レイアウトサポート
Chromebookの画面に関する特徴
ChromebookではPCと同様に、複数ウィンドウの立ち上げと、ウィンドウサイズの変更できることが特徴となります。
APIレベル別のウィンドウ対応
アプリビルド時のtargetSdkVersion
によって挙動が変更される。
API level | Window manager behavior |
---|---|
<= Android 1.5 (API level 3) | 全画面表示固定 |
Android 1.6 (API level 6) ~ Android 6.0 (API level 23) | リサイズが保証されていないアプリとして扱われる。起動時のデフォルトは、縦画面のphoneサイズとして起動する。端末の画面表示切り替えボタンによって、画面サイズが切り替えられた場合はアプリを再起動させる。 |
Android 7.0 (API level 24) resizable | マニフェストに何も指定をしていない場合はデフォルトでリサイズをサポートしているものとして扱い、ウィンドウをアプリの再起動なしでリサイズさせます。アプリの起動時は、デフォルト全画面で、M60以下のChromeOSの場合だけ、Nexus 5Xの縦画面のサイズで表示されます。 |
Android 7.0 (API level 24) un-resizable | マニフェストに、android:resizeableActivity="false"を指定した場合、リサイズが行われないようになります。ただし、特定のユーザー処理が行われるとウィンドウのリサイズ処理が行われます。 |
Android 7.0 (API level 24) app-controlled | マニフェストで指定したとおりの処理が実行されます。 |
アプリの、最大化状態や画面サイズは記録されます。
Root Activityルール
ChromebookのウィンドウはActivityのスタックごとに別れ、それぞれのスタックごとに、ウィンドウの 向き と サイズ を管理しています。
また、各スタックの一番下のActivity設定が、そのスタックに上のすべてのActivityの属性を決定するため注意必要となります。
例えば、一番下のスタックのRootActivityが画面の向き変更自由で、後からスタックに積まれたSubActivityが縦画面固定の設定をしていても、この設定は無視されます。また、ウィンドウサイズもすでにあるスタックの属性をそのまま受け継ぎます。
デバイスモードの場合の注意:
タブレットモードの向きはロックされず、Android通常の画面回転と同様に処理されます。
ただし、targetSdkVersion
がAndroid 6.0(API level 23)以下の場合はロックされます。
別ウィンドウで立ち上げたい場合
Activityの立ち上げを別スタックにすればよいため、IntentのIntent.FLAG_ACTIVITY_NEW_TASK
Flagを指定することで、別ウィンドウ立ち上げが行える。
Androidのマルチウィンドウの種類について
マルチウィンドウでは、Activityを別ウィンドウで立ち上げることによる別画面で情報を同時に見せたり、アプリ内の別画面や、複数のアプリの間でドラッグ&ドロップによるデータの共有を行うことができます。
マルチウィンドウは、Android 7(Nouger)で大きく変更が入り、特にPCやTV向けのサポートが多くありました。
複数のウィンドウを表示するには、主に以下のタイプがあります。
スマホ | Chromebook | |
---|---|---|
フリーフォーム | x | ○ |
分割画面 | ○ | x |
ピクチャーインピクチャー | ○ | x |
Chromebookでは、フリーフォームのウィンドウのみ対応となる。
Chromebookのウィンドウサイズの種類
Chromebook上でのウィンドウサイズ。
- フリーフォーム
- スマホサイズ
- タブレットサイズ
- ハーフレイアウト -> タブレットサイズ
- 全画面レイアウト -> PCサイズ
ユーザー操作:
- F3キー:全画面<->フリーフォームレイアウトの切替が可能
- 画面の両端へのドラッグ:ハーフレイアウトにすることが可能
- デバイスモードへの変更:タブレットモード<->PCモードの切り替えが可能
- 画面回転 + 画面サイズ変更が発生
- (ウィンドウの端をドラッグ):画面サイズを自由に変更可能
ウィンドウサイズの指定
Android 7.0のウィンドウ設定
Android 7.0 (API level 24) 以降の、すべて対応を行う場合の各種設定。
<manifest>
<!-- マルチウィンドウに対応しているかの指定。
Applicationレベルか、Activityレベルで設定 -->
<application
android:resizeableActivity="[true|false]">
<!-- -->
<activity
android:name".RootActivity"
android:resizeableActivity="[true|false]">
<!-- 画面の左端から、バックボタンを消す変更 -->
<meta-data
android:name="WindowManagerPreference:SuppressWindowControlNavigationButton"
android:value="[true|false]" />
<!-- 起動時のウィンドウサイズ指定 -->
<meta-data
android:name="WindowManagerPreference:FreeformWindowSize"
android:value="[phone|tablet|maximize]" />
<!-- 起動時のウィンドウの向き指定 -->
<meta-data
android:name="WindowManagerPreference:FreeformWindowOrientation"
android:value="[portrait|landscape]" />
</activity>
<activity
android:name".FreeActivity">
<!-- DP値でウィンドウサイズや位置を指定する場合 -->
<layout
android:defaultHeight="500dp"
android:defaultWidth="600dp"
android:gravity="[top|bottom|start|end]"
android:minHeight="450dp"
android:minWidth="300dp"/>
</activity>
</application>
</manifest>
動的に起動するウィンドウサイズを変更する
新たなアクティビティを起動する際に、 ActivityOptions.setLaunchBounds()
を指定したActivityOptionsを使用することによって新しいアクティビティの画面上での位置を指定することができます。
ただし、マルチウィンドウモードではない場合などは無視されます。
val options = ActivityOptions.makeBasic().apply {
launchBounds = Rect(0, 0, 500, 500)
}
startActivity(it, options.toBundle())
※新規スタックでなければウィンドウサイズ等の変更はできず、既存のスタックの属性が継承されることに注意
分割画面で、ウィンドウを隣に表示する Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT
ドラッグ&ドロップサポート
複数のウィンドウ表示ができるため、画面内だけではなく、画面感のドラッグ&ドロップも簡単にできるようになるため、Chromebookでは重要なユーザー操作となる。
別アプリとのドラッグ&ドロップもサポートするために、ドラッグするデータの形式などを指定する必要があります。
共有できるデータは、主に以下の2種類になります。
- テキスト
- URL
※別アプリへ共有する場合は、相手のアプリがそのMIMETYPEをサポートしている必要があります。
ドラッグする側の処理
- ドラッグするデータの作成
- ClipDataクラスの作成
- 指定できるデータと、それぞれのMIMETYPE
- テキスト:ClipDescription.MIMETYPE_TEXT_PLAIN
- HTMLテキスト:ClipDescription.MIMETYPE_TEXT_HTML
- URL:ClipDescription.MIMETYPE_TEXT_URILIST
- Drag中のViewの指定
-
View.DragShadowBuilder
クラスを使用する
-
-
View.startDragAndDrop()
の呼び出し
val targetView = it as TextView
// 1. ドラッグするデータの作成
val dragContent = "Dragged Text: ${targetView.text}"
val item: ClipData.Item = ClipData.Item(dragContent)
val dragData: ClipData = ClipData(dragContent, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item)
// 2. Drag中のViewの指定
val dragShadow = View.DragShadowBuilder(targetView)
// 3. View.startDragAndDrop()の呼び出し
targetView.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL)
View.startDragAndDrop()
は、Android7.0より前に存在したView.startDrag()
のエイリアスで、DropPermissionなどのマルチウィンドウ操作向けのフラグをつけるために拡張されたメソッドです。
DropPermissionsについては、後ほど説明します。
その他の操作:
それぞれ、DragAndDrop中のイベントに対してキャンセルとシャドーの変更ができます。
-
View.cancelDragAndDrop()
:実行中のドラッグ操作をキャンセルします。 -
View.updateDragShadow()
:実行中のドラッグ操作のドラッグシャドウを置き換えます。
※ドラッグ操作を開始したアプリだけが呼び出せます。
ドロップされる側の処理
別のウィンドウからイベントを受け付ける可能性があるため、イベント内のデータのMimeTypeなどを確認する必要があります。
DragEvent.ACTION_DRAG_STARTED
とDragEvent.ACTION_DRAG_ENDED
イベントは、他のウィンドウ(別アプリ含む)でドラッグがスタートしたり、終了したタイミングで呼び出されます。
view.setOnDragListener { view, event ->
when (event.action) {
DragEvent.ACTION_DRAG_STARTED -> {
// Limit the types of items that can be received
if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
// Greenify background colour so user knows this is a target
view.setBackgroundColor(Color.argb(55, 0, 255, 0));
return true
}
false
}
DragEvent.ACTION_DRAG_ENTERED -> {
// Increase green background colour when item is over top of target
view.setBackgroundColor(Color.argb(150, 0, 255, 0))
true
}
DragEvent.ACTION_DRAG_LOCATION -> {
true
}
DragEvent.ACTION_DRAG_EXITED -> {
// Increase green background colour when item is over top of target
view.setBackgroundColor(Color.argb(55, 0, 255, 0))
true
}
DragEvent.ACTION_DROP -> {
requestDragAndDropPermissions(event)
val item: ClipData.Item = event.clipData.getItemAt(0)
if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
(view as TextView).also { target ->
target.setTextSize(TypedValue.COMPLEX_UNIT_SP, 30f)
target.text = item.text
}
return true
}
false
}
DragEvent.ACTION_DRAG_ENDED -> {
// Increase green background colour when item is over top of target
view.setBackgroundColor(Color.argb(0, 255, 255, 255))
true
}
else -> false
}
}
DropPermissions
ドラッグ&ドロップでURIを共有する場合、受け取り側でURIがどのようなものかを判断してパーミッションをリクエストすることはできません。そのため、パーミッションをドラッグする側が取得し、それをドロップされる側に渡す仕組みがDropPermissionsです。
ドロップされる側は、渡されたURIに対するパーミションの実装をすることなくURIのデータを扱うことができます。
また、パーミッションが付与されるのはDragEventで渡されたデータのURIだけで、他のURIに対しては権限が付与されないためユーザーにとっても安心して利用できる機能になっています。
ドラッグする側の処理
View.startDragAndDrop()
の第4引数のフラグをOR(|)で連結して指定します。
それぞれ、権限周りのフラグは以下の通りです。
- 他アプリとのDrag許可
- View.DRAG_FLAG_GLOBAL
- URIへの権限許可 ※1の指定必須
- View.DRAG_FLAG_GLOBAL_URI_READ:URIに対する読み込み権限を付与
- View.DRAG_FLAG_GLOBAL_URI_WRITE:URIに対する書き込み権限を付与
- URIへの権限許可オプション系 ※2の指定必須
- View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION:
revokeUriPermission
するまで権限を付与する - View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION:フォルダ単位で権限を付与する
- View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION:
例:
targetView.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL|DRAG_FLAG_GLOBAL_URI_READ)
※前提として、grantUriPermission
などでURIへのパーミッションを取得しているものとします。
ドロップされる側の処理
ドロップされる側は、Activity.requestDragAndDropPermissions(DragEvent)
を呼び出すだけで必要なパーミッションを取得できます。
view.setOnDragListener { v, event ->
when (event.action) {
DragEvent.ACTION_DROP -> {
// 権限が取得できない場合や不正な場合は、nullが変える
val permissions: DragAndDropPermissions? = requestDragAndDropPermissions(event)
// 使用が終わったり、不要になったらreleaseする
permissions?.release()
true
}
}
デザインについて
GoogleのMaterial Designのの公式サンプルだとレスポンシブにGridを使用したデザインが多い。
RecyclerViewだと表示するアイテムのリストの列数を動的に変更するといった対応が多い。
「レスポンシブ」 + 「コンテンツを固定」 => 「コンテンツ数」 & 「余白」で調整。
詳しくは、Responsive layout gridを見る。
Google Material DesignのShrineの例:
ライフサイクル
ウィンドウサイズ変更時のライフサイクル
Android 7.0(API level 24)のマルチウィンドウの標準的なライフサイクルと同様。
画面回転時のライフサイクルと同様に処理され、画面の再構成(onCreate)が行われます。
ウィンドウ切替時のライフサイクル
Android 7のマルチウィンドウの標準的なライフサイクルと同様。
マルチ ウィンドウ モードでは、ユーザーが直前に操作したアクティビティのみが任意の時点でアクティブになる。このアクティビティは、トップレベルにあると見なされ、他のすべてのアクティビティは、表示されていても一時停止状態(onPause)になります。
ただし、一時停止状態ではあるが、表示されているこれらのアクティビティには、表示されていないアクティビティよりも高い優先度が付与されます。 ユーザーが一時停止状態のアクティビティのいずれかを操作した場合、そのアクティビティが再開(onResume呼び出し)され、前のトップレベルのアクティビティが一時停止(onPause呼び出し)します。
画面Bにフォーカス変更 | 画面Aの中身をクリック
画面A:onPause(優先度:高) -> onResume
画面B:onResume -> onPause(優先度:高)
画面C(非表示):onPause(優先度:低)
動画アプリなどの場合は、onPauseで各種のリソースを破棄している場合はマルチウィンドウの場合だけonStop() で動画を一時停止し、onStartで再生を再開するようにするなどの検討が必要です。
Activity.isInMultiWindowMode()
Fragment.isInMultiWindowMode()
ウィンドウサイズ変更時のアニメーション
画面切り替え時のアニメーション
ウィンドウサイズ変更に合わせて、別々のレイアウトを表示することが多いですが、そのままではレイアウトがただ切り替わるだけで、画面変更が頻繁に発生するChromebookでは見栄えがよくありません。
自力でのViewのアニメーション実装やレイアウト切り替えは複雑で難しいため、ConstraintLayout2から使用できるConstraintLayoutStateを使用した画面変更時のアニメーションのサポートについて書きます。
- ConstraintLayoutのバージョンを2にアップデート※まだ、beta段階
- レイアウトファイルの修正
-
constraint_states.xml
ファイルの作成 - ConstraintLayoutの設定と、ステート変更のハンドリング
- AndroidManifestへの画面変更時の設定追加
0. レイアウトファイルの修正
app/build.gradle
のdependencies内の、constraintlayoutのバージョンを変更する。
dependencies {
implementation "androidx.constraintlayout:constraintlayout:2.0.0-alpha2"
}
1. レイアウトファイルの修正
以下のように、画面サイズなどの条件によってフォルダ分けされているレイアウトファイルを、すべてlayoutフォルダ内に入れます。
- /layout/activity_main.xml
- /layout-land/activity_main.xml -> /layout/activity_main_land.xml
- /layout-w400/activity_main.xml -> /layout/activity_main_w400.xml
- /layout-w600-land/activity_main.xml -> /layout/activity_main_w600_land.xml
※アニメーションさせたいレイアウト要素が、ConstraintLayoutで書かれていない場合はConstraintLayoutへの書き換えが必要。
※layoutのRoot要素がConstraintLayoutである必要はなく、画面内の一部のConstraintLayoutに対して適用することも可能です。
2. constraint_states.xml
ファイルの作成
layoutフォルダによる各レイアウトの適用をやめたので、 ConstraintLayoutStates
によって各Viewの状態とlayoutファイルを紐づけます。
res/xml/
フォルダに、constraint_states.xml
ファイルを作成します。
<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 縦画面 -->
<State
android:id="@+id/constraintStatePortrait"
app:constraints="@layout/activity_main">
<!-- 縦画面 x 横幅が395db以下 => スマホの縦画面サイズ -->
<Constraints
app:constraints="@layout/activity_main"
app:region_widthLessThan="395dp" />
<!-- 縦画面 x 横幅が400db以上 => タブレットの縦画面サイズ -->
<Constraints
app:constraints="@layout/activity_main_w400"
app:region_widthMoreThan="400dp" />
</State>
<!-- 横画面 -->
<State
android:id="@+id/constraintStateLandscape"
app:constraints="@layout/activity_main_land">
<!-- 横画面 x 横幅が595db以下 => スマホの横画面サイズ -->
<Constraints
app:constraints="@layout/activity_main_land"
app:region_widthLessThan="595dp" />
<!-- 横画面 x 横幅が600db以下 => タブレットの横、PCのフルスクリーンサイズ -->
<Constraints
app:constraints="@layout/activity_main_w600_land"
app:region_widthMoreThan="600dp" />
</State>
</ConstraintLayoutStates>
3. ConstraintLayoutの設定と、ステート変更のハンドリング
Viewのセットアップ
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 先ほど作成したConstraintLayoutStatesのxmlを指定する
constraintMain.setLayoutDescription(R.xml.constraint_states)
// stateの変更に合わせて処理を行う場合は、setOnConstraintsChangedを使用することで変更イベントを受け取れる
constraintMain.setOnConstraintsChanged(object : ConstraintsChangedListener() {
override fun preLayoutChange(state: Int, layoutId: Int) {
// レイアウト変更適用前の設定を記述
val changeBounds = ChangeBounds().apply {
duration = 600
interpolator = AnticipateOvershootInterpolator(0.2f)
}
TransitionManager.beginDelayedTransition(constraintMain, changeBounds)
when (layoutId) {
R.layout.activity_main -> {
val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
recyclerReviews.layoutManager = reviewLayoutManager
}
R.layout.activity_main_land -> {
val reviewLayoutManager = GridLayoutManager(baseContext, 2)
recyclerReviews.layoutManager = reviewLayoutManager
}
R.layout.activity_main_w400 -> {
val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
recyclerReviews.layoutManager = reviewLayoutManager
}
R.layout.activity_main_w600_land -> {
val reviewLayoutManager = GridLayoutManager(baseContext, 2)
recyclerReviews.layoutManager = reviewLayoutManager
}
}
}
override fun postLayoutChange(stateId: Int, layoutId: Int) {
// レイアウトの変更を適用するために、requestLayoutを呼び出し
constraintMain.requestLayout()
}
})
configurationUpdate(resources.configuration)
}
// 画面回転によるレイアウト変更を行う場合
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
configurationUpdate(newConfig)
}
private fun configurationUpdate(configuration: Configuration) {
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 自分で特定のsetStateのレイアウトを指定することもできる
constraintMain.setState(R.id.constraintStateLandscape, configuration.screenWidthDp, configuration.screenHeightDp)
} else {
constraintMain.setState(R.id.constraintStatePortrait, configuration.screenWidthDp, configuration.screenHeightDp)
}
}
4. AndroidManifestへの画面変更時の設定追加
AndroidManifestで、該当のactivityタグにconfigChanges
を指定します。
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout">
※ConstraintStateでは、ConstraintLayout内のConstraintを更新するため、画面内の要素を変更することができないことに注意してください。
サイズ変更時の描画遅延時の色指定を行う
ユーザーがウィンドウのサイズを変更した際に、ユーザー操作に追従するようにアクティビティのサイズ変更が行われます。
その際に、アプリで新しく表示された領域を描画するまでに時間がかかる場合、windowBackground
属性またはデフォルトの windowBackgroundFallback
システム属性によって指定された色でこれらの領域が一時的に塗りつぶされます。
そのため、色差が大きい場合はActivityの背景色や、アプリのテーマカラーなどを指定しておくことをオススメします。
メッセージ表示
エラーメッセージなどの表示にDialog、Toastなど複数の方法があるが、画面サイズ変更時に追従するかどうかなどはそれぞれ異なるので注意。
リサイズ対応 | その他 | |
---|---|---|
PupupWindow | ☓ | 表示後のサイズ変更は自分で行う必要がある |
Toast | △ | 端末の画面を基準として、表示が行われる。固定サイズ |
Dialog | ○ | 画面内に表示され、リサイズにもしっかりと追従する |
ウィンドウ対応周りのAndroidデバッグ設定
開発者向けオプション > アプリ項目の各設定を有効化します。
- タイトルにデバッグ情報を表示する
- アクティビティサイズを変更可能にする
- 枠線のドラッグによるウィンドウの自由なサイズ調整を許可
- API レベル24以上のすべてのアプリケーションで自由形式のサイズ変更を有効にする
- 画面の向きが固定されたアプリのサイズ変更
入力サポート
Chromebookの入力周りのサポートについて
入力ソース
- タッチ
- スタイラスペン
- トラックパッド
- マウス
- キーボード
トラックパッド/マウスサポート
マウスカーソルサポート
Android 7.0 から Custom Pointer API が追加された。
基本的な動作については、実装しなくてもButtonのクリックや、TextViewのテキスト選択など大抵のことはデフォルトでカーソルが指定されています。
自分でポインターアイコンを設定する場合は、それぞれViewクラスに設定します。
-
View.setPointerIcon()
にIconをセットする。 -
View.onResolvePointerIcon()
をオーバーライドする。
ポインターアイコンは、デフォルトで用意されているシステムアイコンを使用するか、オリジナルのアイコンを指定することができます。
デフォルトのシステムアイコンを指定する場合
android:pointerIcon="hand"
or
val icon = PointerIcon.getSystemIcon(application, PointerIcon.TYPE_HAND)
view.pointerIcon = icon
デフォルトのシステムアイコンの種類
- PointerIcon.TYPE_ARROW
- PointerIcon.TYPE_CONTEXT_MENU
- PointerIcon.TYPE_HAND
- PointerIcon.TYPE_HELP
- PointerIcon.TYPE_WAIT
- PointerIcon.TYPE_CELL
- PointerIcon.TYPE_CROSSHAIR
- PointerIcon.TYPE_TEXT
- PointerIcon.TYPE_VERTICAL_TEXT
- PointerIcon.TYPE_ALIAS
- PointerIcon.TYPE_COPY
- PointerIcon.TYPE_NO_DROP
- PointerIcon.TYPE_ALL_SCROLL
- PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW
- PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW
- PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
- PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
- PointerIcon.TYPE_ZOOM_IN
- PointerIcon.TYPE_ZOOM_OUT
- PointerIcon.TYPE_GRAB
- PointerIcon.TYPE_GRABBING
オリジナルのアイコンを使用する場合
pointer-iconタグのxmlファイルを、xmlフォルダの下などに追加する。
例:res/xml/custom_pointer.xml
<?xml version="1.0" encoding="utf-8"?>
<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
android:bitmap="@mipmap/ic_launcher"
android:hotSpotX="24px"
android:hotSpotY="24px"/>
それを、UIロジック側で読み込む。
val icon = PointerIcon.load(application.resources, R.xml.custom_pointer))
view.pointerIcon = icon
右クリックサポート
ラップトップだと、longClickListenerなどの代わりに右クリックでのイベントハンドリングを行う。
view.setOnContextClickListener {
true
}
registerForContextMenu(view)
override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
val inflater = activity?.menuInflater ?: return
when (v?.id) {
R.id.menu_button -> {
// Create cart product context menu
inflater.inflate(R.menu.shr_cart_product_context_menu, menu)
}
}
}
override fun onContextItemSelected(item: MenuItem?): Boolean {
// Action selected menu item.
return true
}
キーボードサポート
Chromebookではキーボードのサポートとして、タブキーによるフォーカス変更、ショートカットキーによる操作を推奨してます。
タブキー、方向キーサポート
- タブによるnextフォーカスの設定
- 方向キーによるそれぞれのフォーカスの設定
Focusable設定
android:focusable="true"
or
view.setFocusable = true
また、必要に応じて背景などのリソースをFocusableなものにしておきます。
タブキーサポート
android:nextFocusForward="@id/next_view"
view.nextFocusForwardId = R.id.next_view
方向キーサポート
android:nextFocusLeft="@id/view_to_left"
android:nextFocusRight="@id/view_to_right"
android:nextFocusUp="@id/view_above"
android:nextFocusDown="@id/view_below"
or
view.nextFocusLeftId = R.id.view_to_left
view.nextFocusRightId = R.id.view_to_right
view.nextFocusTopId = R.id.view_above
view.nextFocusBottomId = R.id.view_below
キーボードナビゲーションクラスタ
Android 8.0から、ViewGroup単位でタブキーのハンドリングを行う方法が追加されました。
画面の構成を以下の図のように、ナビゲーションクラスタ(タブキーで移動させたいグループ)ごとにまとめます。
まとめた各クラスタのrootのViewにだけ、keyboardNavigationCluster
プロパティを指定します。
android:keyboardNavigationCluster="true"
or
view.setKeyboardNavigationCluster = true
※ネストされていないクラスタが階層の別のレベルに表示されることがあっても、クラスタはネストできません。クラスタをネストしようとすると、フレームワークは最上位の ViewGroup 要素のみをクラスタとして処理します。
つまり、ConstraintLayoutなどで、全クラスタがフラットな状態になるようにViewを組む必要があります。
ショートカットキーサポート
アプリ内でよくある操作を、ショートカットとしてサポートします。
Activity.dispatchKeyShortcutEvent
をオーバーライドすることによって、キーを押された際のイベントをハンドリングすることができます。
override fun dispatchKeyShortcutEvent(event: KeyEvent?): Boolean {
if (event?.keyCode == KeyEvent.KEYCODE_Z &&
event.hasModifiers(KeyEvent.META_CTRL_ON)) {
// Ctrl + z => undo
viewModel.onUndoKeyShortcut()
return true
} else if (event?.keyCode == KeyEvent.KEYCODE_Z &&
event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)) {
// Ctrl + Shift + z => redo
viewModel.onRedoKeyShortcut()
return true
} else {
return false
}
}
デフォルトで有効になっているキーボード操作
-
Escキー
とF1キー
は、スマホのバックボタンと同じ挙動になります。
疑似タッチスクリーンの許可
Chrome OS バージョン M53 以降では、android.hardware.touchscreen 機能を明示的に要求しないすべての Android アプリが、android.hardware.faketouch 機能をサポートする Chrome OS デバイスでも機能するようになりました。そのため、何も対応する必要はありません。
逆に、Chrome OS バージョン M52 以下では、デフォルトでAndroidアプリは android.hardware.touchscreen機能を必要とするため、タッチスクリーンがないChromebookだとインストールできないため、疑似タップインターフェースを備えた端末でもアプリを使用できるようにしたい場合はタッチスクリーンが必須ではないことを明示的に宣言する必要があります。
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
疑似タップインターフェースを備えたデバイスは、基本的なタップイベントをエミュレートする入力システムをユーザーに提供します。
たとえばユーザーは、マウスまたはリモコンを操作して、画面上のカーソルの移動、リストのスクロール、画面の一部から別の部分への要素のドラッグなどを行うことができます。
ChromeOS エミュレーターでのデバッグ方法
参考サイト:https://developer.android.com/topic/arc/emulator?hl=ja
別PCからのDebugについて
外部PCからChromebookに対して、adb接続してアプリをインストールしデバッグする方法です。
初期の状態は、ノーマルモードとなっており外部のPCからのファイル書き込みや、通信などなにもできない状況です。
主な手順
- ChromeOSの開発者モード化
- GooglePlayの利用規約に同意
- Android設定の開発者モード化
- ADBデバッグの許可
- 外部PCのAndroidStudioからの接続
- ChromeBookのIPアドレスの確認
- AndroidStudioでの確認
ChromeOSの開発者モード化
開発者モードに切り替えると、 "root" shellへのアクセスができるようになります。
ただし、切り替えると端末が再起動し、端末上のすべてのデータが消えます。
また、ノーマルモードに戻す際にも同じく端末が再起動し、端末上のすべてのデータが消えます。
PC内にあるデータは、GoogleDriveなどにしっかりと保存しておきましょう。
参考サイト:https://www.chromium.org/chromium-os/poking-around-your-chrome-os-device
リカバリーモード
デベロッパーモード
各起動後の注意として、毎回の起動時にはCtrl + Dを押して起動します。
でないと、ノーマルモードでの起動となりすべてのデータが消えます。
ノーマルモードへの切り替え方法
再起動後、Spaceキー を押して起動します。
ノーマルモードへの切り替え時もデータが消えるため、注意してください。
Android設定の開発者モード化
ADB デバッグを有効にするには、次のステップを実行します。
- 画面の右下部にあるクロック アイコンをクリックします。
- [Settings] をクリックします。
- [Android Apps] セクションで、[Manage your Android apps in Settings] という行にある [Settings] リンクをクリックします。これにより、Android アプリの設定が表示されます。
- [About device] をクリックします。
- [Build number] を 7 回クリックして、デベロッパー モードに移行します。
- ウィンドウの左上部にある矢印をクリックして、メインの [Settings] 画面に戻ります。
- 新しい [Developer options] アイテムをクリックし、[ADB debugging] をアクティブにし、[OK] をクリックして、ADB デバッグを有効にします。
外部PCのAndroidStudioからの接続
別のPCから、ChromebookにUSB接続してAndroidStudioでデバイス確認を行うと 100.115.92.2:5555
が表示されるがそれは外部PCからは接続できないので注意。
ChromebookのIPを確認する
ChromeOS情報のchronosユーザーのターミナル(ctrl+alt+T
+ shell)上で、「 ip -4 a 」と入力。
chronos@localhost / $ ip -4 a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: arcbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet 100.115.92.1/30 brd 100.115.92.3 scope global arcbr0
valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
inet 192.168.128.100/24 brd 192.168.128.255 scope global wlan0
valid_lft forever preferred_lft forever
6: vmtap0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
inet 100.115.92.5/30 brd 100.115.92.7 scope global vmtap0
valid_lft forever preferred_lft forever
一番下の、vmtap0のIPの5555番ポートで別のPCから adb connect
できる。
$adb connect 192.168.0.32:5555
あとは、いつも通り起動確認のダイアログがでるのでOKをクリックする。
ChromeOS上でのAndroidアプリの開発
主な手順
- ChromeOSの開発者モード化
- GooglePlayの利用規約に同意
- Android設定の開発者モード化
- ADBデバッグの許可
- (↑までは、ChromeOSでのアプリデバッグ方法を参照)
- Linuxの有効化
- AndroidStudioのインストール
- ファイアウォールの設定
- 端末のAndroidStudioからの接続
Linuxの有効化
参考サイト:https://developer.android.com/topic/arc/studio
AndroidStudioのインストール
ChromeOSでのAndroidStudioのインストール
AndroidStudioの起動
「 ./android-studio/bin/studio.sh 」
ファイアウォール設定
- Chromeブラウザ上で、 Ctrl+Alt+T を押してChrome OS端末を起動する
- 「 shell 」と入力して、bash コマンドシェルを開始する
crosh> shell
chronos@localhost / $
端末のAndroidStudioからの接続
Linuxの ターミナル
アプリを起動。
{username}@penguin adb kill-server
{username}@penguin connect_adb
connect_adb
は、AndroidStudioのインストール時に.bashrcに、以下のように登録されている関数。
function connect_adb() {
adb connect 100.115.92.2:5555
}
参考リンク
情報サイト
Androidアプリの作り方について
- Chrome OS デバイス
- Apps for Chrome OS overview
- Chromebook 向けアプリの最適化
- Get Your Android App Ready for Chromebooks
- Window management
- マルチ ウィンドウのサポート
- google/rally
- New Features in ConstraintLayout 2.0
- Responsive layout grid
- 入力とナビゲーション
- Chromebook 向けアプリ マニフェストの互換性
- AndroidNのDropPermissionsを使ってアプリケーション間でDragAndDropする
Google I/O
- What’s new in Android apps for Chrome OS (Google I/O '18)
- Android Apps for Chromebooks and Large Screen Devices (Google I/O '17)
Google Codelabs
セットアップ
開発者モード
Androidの開発環境
- Chromebook 向けアプリの最適化 セットアップ
- Chromebookで動作するAndroidアプリにadbを接続する方法
- ChromebookでAndroidアプリをデバッグする方
- How to configure your Chromebook to use ADB
- Connect Android USB Devices in Chrome OS
- Android Container Build