20190211のAndroidに関する記事は7件です。

Material Component LibraryのMaterialButtonのtextAllCapsをテーマで変更する

AppCompatButton同様、MaterialButtonもデフォルトでは textAllCaps="true"で全て大文字で表示されます。

AppCompatButtonの場合は、以下のようにテーマを指定すれば小文字にすることができました。

<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
    <item name="buttonStyle">@style/AppButton</item>
</style>

<style name="AppButton" parent="Widget.AppCompat.Button">
    <item name="android:textAllCaps">false</item>
</style>

MaterialButtonの場合は、指定する属性が少し変わるので注意が必要です。
具体的には、materialButtonStyle という新しい属性を指定します。

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <!-- buttonStyleではなくmaterialButtonStyleを指定すること -->
    <item name="materialButtonStyle">@style/AppButton</item>
</style>

<style name="AppButton" parent="Widget.MaterialComponents.Button">
    <!-- MaterialComponentで定義された属性を指定するのでandroid:は不要 -->
    <item name="textAllCaps">false</item>
</style>

以上です。

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

three.jsをスマホアプリで動かして暖をとる(react-native-webglの導入手順)

はじめに

この記事では、react-native-webglというライブラリの導入手順について書きます。

react-native-webglは、React NativeWebGLを使うためのライブラリです。
これを使えばネイティブアプリでthree.jsが使えるようになるので、試しに作ってみました!

とりあえずスマホカイロにしてみましたが、誰かかっこいいアプリ作ってくれないかな・・・

iOS: https://itunes.apple.com/us/app/three-js-native/id1447928256
Android: https://play.google.com/store/apps/details?id=com.three

Simulator Screen Shot - iPhone 8 Plus - 2019-02-06 at 20.52.51.png Simulator Screen Shot - iPhone 8 Plus - 2019-02-06 at 21.02.12.png

  • アプリ内の4種類のシーンのうち、Octahedronをたくさん表示するものが一番あったまれます。
  • segments(ポリゴンの分割数)などのパラメータは、最大にするとメモリが足らずアプリが落ちることがありますので、徐々に上げてみてください。

React Nativeの準備

公式のgetting-started通りに準備しましょう。
今回はexpoを使わないので、”Quick Start”ではなく、”Building Projects with Native Code”のタブに従ってください。

react-native-webglのインストール

React Nativeの最新はこの記事の執筆時点で0.58ですが、このバージョンではビルドが通らないため、0.57にします。

# init
react-native init threejsNative
cd threejsNative

# react-nativeをv0.57に下げる
yarn remove react-native
yarn add react-native@0.57

# ネイティブのプロジェクトも新しく作り直す
rm -rf ios android
react-native eject

その後、react-native-webglのパッケージをインストールしてリンクします。

yarn add react-native-webgl
react-native link react-native-webgl

iOS

New Build SystemではGPUImageというプロジェクト周りでエラーが出てしまうため、Legacy Build Systemに変更します。

File -> Workspace Settings... -> Build System -> Legacy Build System

Screen Shot 2019-02-08 at 1.35.54.png
Screen Shot 2019-02-08 at 1.36.12.png

Android

issueを読みつつ諸々を書き換えます。

