20200912のAndroidに関する記事は11件です。

Cookpad LicenseToolsPluginの横スクロール対策

クックパッドのAndroid向けのライセンス管理プラグイン cookpad / LicenseToolsPlugin
ライセンス表示ページ作る際にいつもお世話になってます!

今回はプラグインから出力されたHTMLで横スクロールされて困ってる人へ向けての対策。
※ 2020/03/19リリースのv1.2.2時点の対策になります

この問題はライセンスのURLが改行されずに行で表示されることが原因となっています。
Wrap html links in licenses.html #2

なのでURLを改行するように修正を行います。
Issuesを辿って旧プラグインのIssuesに行くと暫定対応の方法が書かれているのでそのとおりに対応すればOKです。
Wrap html links in licenses.html css #74

これで ./gradlew generateLicensePage のHTML出力時に改行設定を追加してくれます。

対策前 対策後
CookpadLicenseToolsPlugin_wrap_before.png CookpadLicenseToolsPlugin_wrap_after.png

私の場合は以下のようにプロジェクトに書いてます(Issuesの内容とほぼ同じです)

app/build.gradle

apply plugin: "com.cookpad.android.plugin.license-tools"
apply from: '../tools/cookpadLicensePatch.gradle'

tools/cookpadLicensePatch.gradle

// Wrap html links in licenses.html
// https://github.com/cookpad/LicenseToolsPlugin/issues/2
// https://github.com/cookpad/license-tools-plugin/issues/74
task patchLicensePageStyles() {
    doLast {
        def licensesHTMLFile = new File(project.rootDir, "/app/src/main/assets/licenses.html")
        try {
            def additionalStyles = "    a { word-break: break-all; }"
            def licensesHTML = licensesHTMLFile.getText('UTF-8')
            licensesHTML = licensesHTML.replaceFirst(/(\s*<\/style>)/, "\n" + additionalStyles + "\$1")
            licensesHTMLFile.text = licensesHTML
        } catch(Exception e) {
            println e
        }
    }
}
generateLicensePage.finalizedBy(patchLicensePageStyles)

リンク先と違うのはカスタマイズCSSファイルを使わず文字列で直接設定しているところ。
今回の対策に必要なCSSが短いのと複数ファイル管理が面倒なためです。

半角スペースが入っているのは出力された licenses.html のStyleのインデントを揃えたかっただけなので無くても問題ありません。

  <style>
    ...
    input:checked ~ .license {
    max-height: none;
    }
    a { word-break: break-all; }
  </style>

tools/cookpadLicensePatch.gradle はapp/build.gradleに直接書いて良いです。
私はbuild.gradleが大きくなりすぎないようにするためと、この設定はプロジェクトに依存しておらず他のプロジェクトでも使える、ということを表すために別ファイルに書くようにしています。


旧プラグインの修正PRがマージされていなかった理由が気になるところ。
修正されるといいな。

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

Inside Flutter Hooks

概要

Flutter Hooksを使う機会があり、すごい便利だなと思っていたのですが、
内部的にどんな風に実装されているのか掘り下げてみようかと思い、今回色々調べてみました。
(何か間違っていたりしたらコメントいただけると嬉しいです :bow: )

Flutter Hooks とは?

React hooksをFlutterで実装したものになります。

作者はProvider等でおなじみのRemiさんです。

サンプルの実行環境

flutter: v1.20.3
flutter_hooks: 0.14.0

Flutter Hooksの基本的な仕組み

useMemoized を掘り下げる

一番シンプルな useMemoized というhookを例にFlutter Hooksがどのような仕組みになっているのか追ってみたいと思います。

そもそも useMemoized とは?

useMemoized は何回ビルドが走っても初期値をキャッシュしてくれるhookです。

↓簡単なサンプルとして現在時刻を useMemoized でキャッシュし、Floating Action Button をタップする度に
カウンターが増えて再ビルドが走るようなサンプルを作成してみました。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final DateTime now = useMemoized(() => DateTime.now()); // 初期値として現在日時を保存
    final ValueNotifier<int> counter = useState<int>(0);
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Scaffold(
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  now.toString(),
                  style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
                ),
                Text(
                  counter.value.toString(),
                  style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
                )
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => counter.value++, // カウンターが増えWidgetのビルドが走る
            child: Icon(Icons.add),
          ),
        ));
  }
}

実行結果
android.gif (68.2 kB)

↑ counterの値が更新されても初期値として設定した現在日時の値は変更されていないのが分かるかと思います。
(2020-09-11 11:21:40.... の箇所)

useMemoized の実装は?

こちら で実装されています。以下に関連箇所だけ抜き出しました。

T useMemoized<T>(T Function() valueBuilder,
    [List<Object> keys = const <dynamic>[]]) {
  return use(_MemoizedHook(
    valueBuilder,
    keys: keys,
  ));
}

class _MemoizedHook<T> extends Hook<T> {
  const _MemoizedHook(
    this.valueBuilder, {
    List<Object> keys = const <dynamic>[],
  })  : assert(valueBuilder != null, 'valueBuilder cannot be null'),
        assert(keys != null, 'keys cannot be null'),
        super(keys: keys);

  final T Function() valueBuilder;

  @override
  _MemoizedHookState<T> createState() => _MemoizedHookState<T>();
}

class _MemoizedHookState<T> extends HookState<T, _MemoizedHook<T>> {
  T value;

  @override
  void initHook() {
    super.initHook();
    value = hook.valueBuilder();
  }

  @override
  T build(BuildContext context) {
    return value;
  }

  @override
  String get debugLabel => 'useMemoized<$T>';
}

より詳細に見ていこうと思います。:eyes:

useメソッド

先ずは useMemoized 内で使用されている use(_MemoizedHook(...useメソッド を掘り下げてみたいと思います。
この useメソッド は flutter_hooks/lib/src/framework.dart で以下の様に実装されています。

R use<R>(Hook<R> hook) => Hook.use(hook);

Hook というクラスのstatic メソッド use に引数のhook(ここでは_MemoizedHook)を渡して呼んでいます。
ここで Hook というクラスが出てきました。今度はこの Hook に着目したいと思います。

Hookクラス

Hookクラスはこちらに実装されています。
以下に要約したものを抜き出してみました。

@immutable
abstract class Hook<R> with Diagnosticable {
  const Hook({this.keys});

  @Deprecated('Use `use` instead of `Hook.use`')
  static R use<R>(Hook<R> hook) {
    assert(HookElement._currentHookElement != null, '''
Hooks can only be called from the build method of a widget that mix-in `Hooks`.
Hooks should only be called within the build method of a widget.
Calling them outside of build method leads to an unstable state and is therefore prohibited.
''');
    return HookElement._currentHookElement._use(hook);
  }

  final List<Object> keys;

  @protected
  HookState<R, Hook<R>> createState();
}

先程出てきた Hook.use に着目したいと思います。
@Deprecated となっていて直接 Hook.use は呼ばずに先程の useメソッド を呼ぶようにとなっています。

ここでは HookElement._currentHookElement._use(hook) が呼ばれており、
HookElement._currentHookElement は後でも出てきますが こちらにstatic変数として定義されています。
HookElement._currentHookElement._use は別途掘り下げるとして createState で生成される HookState を見てみます。

HookStateクラス

こちらに実装されています。こちらも要約したものを以下に抜き出してみました。

abstract class HookState<R, T extends Hook<R>> with Diagnosticable {
  @protected
  BuildContext get context => _element;
  HookElement _element;

  T get hook => _hook;
  T _hook;

  @protected
  void initHook() {}

  @protected
  void dispose() {}

  @protected
  R build(BuildContext context);

  @protected
  void didUpdateHook(T oldHook) {}

  void deactivate() {}
  void reassemble() {}

  @protected
  void setState(VoidCallback fn) {
    fn();
    _element
      .._isOptionalRebuild = false
      ..markNeedsBuild();
  }
}

こうして見ると HookHookState の関係が StatefulWidgetState の関係に似てますね :eyes:

HookState 内に先程出てきた HookElement を保持し、BuildContext としてgetできるようにしています。
後で出てきますが、HookElementComponentElement をimplementしているので BuildContext として扱う事ができます。
詳しくはこちらを参照して下さい。
FlutterのBuildContextとは何か - Qiita

HookElement mixin

こちらに実装されています。こちらも要約して抜き出してみました。
HookElementuse メソッドでの処理がFlutter Hooksのキモとなる処理になってきます。

mixin HookElement on ComponentElement {
  static HookElement _currentHookElement;

  _Entry<HookState> _currentHookState;
  final LinkedList<_Entry<HookState>> _hooks = LinkedList();
  LinkedList<_Entry<bool Function()>> _shouldRebuildQueue;
  LinkedList<_Entry<HookState>> _needDispose;
  bool _isOptionalRebuild = false;
  Widget _buildCache;

  @override
  Widget build() {
    // 色々な前処理 ...
    _currentHookState = _hooks.isEmpty ? null : _hooks.first; // ①
    HookElement._currentHookElement = this; // ②
    try {
      _buildCache = super.build();
    } finally {
      // 後処理 ....
    }
    return _buildCache;
  }

  R _use<R>(Hook<R> hook) {
    if (_currentHookState == null) {
      _appendHook(hook);
    } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) { // ③
      final previousHookType = _currentHookState.value.hook.runtimeType;
      _unmountAllRemainingHooks();
      if (kDebugMode && _debugDidReassemble) {
        _appendHook(hook);
      } else {
        throw StateError('''
Type mismatch between hooks:
- previous hook: $previousHookType
- new hook: ${hook.runtimeType}
''');
      }
    } else if (hook != _currentHookState.value.hook) {
      final previousHook = _currentHookState.value.hook;
      if (Hook.shouldPreserveState(previousHook, hook)) { // ④
        _currentHookState.value
          .._hook = hook
          ..didUpdateHook(previousHook);
      } else {
        _needDispose ??= LinkedList();
        _needDispose.add(_Entry(_currentHookState.value));
        _currentHookState.value = _createHookState<R>(hook);
      }
    }

    final result = _currentHookState.value.build(this) as R;
    _currentHookState = _currentHookState.next; // ⑤
    return result;
  }
}

先程 HookState で出てきた HookElement._currentHookElement が定義されています。

大まかな処理の流れ

  • _currentHookState

    • HookState の LinkedListになっておりビルド中に useXXX で呼ばれた際の各HookStateの一覧を呼ばれた順で保持しています
  • build メソッド

    • ① : 前回Widgetのビルドが走った際の HookStateのLinkedList のキャッシュがあれば _currentHookState にセットしています
    • ② : staticな領域に現在build中のHookElementをセットしています

    HookWidgetStatefulHookWidget クラスを使ったWidgetのビルドでは内部的に HookElement を使用しているので HookElementbuild() メソッドが呼ばれます。

  • use メソッド

    • ③ : 前回ビルドした時のHookと今回ビルド中のHookの runtimeType が異なっている場合
      • _currentHookStateに格納されているHookStateをすべてクリアします
      • Debug中の場合(開発しててuseXXXを変更した等)今回Hookを新たに格納します
    • ④ : 前回ビルド時のHookと異なるオブジェクトの場合 shouldPreserveState メソッドでKeyが前回と異なっているかチェックを行います
      • 異なっている場合
        • 一旦以前のHookStateは破棄して今回のHookStateに入れ替えます
      • 異なっていない場合
        • HookStatedidUpdateHook が呼ばれます
    • ⑤ : 次に備えて、_currentHookState.next で次のHookStateにLinkedList内の位置を移動させています

LinkedListを使用して前回ビルドのHookStateと比較する処理は flutter_hooksのREADMEにも載っていますが
React hooks: not magic, just arrays | by Rudi Yardley | Medium
こちらを読むとさらに理解が深まりそうでした。

useMemoized に立ち返って

ここで useMemoized 内で呼ばれていた Hook.use に立ち返ってみると HookElement._currentHookElement._use(hook) が呼ばれていました。

引数の hook_MemoizedHook が設定され use メソッドが呼ばれることになります。
_currentHookState がnullの場合(初めてWidgetビルド中にuseXXXが呼ばれた場合)は _appendHook が呼ばれてます。
_appendHook は何をしているかというと

extension on HookElement {
  HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) {
    assert(() {
      _debugIsInitHook = true;
      return true;
    }(), '');

    final state = hook.createState()
      .._element = this
      .._hook = hook
      ..initHook();

    assert(() {
      _debugIsInitHook = false;
      return true;
    }(), '');

    return state;
  }

  void _appendHook<R>(Hook<R> hook) {
    final result = _createHookState<R>(hook);
    _currentHookState = _Entry(result);
    _hooks.add(_currentHookState);
  }
}