android/build.gradle
  buildscript {

    ext {

-     minSdkVersion = 16
+     minSdkVersion = 17

    }

    dependencies {

+     classpath 'de.undercouch:gradle-download-task:3.1.2'

    }
node_modules/react-native-webgl/android/src/main/jni/Application.mk
- APP_PLATFORM := android-9
+ APP_PLATFORM := android-16

- APP_STL := gnustl_shared
+ APP_STL := c++_shared

- NDK_TOOLCHAIN_VERSION := 4.9
node_modules/react-native-webgl/android/build.gradle
  task packageRNWebGLLibs(dependsOn: buildRNWebGLLib, type: Copy) {

-   exclude '**/gnustl_shared.so'
+   exclude '**/libc++_shared.so'

  }

  android {

-   buildToolsVersion '25.0.0'
+   buildToolsVersion '27.0.3'

  }

  dependencies {
-   compile "com.facebook.react:react-native:+"  // From node_modules
+   implementation "com.facebook.react:react-native:+"  // From node_modules
  }

exclude '**/libc++_shared.so'でビルドが通らない時はinclude '**/libc++_shared.so'でうまくいく場合があります。

three.jsを読み込む

あとは公式のexampleの通りです。

yarn add three
three.js
const THREE = require("three");
global.THREE = THREE;
if (!window.addEventListener)
    window.addEventListener = () => { };
require("three/examples/js/renderers/Projector");
export default THREE;
App.js
import React from "react";
import { View } from "react-native";
import { WebGLView } from "react-native-webgl";
import THREE from "./three";

export default class App extends React.Component {
  requestId: *;
  componentWillUnmount() {
    cancelAnimationFrame(this.requestId);
  }
  onContextCreate = (gl: WebGLRenderingContext) => {
    const rngl = gl.getExtension("RN");

    const { drawingBufferWidth: width, drawingBufferHeight: height } = gl;
    const renderer = new THREE.WebGLRenderer({
      canvas: {
        width,
        height,
        style: {},
        addEventListener: () => {},
        removeEventListener: () => {},
        clientHeight: height
      },
      context: gl
    });
    renderer.setSize(width, height);
    renderer.setClearColor(0x000000, 1);

    let camera, scene;
    let cube;

    function init() {
      // ここにcameraやsceneのコードを書く
      camera = new THREE.PerspectiveCamera(75, width / height, 1, 1100);
      camera.position.y = 150;
      camera.position.z = 500;
      scene = new THREE.Scene();

      let geometry = new THREE.BoxGeometry(200, 200, 200);
      for (let i = 0; i < geometry.faces.length; i += 2) {
        let hex = Math.random() * 0xffffff;
        geometry.faces[i].color.setHex(hex);
        geometry.faces[i + 1].color.setHex(hex);
      }

      let material = new THREE.MeshBasicMaterial({
        vertexColors: THREE.FaceColors,
        overdraw: 0.5
      });

      cube = new THREE.Mesh(geometry, material);
      cube.position.y = 150;
      scene.add(cube);
    }
    const animate = () => {
      this.requestId = requestAnimationFrame(animate);
      renderer.render(scene, camera);

      // ここにアニメーションのコードを書く
      cube.rotation.y += 0.05;

      gl.flush();
      rngl.endFrame();
    };

    init();
    animate();
  };
  render() {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <WebGLView
          style={{ width: 300, height: 300 }}
          onContextCreate={this.onContextCreate}
        />
      </View>
    );
  }
}

three.jsの部分は通常とほとんど同じように書けます!

実行

iOS/Androidそれぞれでオブジェクトが表示されれば成功です。

react-native run-ios
react-native run-android

まとめ

react-native-webglを使って、スマホアプリでthree.jsを動かす方法について書きました。
three.jsを使ったかっこいいアプリがさらに増えていくと嬉しいですね!

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

【Android】DroidKaigi 2019 まとめ

DroidKaigi とは

エンジニアが主役のAndroidカンファレンス。
DroidKaigi 2019 は 2019年2月7日(木)、8日(金)の2日間開催。

以下が公式サイトのリンクです。
DroidKaigi 2019 公式サイト

チケットがないと入場できません。ちなみにチケット代は以下でした。
・ 早期割引: 10,000円
・ 一般: 15,000円
・ 学生: 4,000円

高いと感じるかもしれませんが、パーティの参加費も含まれています。お昼のお弁当も出ました。

タイムテーブル

各セッションの詳細は公式サイトにタイムテーブが載っています。
公式サイトタイムテーブル

各セッションの発表資料を公開していたのでまとめさせていただきました。
リンクがあるところが公開されている資料を見つけられたセッションです。

1日目

Hall

Hall A Hall B
ウェルカムトーク
マルチモジュールなプロジェクトでテストはどう変わる?
マルチモジュールプロジェクトでの Dagger2を用いた Dependency Injection Optimize Builds with Android Plugin for Gradle 3.3.0+
Fireside Chat

Room1