HookクラスcreateState を呼び出して HookState を作成し _currentHookState に設定しています。
上記であった前回ビルド時のHookStateと比較する等の処理が終わったあと以下の処理が行われます。

    final result = _currentHookState.value.build(this) as R;
    _currentHookState = _currentHookState.next;
    return result;

ここで HookState の buildメソッドを呼び出して戻りuseメソッドの戻り値として返しています。
useMemoized の場合だと _MemoizedHookState の buildが呼ばれることになり、
_MemoizedHookState の buildは単に保存した値を返しているだけなので、いくらWidgetのビルドが走っても
更新されない保存された値を返し続けるという仕組みになっているようです :sparkles:

  @override
  T build(BuildContext context) {
    return value;
  }

HookWidget

最後にhookを使う側で必要な HookWidget クラスを見てみたいと思います。

abstract class HookWidget extends StatelessWidget {
  const HookWidget({Key key}) : super(key: key);

  @override
  _StatelessHookElement createElement() => _StatelessHookElement(this);
}

class _StatelessHookElement extends StatelessElement with HookElement {
  _StatelessHookElement(HookWidget hooks) : super(hooks);
}

すごいシンプルで、StatelessWidget クラスを継承し、Elementを生成する際に
HookElement を実装した _StatelessHookElement を返すようになっています。

また、StatefulWidget 版も用意されている様でした。

abstract class StatefulHookWidget extends StatefulWidget {
  const StatefulHookWidget({Key key}) : super(key: key);

  @override
  _StatefulHookElement createElement() => _StatefulHookElement(this);
}

class _StatefulHookElement extends StatefulElement with HookElement {
  _StatefulHookElement(StatefulHookWidget hooks) : super(hooks);
}

ここまで仕組みがどうなっているのか超ざっくり説明しました。

主な登場人物とざっくり相関図

これまでで登場してきたクラスやmixinを相関図にしてみました。

  • Hook
  • HookElement
  • HookState
  • HookWidget
  • StatefulHookWidget (※今回は省いています)

flutter_hooks相関図.png (48.9 kB)

※ 間違っていたらご指摘いただけると嬉しいです :bow:

他のhooks達

ここまでで何となくでも仕組みが理解できたので、他のhooksも見てみたいと思います。

useContext

実装はこちら
これは至ってシンプルで以下の様に実装されています。

BuildContext useContext() {
  assert(
    HookElement._currentHookElement != null,
    '`useContext` can only be called from the build method of HookWidget',
  );
  return HookElement._currentHookElement;
}

実装をみるとなぜbuild中じゃないと呼び出せないのか分かりますね :eyes:
ちなみに HookElement._currentHookElement が null になるタイミングはWidgetのビルドが終わったタイミングになります。

useEffect

実装はこちら

抜き出したもの
void useEffect(Dispose Function() effect, [List<Object> keys]) {
  use(_EffectHook(effect, keys));
}

class _EffectHook extends Hook<void> {
  const _EffectHook(this.effect, [List<Object> keys])
      : assert(effect != null, 'effect cannot be null'),
        super(keys: keys);

  final Dispose Function() effect;

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

class _EffectHookState extends HookState<void, _EffectHook> {
  Dispose disposer;

  @override
  void initHook() {
    super.initHook();
    scheduleEffect();
  }

  @override
  void didUpdateHook(_EffectHook oldHook) {
    super.didUpdateHook(oldHook);

    if (hook.keys == null) {
      if (disposer != null) {
        disposer();
      }
      scheduleEffect();
    }
  }

  @override
  void build(BuildContext context) {}

  @override
  void dispose() {
    if (disposer != null) {
      disposer();
    }
  }

  void scheduleEffect() {
    disposer = hook.effect();
  }

  @override
  String get debugLabel => 'useEffect';

  @override
  bool get debugSkipValue => true;
}

使い方としては useEffect 第一引数で渡された処理が初回呼ばれて、以降は第二引数のKeyに変更が無い限り
処理が呼ばれる事はありません。

    useEffect(() {
      print('useEffect');
      return () => print('dispose');
    }, const []);

↑の例だと第二引数のKeyに const [] を指定しているので初回だけしか print('useEffect'); は呼ばれません。
また第一引数の戻り値として終了処理を Function() として返せるのでもう一度処理が呼ばれる前にクリアさせたい等に使えそうです。

Keyが変更されるサンプルとして useMemoized のサンプルに useEffect を呼ぶ処理を追加してみました。

// ... 省略
class MyApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final DateTime now = useMemoized(() => DateTime.now());
    final ValueNotifier<int> counter = useState<int>(0);
    // ☆ここから追加
    useEffect(() {
      print('useEffect');
      return () => print('dispose');
    }, [counter.value]);
// ... 省略

↑のサンプルを実行し + ボタンをタップすると print('useEffect');print('dispose'); が呼ばれるのが分かるかと思います。
android2.gif (139.9 kB)

useEffectのしくみ

初回呼ばれる initHook 内で scheduleEffect メソッドを呼び出しています。
scheduleEffect メソッドがどうなっているかというと

  void scheduleEffect() {
    disposer = hook.effect();
  }

useEffect の第一引数で渡された Function() を 呼び出し戻り値の dispose を内部で保存しています。
disposeはここでは typedef Dispose = void Function(); として定義されています。
このタイミングで初回の処理を呼び出しています。

次に第二引数のKeyが変更された時点の処理を見てみたいと思います。

    } else if (hook != _currentHookState.value.hook) {
      final previousHook = _currentHookState.value.hook;
      if (Hook.shouldPreserveState(previousHook, hook)) {
        _currentHookState.value
          .._hook = hook
          ..didUpdateHook(previousHook);
      } else {
        _needDispose ??= LinkedList();
        _needDispose.add(_Entry(_currentHookState.value));
        _currentHookState.value = _createHookState<R>(hook);
      }
    }