時間 セッション名
11:20 What is Navigation Architecture Component and Tips
12:50 LiveData と Coroutines で実装する DDD の戦術的設計
14:00 PWAでここまでできる
14:50 Chrome Custom Tabsの仕組みから学ぶプロセス間通信
15:40 Androidエンジニアが抑えておくべきUnicode Emojiの知識
16:30 From Monolithic to Modularized codebase with Dagger

Room2

時間 セッション名
12:50 EspressoのテストをAndroidの最新トレンドに対応させよう
14:00 ぼくのかんがえた最強のUsecaseの作り方 ~あるいはビジネスロジックとはなにかという1つの回答~
14:50 R8、Proguard徹底比較
16:00 詳解定期購入
17:10 シームレスに遷移可能な画面を他のアプリに提供する方法

Room3

時間 セッション名
11:20 Unit test for ViewModel and LiveData
12:50 マテリアルデザインの起源とベースとなる哲学
14:00 Master of Android Theme
14:50 The good and bad of modern app architecture
15:40 Understanding Kotlin Coroutines: コルーチンで進化するアプリケーション開発
16:50 クロスプラットフォームモバイルアプリ開発ツール総ざらい2019 〜Titanium MobileからKotlin/Nativeまで〜

Room4

時間 セッション名
12:50 アプリをさらに成長させるための技術戦略(振り返りとこれから)
14:00 つらいと評判のAndroid BLEを頑張って使い続けた話
14:50 いかにしてビットコインを扱うか
15:40 ちゃんとつくる Google Assistant アプリ
16:30 クロスプラットフォーム開発3種の神器 React Native

Room5

時間 セッション名
12:50 Codelabs: Android Fundamental Course

Room6

時間 セッション名
12:50 Server-side Kotlin for Frontend 複雑なAndroidアプリ開発に対するアプローチ
14:00 FCMを使った用途に合わせたPush通知設計
14:50 Chromebookで始めるラップトップ向けAndroidアプリ
16:00 Redux for Android
17:10 Chrome + WebAuthn で実現できるパスワードレスなユーザー認証体験と開発者の課題

Room7

時間 セッション名
12:50 Deep Dive to fido.fido2 Packages
14:00 Dexs, R8 & 3.2
14:50 Exploring the Android Transform API
15:40 Grid Systems And Android
16:40 ExoPlayer in RecyclerView(*), a proposal

2日目

Room1

時間 セッション名
10:30 Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上
11:20 Spek2+MockK+JaCoCoでイケてる Unit Test環境を手に入れろ!
12:50 Deep dive into MotionLayout
14:00 Android Studio設定見直してみませんか?
14:50 今日から始める依存性の注入
15:40 Androidにおけるパフォーマンスチューニング実践
16:50 Navigation Architecture Component によるアプリ内遷移の管理
17:40 実践 WorkManager
18:30 Android アプリ開発における、デザイナーとエンジニアのワークフロー

Room2

時間 セッション名
10:30 中規模以上のアプリ開発におけるCIレシピとリリースフロー戦略
11:20 Guide to app architectureを踏まえた既存アプリの設計改良
12:50 Trash Talk
14:00 SpekでUnitTestを書こう
14:50 ハマった時に役立つ通信系デバッグの話
15:40 Multi-module Androidアプリケーション
16:50 デザイナーとエンジニアの距離をより近づけるLottie利用術
17:40 AndroidThingsでのプロダクト開発
18:30 build.gradle.ktsに移行しよう

Room3

時間 セッション名
10:30 Not Just Rotation: Configuration Changes on Android
11:20 Gradle BOM importでライブラリバージョン管理
12:50 Best practice for text on Android and its internals.
14:00 AndroidVitals徹底活用
14:50 Lifecycle, LiveData, ViewModels - The inner wiring
15:40 Journey of APK from compilation to launch
16:50 UIテスト(Espresso)の高速化をさらにすすめる
17:40 ああ、素晴らしきTDD ~アプリとエンジニアの心に安寧を~
18:30 巨大なアプリ開発を支えるフラグ管理術

Room4

時間 セッション名
10:30 React NativeとExpoを用いたクロスプラットフォーム開発入門
11:20 Slice Your App: Inside Slices and How to build it
12:50 カスタムブラウザの作り方 ー 実例から学ぶ Chrome と Firefox のカスタムビルド開発 ー
14:00 FridaによるAndroidアプリの動的解析とフッキングの基礎
14:50
15:40 WiFi Direct + VpnServiceでSIM無しAndroidをWeb世界に社会復帰させる話
16:50 WebView+ViewGroupを実現するAOSPメールアプリの内部実装とニュースアプリへの応用
17:40 WebViewを守るSafe Browsingのコントロール
18:30 Google Play Consoleのリリーストラックを有効活用してリリースフローの最適化を行った話

Room5

時間 セッション名
10:30 Android App Improvement Challenge Part1: 機能実装編
12:50 Android App Improvement Challenge Part 2: リファクタ編
15:00 Codelabs: Free Theme

Room6

時間 セッション名
10:30 外部デバイスと密に連携するAndroidアプリに最適なアーキテクチャとは?
11:20 Wi-Fi RTTによる屋内測位アプリを作ろう
12:50 All About Test of Flutter
14:00 実践Lottie
14:50 Android Enterpriseで実現できる端末管理の世界
15:40 ゼロから実装する縦書きTextViewとその周辺技術
16:50 2019年の技術であのARアプリを再現する
17:40 FlutterでのWidgetツリーへの状態伝播とアクセス制限の基本戦略
18:30 BLEアプリ設計パターン

Room7

時間 セッション名
10:30 From TensorFlow to ML Kit: power your Android application with machine learning
11:20 Code Review as a Collaborative Journey
12:50
14:00 What does "adb lolcat" do? A deep dive into adb
14:50 Troubleshooting your designer's, and vector graphics
15:40 Sharing Code between iOS & Android with Rust
16:50 Fast Prototypes with Flutter + Kotlin/Native
17:40 Building for next billion
18:30 Animations in Flutter

動画

各セッションでは動画撮影をしていました。
後日に公開されるということで、この取り組みも素晴らしいと感じました。
Youtube DroidKaigiチャンネル

終わりに

私自身、今回が DroidKaigi 初参戦!
セッションをしていただいていた方々は企業のエンジニアでしたが、企業の宣伝はほとんどないところが素晴らしかったです。

全てのセッションの資料を見つけようと思いましたが、見つけられませんでした。
力不足ですいません(。>_<。)

見つけることができたら更新します。

おまけ

個人的に良かったのは、パーティに出てきたドロイド君のケーキです(もちろん参加者は食べることができます)
↓写真を貼っておきます。
cake.jpeg

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

RxJavaのintervalでonPauseで停止、onResumeで再開させる

やりたいこと

Observable.intervalを使って5秒おきにViewを操作させるアプリがあったとする。
アプリがバックグラウンドになればView操作を停止、フォアグラウンドならView操作を再開させたい。
これをRxJavaで実装してみた。

コード

private AtomicBoolean resumed = new AtomicBoolean();
private AtomicBoolean stopped = new AtomicBoolean();

private void start() {

    Observable.interval(5, TimeUnit.SECONDS)
      .observeOn(AndroidSchedulers.mainThread())
      .takeWhile(new Predicate<Long>() {
          @Override
          public boolean test(Long aLong) throws Exception {
              return !stopped.get();
          }
      })
      .filter(new Predicate<Long>() {
          @Override
          public boolean test(Long aLong) throws Exception {
              return resumed.get();
          }
      })
      .subscribeWith(new DisposableObserver<Long>() {

          @Override
          public void onNext(Long aLong) {
              logger.d("onNext " + aLong);
              // ここに動作させたい処理
          }

          @Override
          public void onError(Throwable e) {
              logger.e(e.getMessage());
          }

          @Override
          public void onComplete() {
              logger.d("onComplete");
          }
    })
}

@Override
public void onResume() {
    super.onResume();
    resumed.set(true);
    stopped.set(false);
    start();
}

@Override
public void onPause() {
    super.onPause();
    resumed.set(false);
    stopped.set(true);
}

感想

希望する動作をしてくれてはいるが、本当はKotlin+Coroutineで実装したかった。
今回はJavaという制約があったので仕方ない。

参考リンク

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

Flutterウィークリー #45

Flutterウィークリーとは?

FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/