既に説明した通り、HookElementuse メソッド内でKeyが変更されたかの判定を shouldPreserveState で行っており
Keyが変更されている場合、新たにHookStateを作り直しています。
作り直す際に initHook が呼ばれ内部で scheduleEffect を呼んでいます。
破棄された方のHookStateはbuildの最後で dispose が呼ばれ、内部で保持していた disposer() を呼び出しています。

useState

実装はこちら

抜き出したもの
ValueNotifier<T> useState<T>([T initialData]) {
  return use(_StateHook(initialData: initialData));
}

class _StateHook<T> extends Hook<ValueNotifier<T>> {
  const _StateHook({this.initialData});

  final T initialData;

  @override
  _StateHookState<T> createState() => _StateHookState();
}

class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
  ValueNotifier<T> _state;

  @override
  void initHook() {
    super.initHook();
    _state = ValueNotifier(hook.initialData)..addListener(_listener);
  }

  @override
  void dispose() {
    _state.dispose();
  }

  @override
  ValueNotifier<T> build(BuildContext context) {
    return _state;
  }

  void _listener() {
    setState(() {});
  }

  @override
  Object get debugValue => _state.value;

  @override
  String get debugLabel => 'useState<$T>';
}

こちらは既に useMemoized のサンプルで使ってましたが、こちらは ValueNotifier をラップし
扱いやすくしてくれているhooksになります。

useStateのしくみ

こちらはシンプルで initHook 時に ValueNotifier を生成し、build時には生成した ValueNotifier を返しています。

まとめ

基本的なhooksの仕組みを何となくでも把握しとけば、他のhooksもソースコードを読むことで
ある程度理解できるようになりました :sparkles:
今回のように一番シンプルなものから掘り下げていくのは余分なInputが少ない分理解しやすいですね。

内部的な処理が分かっていれば、useContext をWidgetのビルドタイミング以外で使用したらダメだとか
事前に分かるので、広範囲でお世話になるライブラリ等は事前に内部がどんな風になっているのか把握しておくと、
トータル的にはハマる時間が無くなってスムーズかもしれません :sparkles:

また次も機会があれば何か掘り下げようかと思います。

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

Firebase Dynamic Links 入門

Dynamic Links とは

Dynamic Links は、アプリのインストールの有無にかかわらず、複数のプラットフォームで機能するリンクです。iOS または Android で Dynamic Links を開くと、ネイティブアプリのリンク先のコンテンツに直接移動します。

ユースケース

  • ウェブユーザーをアプリユーザーにする
    Dynamic Links を開いたユーザーの iOS デバイスまたは Android デバイスにアプリがインストールされていない場合にインストールを促し、インストール完了後にアプリを起動してリンクを開く。

  • SNS、メール、SMS でのキャンペーン
    どのプラットフォームでも機能するリンクを使ってプロモーションの特典を提供する。iOS、Android、ウェブブラウザのいずれであっても、アプリをインストール済みかどうかにかかわらず、既存のユーザーも見込みユーザーも特典を利用できる。

Android アプリでDynamic Links を利用する

こちらのサンプルコードを参考にして、 Dynamic Links を試してみましょう。

FirebaseプロジェクトとAndroidアプリの準備

サンプルコードを入手する
GitHubからソースコードをローカル環境にクローンします。
今回は dynamiclinks ディレクトリのコードを使用します。

$ git clone https://github.com:firebase/quickstart-android && cd quickstart-android/dynamiclinks

Firebaseプロジェクトを作成する

次にサンプルアプリケーションをデプロイするために Firebase プロジェクトを作成します。

Android プロジェクトに Firebase を追加する

作成したFirebaseプロジェクトにクローンしたAndroidアプリを追加します。

Dynamic Links の設定

※ 以下の画像中で設定している値はグローバルで一意でなければならないので、実際に試す場合は別の値を使用してください。

URL prefix を設定する

Firebase コンソールの [Dynamic Links] から Add URL prefix を選択し、Domain を設定します。あとはデフォルト値のまま、VerifyFinish します。

Dynamic Links を作成する

続いて、New Dynamic Link を選択します。

  1. Set up your short URL link
    デフォルトで設定されている値を使用できます。

  2. Set up your Dynamic Link
    Deep link URL を設定します。

  3. Define link behavior for iOS
    今回は iOS アプリは使用しないので、デフォルト値のままにします。

  4. Define link behavior for Android
    deep link が Android アプリで開かれたときの挙動を設定します。
    今回は、別に用意している オブジェクト認識アプリ が起動されるように設定しました。

  5. Campaign tracking, social tags and advanced options (optional)
    デフォルト値のままにします。

ソースコードの編集をする

Android Studio を起動し、app/build.gradle の、dynamic_links_uri_prefix の値を、Set up your short URL link で設定した Link preview の値に書き換えます。

アプリを起動して Dynamic Links の挙動を確認する

Android Studioでアプリを起動すると、Emulator で下のような画面が開きます。
Java の OPEN を選択します。

Send の下に deep link が生成されました。

生成された deep link をコピーし、同じ Emulator デバイス内のブラウザで開きます。

Define link behavior for Android で設定したとおり、オブジェクト認識アプリが起動されました。

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

Compose by exampleの動画を見たメモ

概要

Jetpack Composeの実用的な使い方を知ることができる動画で、すごく色んな部分で面白かったです。

https://www.youtube.com/watch?v=DDd6IOlH3io

https://github.com/android/compose-samples にそれぞれのサンプルがあります。

Theming

Make the easy things easy and the hard things possible というのを大きく語っていました。Colorからダークテーマ対応までは Make the easy things easy にあたるようです。

color、typography、shapeを定義して作る

MaterialTheme(
  colors = ...
  typography = ...
  shapes = ...
) {
}

Color

Material color stytemのクラスで作る。