この記事は#45の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-45

※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。

読み物&チュートリアル

Flutterスプライトシートアニメーション

https://medium.com/flutter-community/sprite-sheet-animations-in-flutter-1b693630bfb3


Luan NicoがFlutterアプリでスプライトシートを使用する方法を紹介します。

紹介: Flutterウィジェット-メーカー、 FlutterのApp-Builderはで書かれたFlutter

https://medium.com/flutter-community/introducing-flutter-widget-maker-a-flutter-app-builder-written-in-flutter-231e8d959348


Norbertは、ビジュアル環境でウィジェットを作成するためのこのツールを作成しました。

クイックヒント:テキストURLをクリック可能なハイパーリンクに変換する

https://medium.com/flutter-community/quicktip-converting-text-urls-into-clickable-hyperlinks-2bb2d3b3b4b2


Darshan KawarがFlutterアプリのURLをリンクするための簡単なコツを紹介します。

dartfmtを使用してコードスタイルを確認してフォーマットする

https://medium.com/@guitcastro/checking-and-format-your-code-style-using-dartfmt-665cc4189223


dartファイルでリンターを実行し、CI経由でGuilherme Torresで実行する方法についての簡単なメモ

Flutter - 開発期間を短縮するためのIDEショートカット

https://medium.com/flutter-community/flutter-ide-shortcuts-for-faster-development-2ef45c51085b


AS用のショートカットのリストをチェックして、 Flutter by Pooja Bhaumikを使った作業を改善してください。

FlutterとWebコードの共有州管理デモ

https://medium.com/flutter-community/flutter-and-web-code-sharing-a-state-management-demo-2e615d3f2b1a


Mellati Meftahが、 Flutter状態を処理しながら、 FlutterとWebの間でコードを共有する方法を紹介します。

FlutterのWebViewのパワー

https://medium.com/flutter-io/the-power-of-webviews-in-flutter-a56234b57df2


FlutterチームのEmily FortunaによるWebView Widgetの詳細な分析。

キャッチャーでFlutterエラーを処理する

https://medium.com/flutter-community/handling-flutter-errors-with-catcher-efce74397862


Jakub Homlala氏は、アプリケーションのエラー処理を支援するための、 FlutterプラグインのCatcherを紹介しています。

アーキテクチャなし

https://buildflutter.com/no-architecture/

例としてFlutterを使用したアーキテクチャを使用しない、Adam Pedleyによるモバイル開発への興味深いアプローチ。

Flutter速い!

https://medium.com/flutter-community/flutter-faster-db1e0fef57ba


Greg Perryは、MVCアーキテクチャをFlutterアプリに適用した経験を共有しています。

Firebase、クラウドストレージ、 Flutter - Flutter Pub - Medium

https://medium.com/flutterpub/firebase-cloud-storage-and-flutter-fa2e91663b95


あなたのFirebase Cloud StorageにFlutterアプリからファイルを保存する方法に関するAseem Wangooによるチュートリアル。

BuiltValueSerializerを使用したカスタムbuilt_valueシリアライザの作成

https://medium.com/@solid.goncalo/creating-custom-built-value-serializers-with-builtvalueserializer-46a52c75d4c5


カスタムシリアライザを作成するGonçaloPalmaによるbuilt_valueの高度な使用法に関する記事。

フレアの逆運動学 - 2次元 - 中

https://medium.com/2dimensions/inverse-kinematics-in-flare-777bbb24bc49


Guido Rossoを使用すると、FlareでのIKを理解して、 Flutterアプリに適したアニメーションを作成できます。

要素、キー、 Flutterのパフォーマンス

https://medium.com/flutter-community/elements-keys-and-flutters-performance-3ef15c90f607


ウィジェットのキーを処理するとパフォーマンスがどのように向上するかについてのTomekPolańskiによる記事。

Flutter + MLKit =❤

https://medium.com/flutter-community/flutter-mlkit-8039ec66b6a


Stefan BlosによるFlutter MLKitの使用に関するチュートリアル

ビデオ&メディア

Flutterローカル認証指紋とFaceID | Dartパッケージ - YouTube

https://www.youtube.com/watch?v=4-P_Su9O5NM