val colors = Colors(
  primary = ...
  primaryVariant = ...

他にもデフォルトやベースラインのカラーパレットから作るビルダー関数もある。
これを使うと変更したい色だけ変更して作ることができる

val colors = lightColors(
  primary = ...
  secondary = ...

複数のテーマがあるサンプルアプリのOwlではこのビルダー関数を使って何個もテーマを作っている。

Typography

Material Themeによってデフォルトが定義されている。
何もカスタマイズしたくなければ。そのまま使える。

val typography = Typography()

以下のようにTextStyleを使ってカスタマイズができる。

val typography = Typography(
  h1 = TextStyle(
    fontFamily = Rubik,
    fontSize = 96.sp,
    fontWeight = FontWeight.Bold,
    lineHeight = 120.sp
  )

Shape

small, medium, largeのサイズのコンポーネントのshapeを定義する。

val shapes = Shapes(
  small = ...
  medium = ...
  large = ...

角丸だとこう。

small = RoundedCornerShape(size = 4.dp)

左上をカットするにはこう。

small = CutCornerShape(topLeft = 16.dp)

テーマの推奨される適応方法

以下のようにしておいて、アプリ内で横断して使えるようにすることができる。

@Compose
fun YellowTheme(
  content: @Composable () -> Unit
) {
  MaterialTheme(
    colors = YellowLightColors,
    ...
  )
}

以下のようにすることで画面の一部だけテーマを変えることができる

fun CourseDetails(...) {
  PinkTheme {
    ...
    BlueTheme {
      RelatedCourses(...)
    }
  }
}

テーマの要素の使い方

型安全にそれぞれの要素にアクセスして利用できる。

Text(
  text = ...
  style = MaterialTheme.typography.subtitle1,
  color = MaterialTheme.colors.(ここで保管が出る)
)

色をコピーして利用することも簡単にできるので、色をハードコードしてしまうことを防げ、複数のテーマをサポートすることに役立つ。

val background =
MaterialTheme.colors.onSurface.copy(
  alpha = 0.2f
)
Surface(color = background) {...}

smart default

バックグラウンドの色を設定するとそれに対応するコンテンツの色が勝手につく。例えば以下ではcolor = primaryを設定しているので、コンテンツの色は勝手にonPrimaryの色になる。
同様にFloatingActionButtonなどでもこの仕組は利用できる。

Surface(color = MaterialTheme.colors.primary) {
  // ここでのデフォルトカラーは `onPrimary` になる。
}

(動画外):動画とは関係なくちょっと気になったので調べてみましたが、以下のようにcontentColorが作られるようです。

@Composable
fun Surface(
    modifier: Modifier = Modifier,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
...)

@Composable
fun contentColorFor(color: Color) =
    MaterialTheme.colors.contentColorFor(color).useOrElse { contentColor() }

また以下のようにAmbientでcontentColorが配られることで利用できるようです。 (ambientについてはこちら )

Providers(ContentColorAmbient provides contentColor, children = content)

ダークテーマ対応

isSystemInDarkTheme()を使って分けるだけ。簡単!

@Composable
fun PinkTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  if (darkTheme) PinkDarkColors else PinkLightColors
...

(動画外)isSystemInDarkTheme()の中ではこんな感じで判定している。

@Composable
fun isSystemInDarkTheme(): Boolean {
    val configuration = ConfigurationAmbient.current
    return (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration
        .UI_MODE_NIGHT_YES
}

ダイナミックテーマ

ここからは Make the easy things easy and the hard things possiblethe hard things possibleにあたる部分です。

ここではダイナミックテーマのサンプルとしてJetcasterというアプリの例で、画像から色を取得してそれをテーマとして使う例。

既存のPaletteライブラリを使って画像からdominantColorを取得して、それを使っている。
これでアニメーションもできる。(軽く書いているけどすごい。。)

val currentImage = ...
val palette = // Paletteライブラリを使って画像からpaletteを取得
val dominantColor = // paletteからdominant colorを取得

val colors = MaterialTheme.colors.copy(
  primary = animate(dominantColor),
)

MaterialTheme(colors = colors) {
  content()
}

Layout

  • Colum: 縦に並べる
  • Row: 横に並べる
  • Stack: 要素同士を上に重ねる (背景と上に乗るものみたいな)
  • ConstraintLayout: Androidエンジニアにはおなじみの制約でレイアウトするレイアウト

Modifierでクリックやpadding、toggleable、verticalScroll()、zoomable()なども使える。すごい。

カスタムレイアウトを作るには @Composable fun Layout()の使い方。

これを作るにはどうすればよいか。Layoutブロック使うとカスタムレイアウトを作れる

image.png
https://youtu.be/DDd6IOlH3io?t=869 より

長くなってしまったのでこの詳細は以下Qiitaに切り出しました。見てみてください。 :pray:
https://qiita.com/takahirom/items/c6625cbc7ebdda49de2f

Animation

シンプルなアニメーション

アニメーションなしでの書き方

val radius = if (selected) 28.dp else 0.dp
val shape = RoundedCornerShape(topLeft = radius)
Surface(
  shape = shape
...

アニメーションありでの書き方
変更したい値でanimte()を使うだけ。

val radius = animate(if (selected) 28.dp else 0.dp)
val shape = RoundedCornerShape(topLeft = radius)
Surface(
  shape = shape
...

シンプルな例を実装してみました。 ( https://github.com/takahirom/jetpack-compose-animation-sample )
simpleanimation.gif

@Composable
fun SimpleAnimation() {
    // 値を保持させている。変更されたときにrecomposeされる。
    var isRightState by remember { mutableStateOf(false) }
  // ここでanimate()を使う。animateは前回の値を持っており、それとの変更でアニメーションが走る。
    val leftMarginSize = animate(if (isRightState) 200.dp else 50.dp)
    Row(
        Modifier.fillMaxWidth()
    ) {
        Spacer(Modifier.width(leftMarginSize))
        Surface(
            color = Color.Green,
            modifier = Modifier.size(100.dp)
                .clickable(onClick = {
                    isRightState = !isRightState
                }),
        ) {}
    }
}

Transition

https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt#L156 にサンプルがあるのですが、
少し複雑なので、かんたんなサンプルを用意しました。

https://github.com/takahirom/jetpack-compose-animation-sample
92987062-10dad680-f4fa-11ea-97fa-2e2faade98e3.gif

// 状態を表すenumを用意する
private enum class BoxSelectedState {
    Selected, Unselected
}

// 変化させたい値のPropKeyを用意する (変数を大文字から始めているのはCompose内部のコードを真似ています)
private val LeftMargin = DpPropKey()
private val BoxColor = ColorPropKey()
private val ShapeCornerPropKey = DpPropKey()

// transitionDefinitionでtransitionDefinitionを定義する
private val BoxTransitionDefinition = transitionDefinition<BoxSelectedState> {
    state(BoxSelectedState.Selected) {
     // それぞれの状態ごとの値を設定する
        this[LeftMargin] = 50.dp
        this[BoxColor] = Color.Green
        this[ShapeCornerPropKey] = 0.dp
    }
    state(BoxSelectedState.Unselected) {
        this[LeftMargin] = 200.dp
        this[BoxColor] = Color.Red
        this[ShapeCornerPropKey] = 24.dp
    }
    // これはなくても動く、durationなどを設定したり、keyframe{} で進捗度が0.5のときにどれなどの設定もできる
    transition {
        LeftMargin using tween(durationMillis = 1000)
        BoxColor using tween(durationMillis = 2000)
        ShapeCornerPropKey using tween(durationMillis = 2000)
    }
}

@Composable
fun BoxTransitionAnimation() {
    var selectedState by remember { mutableStateOf(BoxSelectedState.Selected) }

    val transitionState = transition(
        definition = BoxTransitionDefinition,
        toState = selectedState
    )

    Row(
        Modifier.fillMaxWidth()
    ) {
        Spacer(Modifier.width(transitionState[LeftMargin]))
        Surface(
            // transitionState[]でアクセスする
            color = transitionState[BoxColor],
            shape = RoundedCornerShape(transitionState[ShapeCornerPropKey]),
            modifier = Modifier.size(100.dp)
                .clickable(onClick = {
                    selectedState = when (selectedState) {
                        BoxSelectedState.Selected -> BoxSelectedState.Unselected
                        BoxSelectedState.Unselected -> BoxSelectedState.Selected
                    }
                }),
        ) {}
    }
}

今後Android Studioにアニメーションのinspectorが追加される

今後のcanaryで以下のようにアニメーションをキーフレームで見られるような機能が追加されるみたい。
image.png
https://www.youtube.com/watch?v=DDd6IOlH3io より

アニメーションをスクショを撮ってテストできる

clockTestRuleというのにアクセスして、時間を操作でき、またスクショの比較などもできるので、それを使ってアニメーションのテストが可能なようです。

    private fun compareTimeScreenshot(timeMs: Long, goldenName: String) {
        // Start with a paused clock
        composeTestRule.clockTestRule.pauseClock()

        // Start the unit under test
        showAnimatedCircle()

        // Advance clock (keeping it paused)
        composeTestRule.clockTestRule.advanceClock(timeMs) // 時間を操作する

        // Take screenshot and compare with golden image in androidTest/assets
        assertScreenshotMatchesGolden(goldenName, onRoot()) // 保存されている画像とスクショを比較する
    }

https://github.com/android/compose-samples/blob/e70ba381ac4dfab0de6c6c357a3f00d9ada5cc2a/Rally/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt

まとめ

アニメーションの記述の容易さのように簡単なことは簡単にできてすごく書きやすいと感じました。
また難しいことも、カスタムのレイアウトが簡単に作れたり、Transitionの途中でスクショを撮って比較してテストとかこれまで考えられなかったのですが、そのようなことが可能になっているなどとても拡張性も高いと感じました。

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

PCに接続できない環境でもLogcatのログを取得したい時に使えるアプリ「Logcat Recorder」の紹介

背景

Androidアプリのテストで、Logcatに出力されたログを収集したい時、方法として

  • Android StudioのLogcatを利用する
  • 端末とPCをUSB接続するなどしておき、adb logcat -v time > log.txtコマンドを叩いておく

があると思います。
ですが、耐久試験で1日以上放置しないといけなかったり、PCの環境に不安がある場合に、上記の方法だとどうしても難しい場合もあります。
そんな時に使えるアプリ「Logcat Recorder」を紹介したいと思います。

アプリの概要

https://apkpure.com/jp/logcat-recorder/com.namakerorin.neko
からの引用です。

PCに接続できないような環境下やlogを常時取得しておきたい局面などで重宝します。

■概要
・logcat出力をSDカードなどのストレージに出力します (任意のパスに切り替え可能です)
・logcatの出力形式は"-v threadtime" です
・logファイルは時間(1分~6時間)とサイズ(500KB~100MB)で自動的に分割します
・Broadcast Intentをlogに出力します (Battery、Telephony、Configulation、Wi-fi、Bluetooth、Mediaなど)
・logの全体サイズの制限できます (100MB~4GB、制限なし)
・logを参照したり削除ができるファイラーがついています

Android端末の環境

環境は以下の通りです。

  • 端末名: Xperia 10 Ⅱ
  • OS: Android 10

導入方法

https://apkpure.com/jp/logcat-recorder/com.namakerorin.neko
からAPKファイルをダウンロードします。

1.png

ダウンロードしたら、テストしたい端末とUSB接続するなどしておき、以下のコマンドでインストール&権限付与します。

adb install 'Logcat Recorder_v1.0.2_apkpure.com.apk'
adb shell pm grant com.namakerorin.neko android.permission.READ_LOGS

導入後、アプリを起動すると、以下のような権限許可の画面が出てきます。

2.jpg

両方とも許可したうえで「続行」をタップします。
すると、以下のようなダイアログが出てきます。(うろ覚えですが、Android 8では出てこなかったと思います。どのバージョンから出るようになったかは不明)

3.jpg

いったん「OK」をタップして次に進みます。
導入はこれで完了です。

使い方

アプリを有効にするには、「Record logcat」のトグルをONにするだけです。

4.jpg

ONにすると、ステータスバーのところにアプリアイコンが表示されます。
このアイコンが表示されている間は、ログが保存され続けることになります。
OFFにすると保存が中断されます。

保存されたログの確認方法

「Log viewer」をタップすると、以下の画面に遷移します。

5.jpg

日時ごとにどのくらいのデータ量が保存されているかが確認できます。
チェックボックスをタップすると、画面が以下のように変化し、ゴミ箱ボタンをタップするとログを削除することができます。

6.jpg

日時のテキストをタップすると、さらにテキストファイルが表示されます。
先ほどと同様、チェックボックスをタップすると、画面が以下のように変化し、ゴミ箱ボタンをタップするとログを削除することができます。

7.jpg

また、ファイル名をタップすると別アプリからファイルの中身を閲覧することができます。(私の環境ではエラーが出て表示できませんでした。。)

保存されたファイル群をPCにコピーすることもできて、その場合は以下コマンドを実行します。

adb pull /storage/emulated/0/neko/ [PC上の保存したい場所]

まとめ

PCに接続できないような環境下や、耐久試験などでLogcatのログを取得し続けたいときに使えるアプリ「Logcat Recorder」を紹介しました。
ただし、Android 10以降ではうまく動作しない可能性がありますので注意してください。
(もっといい方法があれば教えてくださると嬉しいです)

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

【Android】VideoViewやExoPlayerを使って動画がうまく再生されない時は、動画形式を確認してみよう

背景

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

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

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

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

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

動画形式の調べ方

動画形式を調べる方法の一つとして、FFmpegを利用するというのがあります。
(他にはMediainfoを利用するという手もあります)
導入方法はHomebrewで以下コマンドを叩くだけです。

brew install ffmpeg

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

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

例えば、以下の動画をダウンロードして確認してみます。
https://bestvpn.org/html5demos/assets/dizzy.mp4

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

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

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

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

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

となっていて、形式が「H.264 AVC Baseline Profile(BP)」っぽいのでこれもサポートしている形式だなというのが分かります。
他にも解像度やフレームレートなどもわかるので、これらの情報を照らし合わせていきながらサポートされているかどうか判断します。
少々泥臭くはなるかもしれませんが、動画形式がサポートされていたかどうかというアプローチで問題を切り分けることができます。
サポートされていなければ実装は問題なさそう、サポートされていれば実装に問題ありそう、といった具合ですね。

まとめ

Androidで動画がうまく再生されないとき、場合によっては動画形式がサポートされていない可能性もありますよ、という話と、動画形式の調べ方についてFFmpegを使ったやり方を簡単に紹介しました。

参考URL

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

Androidで動画がうまく再生されない時は、動画形式を確認してみよう

背景

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

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

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

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

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

動画形式の調べ方

動画形式を調べる方法の一つとして、FFmpegを利用するというのがあります。
(他にはMediainfoを利用するという手もあります)
導入方法はHomebrewで以下コマンドを叩くだけです。

brew install ffmpeg

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

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

例えば、以下の動画をダウンロードして確認してみます。
https://bestvpn.org/html5demos/assets/dizzy.mp4

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

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

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

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

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

となっていて、形式が「H.264 AVC Baseline Profile(BP)」っぽいのでこれもサポートしている形式だなというのが分かります。
他にも解像度やフレームレートなどもわかるので、これらの情報を照らし合わせていきながらサポートされているかどうか判断します。
少々泥臭くはなるかもしれませんが、動画形式がサポートされていたかどうかというアプローチで問題を切り分けることができます。
サポートされていなければ実装は問題なさそう、サポートされていれば実装に問題ありそう、といった具合ですね。

まとめ

Androidで動画がうまく再生されないとき、場合によっては動画形式がサポートされていない可能性もありますよ、という話と、動画形式の調べ方についてFFmpegを使ったやり方を簡単に紹介しました。

参考URL

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

Androidエミュレータが起動しない場合の解決法メモ

どんな内容か?

Xamarinを利用していて、Androidの最近のVersionでテストする必要がありました。デバイスマネージャで新しいデバイスを追加したのですが、起動できずに困った話です。一応の解決方法が分かったのでメモしておくことにしました。同じ悩みを抱えた方の助けとなれば幸いです。全ての起動できない現象に一致する訳ではありません。

環境

OS:Windows10
開発環境:Visual Studio 2019 Community Version 16.7.3

現象

デバイスマネージャで、Andoird9.0(API28)のOSまでは起動するのですが、それ以上のAndroid10.0(API29)やAndroid11.0(API30)が起動できません。詳しく書くと、エミュレータのスマホ枠は表示されるのですが、1秒くらいですぐに消えてダウンしてしまう状況です。
image.png

試行錯誤の様子

※結論だけ知りたい方は飛ばしてください
設定(プロパティ)で音声を無効化したり、GPSを無効化したり、全部のプロパティをOFFにしてもダメ。
メモリを増やしたりいろいろ行っても全く効果ありませんでした。効果なし!
image.png

困ったときはログを確認・・・
したいのですが、Androidデバイスマネージャーから直接起動するとログがどこに出ているのかよくわかりません。
そこで、Android デバイスマネージャーから直接ではなくて、Xamarin(Visual Studio)側からエミュレータをキックしてもらうと少しログが見えました。(既に解決しているので、キャプチャーはないのですが・・・)

emulator: ERROR: VkCommonOperations.cpp:525: Failed to create Vulkan instance.
VCPU shutdown request
VCPU shutdown request
VCPU shutdown request
//(繰り返し)

エラーのキーワードとなりそうなのは上記のメッセージでした。
キーワードから、少しググって、こちらのstack overflowの記事にぶつかりました。

HAXM(Hardware Accelerated eXecution Manager)にたどり着きます。
自分の記憶では、自らインストールした記憶はありませんが昔のことなので分かりません。

HAXMの最新版を適用することで解決しました。
HAXMのインストール時に、ポップアップのメッセージで、「既にインストールされている6.0.2から7.6.1に上げて良いか?」と問合せがあったので、そこそこ古かったようです。

解決策

HAXMの最新のリリースをインストールし解決しました。
image.png
私の場合は、7.6.1を適用して解決しました。

ということで、エミュレータで起動しないOSがある場合は、HAXMの最新版を適用すると幸せになれるかもしれないということで。
現場からは以上です。

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

avdmanagerを実行し「Exception in thread "main" java.lang.NoClassDefFoundError」が発生した場合の対処法

FlutterでAndroidの開発環境を構築する際、avdmanagerをcliから実行すると下記のようなエラーが発生した際の備忘録です。

$ ~/Library/Android/sdk/tools/bin/avdmanager list
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
        at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
        at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
        at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
        at com.android.sdklib.tool.AvdManagerCli.run(AvdManagerCli.java:213)
        at com.android.sdklib.tool.AvdManagerCli.main(AvdManagerCli.java:200)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 5 more

使用するJavaのバージョンをAndroid Studioに同梱されているJavaに変更することで解決しました。

# パスを通す
$ echo 'export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"' >> ~/.zprofile 
$ source ~/.zprofile

# 確認
$ ~/Library/Android/sdk/tools/bin/avdmanager list
Available Android Virtual Devices:
Parsing ~
 :
 :

Android Studioに同梱されているJAVAのパスは、アプリを立ち上げcmd + ; → 「JDK location」から確認が可能です。

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

【Android】XMLに1行追加でImageButtonにエフェクトを付加する

ImageButtonにおいてandroid:background="@null"としてしまうと,タップ時のエフェクトが表示れません。
「imagebutton エフェクト」で検索すると割と手間がかかる方法がヒットするのですが,
別にエフェクトのデザインに拘らない場合,レイアウトファイルに1行追加するだけで簡単にimagebuttonにエフェクトを追加することができます。
ImageButtonの養素に以下を追加するだけです

android:background="?android:attr/selectableItemBackgroundBorderless"

もし,背景を既に設定して変えたくない場合,foregroundも指定できます。

以下,サンプルです

<ImageButton
    android:id="@+id/menuSetting"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:background="?android:attr/selectableItemBackgroundBorderless"
    android:src="@drawable/ic_baseline_settings_24"/>

ImageButtonEffect.gif

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

AWS AmplifyでS3のBucket名を取得する [メモ]

はじめに

基本は自分のための備忘録だが、誰かの参考になれば幸い。
なお、クイックハックです。正式なやり方ではないと思うのでご注意を。

動機

AWSのAmplifyを使ってモバイルアプリを作ることになったが、使いやすい半面、小回りがきかなそうで困ってる。

すごいざっくりのAmplify概要

Amplifyは、CLIから簡単にバックエンドを構築できて、そのバックエンドへのアクセス方法をライブラリが隠蔽するので、開発者はその詳細を知らなくてもアクセス出来るというところがメリット。

また、Dev -> Prodの2Stageで開発するときに、アプリで使うバックエンドを下記のコマンドでDev用、Prod用に切り替えることが出来る。当然、開発者はバックエンドの詳細を知らなくてもよい。

$ amplify env checkout <stage>

困ってること

詳細が隠蔽されているので、バックエンドのリソース名が取れない(ようにみえる)。
例えば、バケット名を表示したいと思っても、のバケット名を取得する方法がない(ようにみえる)。

具体的に困っているのは、Amplifyを使ってS3にアップロードしたファイルを、Amplifyで作成したAPI-Gateway -> Lambdaからアクセスしたい。だが、Lambdaが直接Amplifyが作成したS3のBucketの名前を取得する方法がない(こちらも、ようにみえる)。なので、アプリからAPI-Gatewayを叩くときに、バケット名を渡してやる必要がある。必要があるのだが、上記のようにバケット名を取る方法がない。こまるー。
→Lambdaで使う場合、バケットにアクセスすることを明示しておけば、環境変数に入れてくれるようでした。。Amplify ディスってごめんよ。一応、以下、何かのときのために残しておく。

方式案

方式案1:設定ファイルから読み込む。

上述したamplify env checkout <stage>でバックエンドを切り替えると、/res/raw/amplifyconfiguration.jsonの内容が書き換わる。これにより、開発者は詳細を知らずにバックエンドを切り替えてアプリを動かすことが出来る。

なので、こいつをパースしてやれば良い。

方式案2:android-amplifyをカスタマイズしちゃう。

https://github.com/aws-amplify/amplify-android からcloneして、ソースコード書き換えちゃおうぜ。という方法。

方式案1 vs 方式案2

どちらも、正式な方法じゃないと思われる。思われるが、おそらく方式案1のほうがよろしかろうと思われる。

でも、どうせ正しくないのだから、敢えて方式案2でやってみる。(^_^:)/

ちなみに、正しい方法は、featureリクエスト/pullリクエストを出して実装してもらうことだ。

手順

githubからクローン

$ git clone https://github.com/aws-amplify/amplify-android.git -b release_v1.3.1
$ git checkout -b release_v1.3.1

arrを作成

プロジェクトを開く

プロジェクトをオープンする。モジュールがいろいろ入っているが、トップのプロジェクトをオープンしましょう。

image.png

必要なソースコード編集を行う。

今回は、aws-storage-s3com.amplifyframework.storage.s3.AWSS3StoragePlugin.javaをいじります。(ここでは、すごく適当にやってますが、ご参考頂く方々はもう少しマトモに配慮いただけると信じてます。)

インスタンス変数に下記を追加。

    public String regionStr;
    public String bucket;

configureメソッドのローカル変数をコメントアウト

 public void configure(
            JSONObject pluginConfiguration,
            @NonNull Context context
    ) throws StorageException {
//        String regionStr;
//        String bucket;

ビルド

Android Studioの右にGradleのタスク一覧を出せるウィンドウがあるので1、buildを選んで実行しましょう。

image.png

なんか、エラーがちらほら出ますが、s3のモジュールには出てないようなので、今回は無視します。build/outputs/arr/aws-storage-s3-release.aarが作成されていたらOKです。

arrをインポート

それでは、作成したarrを自分のアプリにインポートします。
image.png

ファイル名に先ほど作成したarrを指定します。

image.png

これにより、Open Modules settingsから依存関係を設定できるようになります。なるはずです、、、なりませんね。おかしい。。。(バグかな? 使っているのはv4.0.1です。)

ということで、自力でbuild.gradleを書き換えます。dependenciesに下記を追加します。

    implementation project(path: ':aws-storage-s3-release')
    implementation 'com.amazonaws:aws-android-sdk-s3:2.18.0' // カスタムのaws-storage-s3-releaseを使うときに必要。理由は不明。

以上で、pluginからバケット名にアクセスすることが出来るようになりました。下のような文を追加して、正しく出力されるか確認してみましょう。

val plugin = Amplify.Storage.getPlugin("awsS3StoragePlugin") as AWSS3StoragePlugin
Log.e(TAG,"Bucket  ${plugin.bucketName} ${plugin.regionStr}")

最後に

普通にjsonファイルをパースしたほう(方式案1)が早そうですね。ただ、これまでAmplifyを使った結果、今後もいろいろ小回りがきかなくて困りそうだったので、カスタマイズすることを前提にarrのインポートをするという手段をとってみました。

どなたかのご参考になれば幸いです。(自己責任でお願いします。)


  1. デフォルトでは右端の細長いところにあるのですが、最初これが見つからなくて困りました。AndroidStudioは機能が多すぎて大変です。。。 

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