ローカル認証パッケージを使用してFlutterアプリケーションに指紋認証とFaceIDローカル認証を簡単に導入する方法についてのビデオ。

Flutter YouTube検索チュートリアルコース

https://www.youtube.com/playlist?list=PLB6lc7nQ1n4jtXh6TgCEIO4kCfIT0-NZl


Flutter YouTubeの検索機能を模倣する方法についてのReso Coderによるプレイリスト。

Flutterインスペクタを使用してスクロール位置を保存する(The Boring Flutter Development Show、Ep。15) - YouTube

https://www.youtube.com/watch?v=ht76lDzPgUQ&feature=youtu.be&linkId=63306304


The Boring Flutter Development Showのこのエピソードでは、EmilyとLaraが、 Flutter Inspectorと、それがどのように使用されるか、そしてページのスクロール位置を維持する方法について話します。

Flutter UI - クリーンデザイン - ヘアスタイリストアプリ - YouTube

https://www.youtube.com/watch?v=td_wyIn9b3k&feature=youtu.be


今回はヘアスタイリストアプリである、アプリuiの作成に関するRaja Yoganによるチュートリアル。

整列(今週のFlutterウィジェット) - YouTube

https://www.youtube.com/watch?v=g2E7yl3MwMk&t=0s&index=26&list=PLOU2XLYxmsIL0pH0zWe_ZOHgGhZ7UasUE


整列ウィジェットを使用すると、ウィジェットをその親ウィジェットの定義済み領域に配置できます。

折りたたみサイドバーとナビゲーション引き出し| Flutter UI - YouTube

https://www.youtube.com/watch?v=2SjvhAUR9aw&feature=youtu.be


Techie Blossomによる、引き出しとして、または足場本体のどこにでも使用できる折りたたみ式の引き出し/サイドバーの作成方法に関するビデオ。

Flutter使ってGmailを作成する

https://www.youtube.com/playlist?list=PLWIO1jq0WronOimzw5BGlB3F9TU7Celpd


Impatient DeveloperによるFlutter Gmailクローンの作成に関するプレイリスト。

Flutterチュートリアル - Flutter GestureDetectorとInkWell - YouTube

https://www.youtube.com/watch?v=pAA62_x0zKE&feature=youtu.be


Whatsupcodersは、GestureDetectorとInkwellをアプリで使用する方法を私たちに示しています。

Flutterトーク

https://blog.codemagic.io/flutter-talks-podcast-fast-beautiful-productive-open/


Flutter専用の新しいPodcast。この最初の号では、Martin Aguinisへのインタビューやそれ以上のことを聞くことができます。

ライブラリ&コード

mdi-dart

https://github.com/csharad/mdi-dart

Flutter用の自動生成されたMaterial Design Iconパッケージ。

データビュー

https://github.com/synw/dataview

アプリケーションのドキュメントディレクトリ用のファイルエクスプローラ。

flutter_clipper_experiments

https://github.com/spagni/flutter_clipper_experiments

カスタムクリッピング実験をテストするためのFlutterアプリ。

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

Chromebookで始めるラップトップ向けAndroidアプリ開発

はじめに

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の特徴

AndroidからみたChromebookの特徴

ChromeOSとAndroidの差分について

ChromeOS -> Androidアプリに合わせている部分

ChromeOS->Android

Androidアプリ -> ChromeOSに合わせている部分

Android->ChromeOS

以下のページで、ChromeOS独自の対応について説明

  • 04_ウィンドウ・レイアウトサポート
  • 05_入力サポート

ChromeOSでのAndroidアプリのデバッグについて

デバッグ全体像

以下で説明

  • 01_ChromeOSのアプリデバッグ方法
  • 02_ChromeOS上でのAndroidアプリの開発

Chromebook向けのアプリの対応として、マルチウィンドウ、フリーフォームレイアウトの対応について、複数サイズの画面の対応について記載。

ウィンドウ・レイアウトサポート

Chromebookの画面に関する特徴

ChromebookではPCと同様に、複数ウィンドウの立ち上げと、ウィンドウサイズの変更できることが特徴となります。

chromeos_window_support

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が縦画面固定の設定をしていても、この設定は無視されます。また、ウィンドウサイズもすでにあるスタックの属性をそのまま受け継ぎます。

root_activity_rule

デバイスモードの場合の注意:
タブレットモードの向きはロックされず、Android通常の画面回転と同様に処理されます。
ただし、targetSdkVersionがAndroid 6.0(API level 23)以下の場合はロックされます。

別ウィンドウで立ち上げたい場合

Activityの立ち上げを別スタックにすればよいため、IntentのIntent.FLAG_ACTIVITY_NEW_TASKFlagを指定することで、別ウィンドウ立ち上げが行える。

Androidのマルチウィンドウの種類について

マルチウィンドウでは、Activityを別ウィンドウで立ち上げることによる別画面で情報を同時に見せたり、アプリ内の別画面や、複数のアプリの間でドラッグ&ドロップによるデータの共有を行うことができます。

マルチウィンドウは、Android 7(Nouger)で大きく変更が入り、特にPCやTV向けのサポートが多くありました。
複数のウィンドウを表示するには、主に以下のタイプがあります。

スマホ Chromebook
フリーフォーム x
分割画面 x
ピクチャーインピクチャー x

Chromebookでは、フリーフォームのウィンドウのみ対応となる。

Chromebookのウィンドウサイズの種類

screen_size

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をサポートしている必要があります。

drag_and_drop

ドラッグする側の処理

  1. ドラッグするデータの作成
    • ClipDataクラスの作成
    • 指定できるデータと、それぞれのMIMETYPE
      • テキスト:ClipDescription.MIMETYPE_TEXT_PLAIN
      • HTMLテキスト:ClipDescription.MIMETYPE_TEXT_HTML
      • URL:ClipDescription.MIMETYPE_TEXT_URILIST
  2. Drag中のViewの指定
    • View.DragShadowBuilderクラスを使用する
  3. 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_STARTEDDragEvent.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に対しては権限が付与されないためユーザーにとっても安心して利用できる機能になっています。

drag_and_drop_permission

ドラッグする側の処理

View.startDragAndDrop() の第4引数のフラグをOR(|)で連結して指定します。
それぞれ、権限周りのフラグは以下の通りです。

  1. 他アプリとのDrag許可
    • View.DRAG_FLAG_GLOBAL
  2. URIへの権限許可 ※1の指定必須
    • View.DRAG_FLAG_GLOBAL_URI_READ:URIに対する読み込み権限を付与
    • View.DRAG_FLAG_GLOBAL_URI_WRITE:URIに対する書き込み権限を付与
  3. URIへの権限許可オプション系 ※2の指定必須
    • View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION:revokeUriPermissionするまで権限を付与する
    • View.DRAG_FLAG_GLOBAL_PREFIX_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の例:

materialdesign_shrine.gif

ライフサイクル

ウィンドウサイズ変更時のライフサイクル

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を使用した画面変更時のアニメーションのサポートについて書きます。

  1. ConstraintLayoutのバージョンを2にアップデート※まだ、beta段階
  2. レイアウトファイルの修正
  3. constraint_states.xml ファイルの作成
  4. ConstraintLayoutの設定と、ステート変更のハンドリング
  5. 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_debug

入力サポート

Chromebookの入力周りのサポートについて

入力ソース

input_method

  • タッチ
  • スタイラスペン
  • トラックパッド
  • マウス
  • キーボード

トラックパッド/マウスサポート

マウスカーソルサポート

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単位でタブキーのハンドリングを行う方法が追加されました。

画面の構成を以下の図のように、ナビゲーションクラスタ(タブキーで移動させたいグループ)ごとにまとめます。

keyboard_cluster

まとめた各クラスタの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 デバッグを有効にするには、次のステップを実行します。

  1. 画面の右下部にあるクロック アイコンをクリックします。
  2. [Settings] をクリックします。
  3. [Android Apps] セクションで、[Manage your Android apps in Settings] という行にある [Settings] リンクをクリックします。これにより、Android アプリの設定が表示されます。
  4. [About device] をクリックします。
  5. [Build number] を 7 回クリックして、デベロッパー モードに移行します。
  6. ウィンドウの左上部にある矢印をクリックして、メインの [Settings] 画面に戻ります。
  7. 新しい [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

ファイアウォール設定

  1. Chromeブラウザ上で、 Ctrl+Alt+T を押してChrome OS端末を起動する
  2. 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アプリの作り方について

Google I/O

Google Codelabs

セットアップ

開発者モード

Androidの開発環境

その他

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

kotlinでAndroid端末内の画像を含むフォルダを検知する

画像系のアプリを作ってる時に端末内の画像を含むフォルダを表示させたかったので作成

ストレージの許可

端末の設定からアプリのストレージ設定を弄らないと内部ストレージにアクセスできない?らしい
詳しくはhttps://direct.fujixerox.co.jp/ap1/sc/beat/ja/support/020211.html

内部ストレージのパスを渡す

内部ストレージのパスをImgFileconクラスのimgfileserchに渡す

MainActivity.kt
package com.example.myapplication

import android.os.Bundle
import android.support.design.widget.BottomNavigationView
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import com.example.myapplication.R.attr.content
import android.content.Context
import android.os.Environment

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /*内部ストレージのパス取得*/
        val path = Environment.getExternalStorageDirectory().getPath()
        val dir  = File(path)

      /*内部ストレージのパスを渡す*/
      val imgserch=ImgFilecon()
      imgserch.imgfileserch(dir.toString())
    }
}

画像を含むフォルダを探す

渡されたパス内のディレクトを再帰的に探索してファイルを探す。見つけたファイルが画像の拡張子(.jpgなど)を持っていたらそのファイルのあるディレクトリのパスを出力
余計な画像が出てきてしまうためAndroidフォルダはリジェクト

ImgFilecon.kt
package com.example.myapplication

import android.os.Parcel
import android.os.Parcelable
import java.io.File

class ImgFilecon() {
   /*画像の含まれるディレクトリを再帰的に探すメソッド*/
    tailrec fun imgfileserch(passname: String): Unit {

       var flag = false
       val dir = File(passname)

       if (dir.listFiles() != null) {
           /*渡されたパスを探査*/
           loop@ for (i in dir.listFiles()) {

            /*ファイルの時の処理*/
            if (i.isFile()) {
              val ext = Extension()
               /*画像ファイル判定*/
               if (!flag) {
                 when (ext.extensionsplit(i.toString(), ".")) {
                   "jpeg", "JPEG", "jpg", "JPG", "png", "PNG", "gif", "GIF" -> {
                           println("画像の含まれるディレクトリのパス" + dir)
                           flag = true
                           }
                           else -> {}
                       }
                   }
               }

             /*ディレクトリの時の処理*/
             else if (i.isDirectory) {
                   val extdir = Extension()
                   /*Androidディレクトリは余計な画像が含まれるので表示しない*/
                   when (extdir.extensionsplit(i.toString(), "0/")) {
                       "/Android" -> {
                           continue@loop
                       }
                       else -> {
                      /*再帰*/
                        imgfileserch(i.toString())
                       }
                   }
               }

           }
       } else println("null")
   }
}

画像ファイルを判断するために拡張子切り出し

パスと区切り文字を渡すと区切り文字より下の文字列を返すメソッド。今回はファイルパスと区切り文字"."を渡して拡張子を取得した。

Extension.kt
package com.example.myapplication

import android.os.Parcel
import android.os.Parcelable

class Extension {

    /*区切り文字で分割するメソッド*/
     fun extensionsplit(filename: String, splitps: String): String {

        /*splitpsで指定した文字のindex*/
         val point = filename.lastIndexOf(splitps)

         if (point != -1) {
             /*pointより下の部分を切り出す*/
             /* 例 xxxx.jpg→jpg*/
             return filename.substring(startIndex = point + 1)
         }
         else return null.toString()
     }
}

デモ

スクリーンショット 2019-02-11 7.04.09.png
こんな感じで端末内の画像を含むフォルダ一覧がAndroidStudioのRunに表示される

参考

ストレージへのアクセス許可:https://direct.fujixerox.co.jp/ap1/sc/beat/ja/support/020211.html

終わりに

Github:https://github.com/tf0101/MyApplication4

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