20190307のAndroidに関する記事は14件です。

FlutterでローカルからNotification

はじめに

Push通知ではなくアプリケーション上から通知を出すためにどうするのか調べました。

環境

Android Studio
Flutter1.0

今回はiOSを表示させる環境用意しなかったので、Androidのみに言及しています。
iOSだと動かない箇所が出てくると思います。

Dependencyの追加

Flutterには通知を簡単に実現するためのflutter_local_notificationsが用意されているので、それをpubspec.yamlに追加してあげます。
バージョンがまだ0.5.1なので、バージョンアップ後に動かなくなる可能性がありますので、この先読む方はご注意ください。

dependencies:
  flutter_local_notifications: ^0.5.1

アプリ側のトリガーを元に通知を行う

アプリ側のイベントを契機に通知を行う方法について見ていきます。

まずは必要となるpackageをimportしてあげます。

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

次からはState<T>内部に実装する必要があるため、必要な実装は先に行なっておきます。
私はFlutterを勉強しながら継ぎ足し製法でソースを拡充させているため、それを使います。
なお、色んなコードが入ってきてしまっているため、本ページでは全体のコードはのせませんがご了承ください。

State<T>の実装が完了したらメンバ変数としてFlutterLocalNotificationsPluginを追加します。
様々なサンプルだとメンバ変数になってませんが、これを使って実際の通知を行う必要があるため、メンバ変数としないと無理です。

FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
NotificationDetails platformChannelSpecifics;

まずは通知を行うための初期化処理を行います。
初期化はState<T>initState内に実装します。
ここでの注意点はサンプルをコピーしただけではエラーとなるため、実装が必要になる部分があることです。

また、実際の通知する際に使用するオブジェクトもここで生成しておいた方が良いです。
AndroidNotificationDetailsのコンストラクタはAndroid8.0以上のチャンネル機能用の通知用があるので、8.0以上を目的で作る場合は必須となります。

  @override
  void initState() {
    super.initState();
    _refresh();

    flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    var initializationSettingsAndroid = AndroidInitializationSettings('app_icon');
    var initializationSettingsIOS = IOSInitializationSettings(
        onDidReceiveLocalNotification: onDidReceiveLocationLocation);
    var initializationSettings = InitializationSettings(
        initializationSettingsAndroid, initializationSettingsIOS);
    flutterLocalNotificationsPlugin.initialize(initializationSettings,
        onSelectNotification: onSelectNotification);

    var androidPlatformChannelSpecifics = AndroidNotificationDetails(
        'your channel id', 'your channel name', 'your channel description',
        importance: Importance.Max, priority: Priority.High);
    var iOSPlatformChannelSpecifics = IOSNotificationDetails();
    platformChannelSpecifics = NotificationDetails(
        androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
  }

そして必要な箇所を追加します。
onDidReceiveLocationLocationはiOS用のようなので、本ページに必要ない実装ですがなぜか実装してあったので、合わせてのせておきます。

onSelectNotificationについてが今回重要な点となります。
このメソッドは通知をクリックした際に呼ばれるイベントとなっているため、その動作について実装が必要となります。

今回はNavigator.pushを使用して別のWidgetを呼び出してます。
そもそもNotificationからの起動時は今の画面の次の画面として起動するか、一から起動してから次の画面として起動するかという動作をする感じなので、Future.syncなどからWidgetを作っても現状では動作がどう変わるかが良くわかりませんでした。

何かパラメータを渡す際は引数のpayloadを使って行う必要があります。
ここはString固定なので、複雑なデータを渡す際はJsonなんかで渡す必要があります。

  Future onSelectNotification(String payload) async {
    await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => Text(payload),
        maintainState : false,),
    );
  }

  Future onDidReceiveLocationLocation(
      int id, String title, String body, String payload) async {
    await showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text(title),
            content:  Text(body),
            actions: <Widget>[
              FlatButton(
                child: Text(payload),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          );
        }
    );
  }

最後に実際に通知を行う箇所です。
initStateで生成したオブジェクトを使用して通知します。
onSelectNotificationに渡ってくるpayloadpushpayloadで設定したものと同じものなので、通知後にパラメータを渡したい場合はここで設定してあげてください。

  Future _onNotification() async {
    await flutterLocalNotificationsPlugin.show(
        0, 'plain title', 'plain body', platformChannelSpecifics,
        payload: 'item id 2');
  }

これで通知できるようになりました。

通知をスケジューリングする

アプリケーショントリガーですぐに実行することが出来るようになりましたが、そのトリガーを元にすぐに出さずにあとで出すようにしたい場合にスケジューリングさせることが出来ます。
その場合にどうするべきかを見ていきます。

AndroidManifest.xmlの修正

スケジューリングされた通知を行うのにAlarmManager使用しているらしく、それが使用できるようにManifestを修正してあげる必要があります。
修正内容は公式書いてあるので、その通りに追加してあげます。

AlermManagerを使用している場合はアプリが起動していなくても起動してくれるので、停止状態でも出せるようになります。

なお、com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiverを追加するとAndroid Studio上でエラーになりますが、特に問題ないので無視してください。

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />

dartファイル

コード的にはscheduleメソッドを使用して、時間を指定あげるだけの違いとなります。

  Future _onNotification() {
    return flutterLocalNotificationsPlugin.schedule(
        0, 'plain title', 'plain body', DateTime.now().add(Duration(seconds: 5)),
        platformChannelSpecifics, payload: 'item id 2');
  }

繰り返し通知

Manifestの修正はしておいてください。
コード的にはperiodicallyShowを使えば実現できます。
指定するRepeatIntervalはenumなので、簡単に繰り返す期間の指定が可能です。

  Future _onNotification() {
    return flutterLocalNotificationsPlugin.periodicallyShow(
        0, 'plain title', 'plain body', RepeatInterval.EveryMinute,
        platformChannelSpecifics, payload: 'item id 2');
  }

特定の時間で繰り返す

毎日定時に起動させたい場合はshowDailyAtTimeを使用します。

  Future _onNotification() {
    var date = DateTime.now().add(Duration(seconds: 5));
    return flutterLocalNotificationsPlugin.showDailyAtTime(
        0, 'plain title', 'plain body', Time(date.hour, date.minute, date.second),
        platformChannelSpecifics, payload: 'item id 2');
  }

特定の曜日、特定の時間で繰り返す

特定の曜日も追加したい場合はshowWeeklyAtDayAndTimeで曜日と時間を指定します。

  Future _onNotification() {
    var date = DateTime.now().add(Duration(seconds: 5));
    return flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime(
        0, 'plain title', 'plain body', Day.Sunday, Time(date.hour, date.minute, date.second),
        platformChannelSpecifics, payload: 'item id 2');
  }

おわりに

本当は起動時にNotificationを設定する方法も合わせて見ていたのですが、没としました。
今の所は状態変化のタイミングに合わせて通知設定をするというのが一番良いです。
面倒だから起動と同時に設定できるようにしたいなーと思ってたんですが、しょうがないので別の方法考えます。

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

Android Test Night #6 内容まとめ

はじめに

Test NightコミュニティのAndroid Test Night 第6回目(2019/3/7)のまとめです。

本イベントはAndroidにおけるテスト周りに関する知識を共有することを目的としたものです。

テスト周りに関するものであれば何でもOKです。 例をあげるとすれば以下のようなものなどです。

  • テストをはじめてみた&ここで苦労した
  • このテスティングフレームワークはここがハマりどころ
  • テスティングフレームワークをこうやって使い分けている
  • こうやって工夫してテストしている
  • オレが考えるAndroidアプリにおけるテスタビリティの高い設計
  • 弊社のCI/CD環境はこんな感じにしている
  • DroidKaigiで話せなかったネタをここで

ハッシュタグは#android_test_nightです!

LT

モバイルテスターの世界戦向けトレーニング - Curiosity and Knowledge

スピーカー

  • mkwrd

内容

  • SeleniumConf Tokyoの宣伝
    • 日本初開催
      • 4月18日、19日
    • テスト自動化の標準となりつつあるSeleniumを学ぼう
    • 160以上のCFPがきた。セッションの選定に時間かけた。
    • 参加者は海を超えて
    • 自分のレベルを図れる
    • 「エンジニアの戦場は世界だ」
  • モバイルテスターの世界向き要素
    • モバイルアプリ開発は世界の潮流
    • 世界にでるのにモバイルテスターほど適した資格はない
  • モバイルテスターの資格
    • ISTQB
      • JSTQBは日本向けの加盟組織
      • AGILE,CORE,SPECIALISTなどたくさんある
      • ASTQB
        • ISTQB以外の資格も扱う。
        • MobileTesting資格を独自に実施
          • CBTなので日本でも受験できる

資料

AzureDevOpsで始めるAndroidのCI/CD

スピーカー

  • nakasho

内容

  • Azure DevOps

    • 組織のOwnerを簡単に切り替えられる。お客さんへの納品が簡単
    • 料金
      • 3360円
      • Free
        • 5ユーザまで無料って?
          • 無料枠利用者5人まで + VSサブスクライバーは人数制限なし + 編集権限なしの人も制限なし
    • AzureRepos
      • 容量無制限のGitリポジトリがもらえる
      • PJに複数のリポジトリ作成が可能
      • ブランチごとに権限設定可能
    • AzurePipeline
      • AzureReposGit,GitHub,Bitbucket、SVNなど使える
      • ビルドテンプレートもいろいろ。今回はAndroidの説明
      • AndroidSigningの設定すればビルドのあとに署名もしてくれる
      • AppCenterTestやDistributeの設定もできる
      • 毎月1800分まで無料
      • 自前でAgentをインストールすることも可能。これつかえば時間の制限なし

資料

Make CI/CD More Comfortable Before/After Code Testing

スピーカー

  • red_fat_daruma

内容

  • モチベーション
    • テストは開発プロセスで重要。
  • Definition

    • 開発を加速させるもの
  • エラーに対して柔軟でないものは悪

    • badパターン
      • Lintが落ちたらテストが実行されないCI
        • Lintはテストを止めるものではない
      • 一つのタスクが失敗したらすべてが失敗する
  • これらを解決させるにはコマンドのステータスを確認して必要に応じてCI/CDを失敗させる

    • プロダクションではエラーは落とす必要のあるがステージングでは実行されてほしい
    • Gradle tasksのcontinueオプションを使う
    • Jcneterのリトライロジックを入れる
    • Dangerのようなレポートシステムを使う。テスト失敗の内容を知りたいから。
  • ジョブの分け方。このように分けるのが理想

    • VitalJob
    • TestJob
    • DeploymentJob
  • ビルドの中間結果を保存・ロードする

    • メジャーなCIサービスにはキャッシュシステムがある。
    • 依存関係のキャッシュ
  • CIサービスのキャッシュの使いどころ

    • 単一PRでのビルド間キャッシュ
      • CIのキャッシュシステムは使うべきではない
      • ビルドの中間情報はGithubイシューとHTMLを利用する
    • 複数PR上でのビルド間キャッシュ
      • 最適なソリューションがわからないので解決策があれば教えて欲しい
  • DeployGate

    • CDN通してるのでダウンロード速度が速い
      • CIでデプロイしたらレスポンスをカールで叩くようになっている。よって人が初回ダウンロードするときにはキャッシュつかえる
    • DistributionTracks
      • GooglePlayStoreの内部トラックのようなバージョン別ダウンロードページ
    • dpg
      • 非公式のCLIベースDeployGateAPIクライアント

資料

形式手法について調べてみた

スピーカー

  • altitude3190

内容

  • 仕様の不備に開発フェーズの速い段階で気づきたい
  • あとになればなるほど手戻り工数が大きい
  • 改善はしていたが場当たり的な対応、工数・スキル的に手に負えなかった
  • 形式手法
    • 仕様を明確に記述
    • 機械的に検証する
    • 数学に基づく科学的な裏付けがある
  • 形式仕様記述
    • VDM++/Event-B/Zなど
  • 定理証明
    • Coq
  • モデル検査
    • Alloy/Promela
    • システムを有限個の状態を持つモデルで表現
    • モデルの状態を機械的網羅的に検査
    • Spinはモデル検査ツールでHomebrewでインストール可能
    • 判例があればトレースとともに表示
  • メリットデメリット
    • 形式手法メリット
      • 検査結果から仕様不備に気付ける
    • デメリット
      • 導入コスト
      • モデル化が難しい場合がある
    • 部分化導入がよい
  • 現場にマッチした形式手法がどれになるか吟味する必要がある

資料

UI テストで楽するための技術

スピーカー

  • ksfee

内容

  • UIテストは大変
    • 環境構築が大変
    • 実行時間がかかる
    • 実行結果の確認
    • 依存が多い
    • 実装が大変
  • 楽する技術
    • テストケースをDSLで
    • テスト結果
      • Composer
        • ログとスクリーンショットを合わせたレポートを提供
    • ネットワーク依存
      • OkReplay
        • OkHTTPの通信をRecord・Replay
        • レスポンスデータの管理が楽
        • ワイルドカードの表現が使えない。動的なリクエストが難しい

資料

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

Android Test Night #6 内容まとめ(随時更新)

はじめに

Test NightコミュニティのAndroid Test Night 第6回目(2019/3/7)のまとめです。

本イベントはAndroidにおけるテスト周りに関する知識を共有することを目的としたものです。

テスト周りに関するものであれば何でもOKです。 例をあげるとすれば以下のようなものなどです。

テストをはじめてみた&ここで苦労した
このテスティングフレームワークはここがハマりどころ
テスティングフレームワークをこうやって使い分けている
こうやって工夫してテストしている
オレが考えるAndroidアプリにおけるテスタビリティの高い設計
弊社のCI/CD環境はこんな感じにしている
DroidKaigiで話せなかったネタをここで

ハッシュタグは#android_test_nightです!

LT

モバイルテスターの世界戦向けトレーニング - Curiosity and Knowledge

スピーカー

  • mkwrd

内容

  • SeleniumConf Tokyoの宣伝
    • 日本初開催
      • 4月18日、19日
    • テスト自動化の標準となりつつあるSeleniumを学ぼう
    • 160以上のCFPがきた。セッションの選定に時間かけた。
    • 参加者は海を超えて
    • 自分のレベルを図れる
    • 「エンジニアの戦場は世界だ」
  • モバイルテスターの世界向き要素
    • モバイルアプリ開発は世界の潮流
    • 世界にでるのにモバイルテスターほど適した資格はない
  • モバイルテスターの資格
    • ISTQB
      • JSTQBは日本向けの加盟組織
      • AGILE,CORE,SPECIALISTなどたくさんある
      • ASTQB
        • ISTQB以外の資格も扱う。
        • MobileTesting資格を独自に実施
          • CBTなので日本でも受験できる

資料

AzureDevOpsで始めるAndroidのCI/CD

スピーカー

  • nakasho

内容

  • Azure DevOps

    • 組織のOwnerを簡単に切り替えられる。お客さんへの納品が簡単
    • 料金
      • 3360円
      • Free
        • 5ユーザまで無料って?
          • 無料枠利用者5人まで + VSサブスクライバーは人数制限なし + 編集権限なしの人も制限なし
    • AzureRepos
      • 容量無制限のGitリポジトリがもらえる
      • PJに複数のリポジトリ作成が可能
      • ブランチごとに権限設定可能
    • AzurePipeline
      • AzureReposGit,GitHub,Bitbucket、SVNなど使える
      • ビルドテンプレートもいろいろ。今回はAndroidの説明
      • AndroidSigningの設定すればビルドのあとに署名もしてくれる
      • AppCenterTestやDistributeの設定もできる
      • 毎月1800分まで無料
      • 自前でAgentをインストールすることも可能。これつかえば時間の制限なし

資料

Make CI/CD More Comfortable Before/After Code Testing

スピーカー

  • red_fat_daruma

内容

  • モチベーション
    • テストは開発プロセスで重要。
  • Definition

    • 開発を加速させるもの
  • 柔軟なCI

  • bad

    • Lintが落ちたらテストが実行されないCI
      • Lintはテストを止めるものではない
    • 一つのタスクが失敗したらすべてが失敗する
  • プロダクションでは落とす必要のあるがステージングでは実行されてほしい

  • Gradletasksのontinueオプション

  • Jcneterのリトライロジックを入れる

  • Dangerはテストが失敗してもしなくても実行してほしい

  • ジョブの分け方

    • VitalJob
    • TestJob
    • DeploymentJob
  • ビルドの中間情報はGithubイシューやHTMLを利用する

  • CIサービスのキャッシュの使いどころ

    • PRでは使うべきではない
  • DeployGate

    • CDN通してるのでダウンロード速度が速い
      • CIでデプロイしたらレスポンスをカールで叩くことで人が初回ダウンロードするときにはキャッシュつかえる
    • DistributionTracks

説明内容が濃くて理解が追いつきませんでした。。。後ほど加筆しますm(_ _)m

資料

形式手法について調べてみた

スピーカー

  • altitude3190

内容

  • 仕様の不備に開発フェーズの速い段階で気づきたい
  • あとになればなるほど手戻り工数が大きい
  • 改善はしていたが場当たり的な対応、工数・スキル的に手に負えなかった
  • 形式手法
    • 仕様を明確に記述
    • 機械的に検証する
    • 数学に基づく科学的な裏付けがある
  • 形式仕様記述
    • VDM++/Event-B/Zなど
  • 定理証明
    • Coq
  • モデル検査
    • Alloy/Promela
    • システムを有限個の状態を持つモデルで表現
    • モデルの状態を機械的網羅的に検査
    • Spinはモデル検査ツールでHomebrewでインストール可能
    • 判例があればトレースとともに表示
  • メリットデメリット
    • 形式手法メリット
      • 検査結果から仕様不備に気付ける
    • デメリット
      • 導入コスト
      • モデル化が難しい場合がある
    • 部分化導入がよい
  • 現場にマッチした形式手法がどれになるか吟味する必要がある

資料

UI テストで楽するための技術

スピーカー

  • ksfee

内容

  • UIテストは大変
    • 環境構築が大変
    • 実行時間がかかる
    • 実行結果の確認
    • 依存が多い
    • 実装が大変
  • 楽する技術
    • テストケースをDSLで
    • テスト結果
      • Composer
        • ログとスクリーンショットを合わせたレポートを提供
    • ネットワーク依存
      • OkReplay
        • OkHTTPの通信をRecord・Replay
        • レスポンスデータの管理が楽
        • ワイルドカードの表現が使えない。動的なリクエストが難しい

資料

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

background-sizeの記載には気を付けよう

androidで、非表示の要素を表示させたときに背景画像設定が消える現象が発生しました。
調べてみたところ、background-sizeの書き方に問題がありました。

before

css
   background: url(../../hogehoge.jpg) left top repeat-y;
   background-size: 100% auto;

100% auto という記載に問題がありました。

after

css
   background: url(../../hogehoge.jpg) left top repeat-y;
   background-size: 100%;

これで直りました。

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

Haxeを用いたゲームエンジン一覧(随時更新)

『Haxe Game Engines』というYouTubeのビデオで、Haxeが使われているゲームエンジンが紹介されていました。意外とあるなぁ、というのと聞いたことないけど調べてみるのも良いなと思いメモのため公開しておきます。この記事に出ている以上のことはビデオ(英語ですが)の方をご覧ください。

ローレベルなものから本格的なゲームエンジンまでいろいろありますが、できれば全てのエンジンに実際に触れてみて分かったことなどを、この記事にも反映していければと思っています。

Lowest Level なもの

NME(Native Media Engine) ⇒ https://github.com/haxenme/nme

image.png

Lime ⇒ https://github.com/openfl/lime

image.png

Intermediate Level なもの

Kha ⇒ https://github.com/Kode/Kha

image.png

OpenFL ⇒ http://www.openfl.org/

image.png

High Level 2D なもの

HaxeFlixel ⇒ http://haxeflixel.com/

image.png

HaxePunk ⇒ http://haxepunk.com/

image.png

Stencyl ⇒ http://www.stencyl.com/

image.png

High Level 3D なもの

Away3D ⇒ https://github.com/openfl/away3d

image.png

Heaps ⇒ https://heaps.io/

image.png

Armory3D ⇒ https://armory3d.org/

image.png

対応プラットフォーム

ゲームエンジン 対応プラットフォーム
NME iOS, Android, Windows, Mac, Linux
Lime windows, mac, linux, neko, android, ios, html5, flash, air
Kha HTML5 (WebGL 2, WebGL and canvas), Windows (Direct3D 12, Direct3D 11, Direct3D 9, Vulkan or OpenGL), Universal Windows Platform (Direct3D 12 or Direct3D 11), macOS (Metal or OpenGL), Linux (Vulkan or OpenGL), Android (via C++ or via Java), iOS (Metal or OpenGL), tvOS, Raspberry Pi, PlayStation 4, Xbox One, Nintendo Switch, Tizen, Flash, Unity 3D, Node.js (for automatically created server versions), Java and AWT, C# and Windows Presentation Foundation
OpenFL Windows, macOS, Linux, iOS, Android, Flash, AIR, HTML5
HaxeFlixel Flash, Windows, Mac, Linux, Android, iOS, Neko, BlackBerry, WebOS
HaxePunk Windows, Mac, Linux, HTML 5 (WebGL), iOS, Android
Stencyl iOS (iPhone/iPad), Android, Windows, Mac, Linux, Flash, HTML5
Away3D Flash, HTML5, iOS, Android, Windows, Mac, Linux
Heaps HTML5 (requires WebGL1, WebGL2 is supported as well) - Mobile (iOS, Android, and tvOS) - Desktop with OpenGL (Win/Linux/OSX) or DirectX (Windows only) - Consoles (Nintendo Switch, Sony PS4, XBox One - requires being a registered developer) - Flash Stage3D
Armory3D HTML5, HTML5 (native), HTML5 Worker, Windows, Windows Universal, OS X, Linux, Android, Android (native), iOS, Raspberry Pi, Tizen, Flash, node.js, Unity, PlayStation Mobile, Java, WPF
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CoordinatorLayoutに表示したSnackBarをスワイプで消せないようにする方法

知らなかったんですが、CoordinatorLayoutにSnackBarを表示すると、左から右へスワイプで消せるんですね。
でも、消したくない!ってこともあるので、消さないようにする方法を調べて実際に消せないようにした

まずは消している実装を調べた

Snackbarが継承している BaseTransientBottomBar で以下のようなコードがあります。

    final void showView() {
        if (mView.getParent() == null) {
            final ViewGroup.LayoutParams lp = mView.getLayoutParams();

            if (lp instanceof CoordinatorLayout.LayoutParams) {
                // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
                final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;

                final Behavior behavior = new Behavior();
                behavior.setStartAlphaSwipeDistance(0.1f);
                behavior.setEndAlphaSwipeDistance(0.6f);
                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
                // 略
                clp.setBehavior(behavior);
                // Also set the inset edge so that views can dodge the bar correctly
                clp.insetEdge = Gravity.BOTTOM;
            }

            mTargetParent.addView(mView);
        }

Snackbarが使ってるViewのLayoutParamsがCoordinatorLayoutだったときだけ特別にBehaviorなるものを設定して、スワイプで消せるようにしてました。

消さないようにする方法

CoordinatorLayoutを使いながらも、スワイプで消さないようにするために、Behaviorを上書きするようにしました。

val snackBar = Snackbar.make(binding.coordinatorLayout, message.text, length).apply {
    addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
        override fun onShown(transientBottomBar: Snackbar) {
            super.onShown(transientBottomBar)
            val lp = transientBottomBar.view.layoutParams
            if (lp is CoordinatorLayout.LayoutParams) {
                lp.behavior = null
            }
        }
    })
}
snackBar.show()

addCallbackで、snackBarが表示出来た後にBehaviorを上書きしてます。 snackBar.show() の直後だと、 BaseTransientBottomBar.showViw() でBehaviorが上書きされてしまうのでスワイプで消えてしまいます。

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

Android Studio TabbedActivity 拡張

各フラグメント表示後、上からリストビューが下りてくるTabbedActivityです。
ポイントはSectionsPagerAdapter#setPrimaryItem()でリストビューのY位置変更処理をする所です。
下記のコードの大部分はTabbedActivity を選択すると、AndroidStudio が自動的に生成してくれます。

サンプル動画(YouTube)

    public static class PlaceholderFragment extends Fragment {

        private static final String ARG_SECTION_NUMBER = "section_number";
        static ListView[] list = new ListView[7];  //リストビュー変数宣言

        public PlaceholderFragment() {
        }

        public static PlaceholderFragment newInstance(int sectionNumber) {
            PlaceholderFragment fragment = new PlaceholderFragment();
            Bundle args = new Bundle();
            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
            fragment.setArguments(args);
            return fragment;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_stage_select, container, false);
            int selector = getArguments().getInt(ARG_SECTION_NUMBER);

///////////////////////////////////////////////ここから追加
            ImageView imageView = rootView.findViewById(R.id.imageView);
            list[selector] = rootView.findViewById(R.id.movingList);
            ArrayList messages = new ArrayList<String>();
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this.getActivity(), android.R.layout.simple_list_item_1, messages);
            list[selector].setAdapter(adapter);

            switch(selector){
                case 0:
                    messages.add("Stage0");
                    messages.add("Level1");
                    break;
                case 1:
                    messages.add("Stage1");
                    break;
                case 2:
                    messages.add("Stage2");
                    list[2].setY(-list[2].getHeight());
                    break;
                case 3:
                    messages.add("Stage3");
                    messages.add("Level 250");
                    //imageView.setImageResource(R.drawable.stage3);
                    break;
                case 4:
                    messages.add("Level 2 室町用");
                    imageView.setImageResource(R.drawable.stage4);
                    break;
                case 5:
                    messages.add("How to play");
                    messages.add("Level 0");
                    imageView.setImageResource(R.drawable.stage5);
                    break;
                case 6:
                    messages.add("Stage6");
                    //imageView.setImageResource(R.drawable.stage6);
                    break;
            }
///////////////////////////////////////////////ここまで追加
            return rootView;
        }

    }

    public class SectionsPagerAdapter extends FragmentPagerAdapter {
        int page;

        public SectionsPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return PlaceholderFragment.newInstance(position);
        }

        @Override
        public int getCount() {
            return 7;  //要素数設定
        }

///////////////////////////////////////ここから追加
        @Override
        public void setPrimaryItem(ViewGroup container, final int position, Object object){
            super.setPrimaryItem(container, position, object);
            if(page == position)return;
            final float hide_y = -PlaceholderFragment.list[position].getHeight();

            //前ページのList隠し処理
            for(int i = 0; i < 7; i++){
                if(PlaceholderFragment.list[i] != null)
                PlaceholderFragment.list[i].setY(-PlaceholderFragment.list[i].getHeight());
            }

            page = position;

            new Thread(new Runnable(){
                float y = hide_y;
                public void run(){
                    while(y < 0) {
                        ListView list = PlaceholderFragment.list[position];
                        list.setY(y);
                        Log.d("messaged", "position y: " + y);
                        y += 5;  //Y軸移動量 / 1ステップ
                        try {
                            Thread.sleep(10);  //タイムインターバル
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            Log.d("messaged", "setPrimaryItem    position: " + position);
        }
////////////////////////////////////////////ここまで追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

androidx.appcompat.widget.Toolbar のタイトルとアイコンの色を変える方法

実装

activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout ..>
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        app:theme="@style/ToolbarTheme" />
styles.xml
<resources>
    <style name="ToolbarTheme" parent="@style/ThemeOverlay.AppCompat.ActionBar">
        <!-- ToolBarのタイトルテキスト色 -->
        <item name="android:textColorPrimary">@color/colorWhite</item>
        <!-- iconの色 -->
        <item name="colorControlNormal">@color/colorWhite</item>
    </style>
</resources>

参考

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

listViewに置かれたボタンからdialog fragmentを表示しようとして泥沼だった話

今回はかなり苦戦しました。(毎回ですけど)

タイトルの通りです。

どうにかこれを達成できたのでその手順を書きます。

まず全体の流れは

※④を訂正し、③を打ち消しました。(2019/3/7)

①listViewでクリックイベントの結果を取得
→②"listViewを表示しているfragment"に上述の結果を渡す
→③"上述のfragmentを表示しているactivity"に上述の結果を渡す
→④ ②のfragmentでdialog fragmentを表示する(訂正あり)

このようなややこしい流れになりました。

コメント欄にてご指摘をいただきましたが、③は完全に勘違いで、activityからしかdialogFragmentを呼び出せないということはなく、fragmentからdialogFragmentを直接呼び出すことが可能でした。④にて後述します。

というわけで手順を①から見ていきます。

①listViewでクリックイベントの結果を取得

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    // ここに処理を書く
    }
});

これは毎度おなじみですね。
R.id.buttonのbuttonの部分は、xmlファイルのコンポーネントのIDと同じにしてください。

②"listViewを表示しているfragment"に上述の結果を渡す
これがなかなか見つからなかったコードです。

SomeAdapter.java
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // ここに処理を書く
        ((ListView) parent).performItemClick(v, position, R.id.button);
    }
});
SomeFragment.java
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        switch (view.getId()) {
            case R.id.button:
                //buttonがclickされたらここに飛びます
        }
    }
});

以上の処理で、fragmentに飛べます。

parentってなんぞや。
→getViewの引数、ViewGroupです。

positionってなんぞや。
→これもlistViewの引数getViewのint型の引数です

※getViewはアダプターを作るときに呼ばれるメソッドです。検索したら出てきます。

④ ②のfragmentでdialog fragmentを表示する(訂正あり)
アクティビティにdisplayDialogメソッドを追加します

SomeFragment.java
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        switch (view.getId()) {
            case R.id.button:
                //buttonがclickされたらここに飛びます
                SomeDialogFragment dialog = new SomeDialogFragment();
                dialog.show(getFragmentManager(), "tag");
        }
    }
});

dialog fragmentをインスタンス化してshowメソッドを呼ぶだけで表示できたようでとても悲しいです。
あと、getFragmentManagerとgetSupportFragmentManagerの違いについてもご指摘をいただきました。
以下はコメント欄の引用です。

バージョン違いではなく、継承元のクラスが何か、によって、getSupportFragmentManagerを使うべきなのかgetFragmentManagerを使うべきなのかが変わります。

android.app.Activityと、
android.app.DialogFragment
android.app.Fragment
なら、
Activity#getFragmentManagerを使う

android.support.v7.app.AppCompatActivityと、
android.support.v4.app.Fragment
android.support.v4.app.DialogFragment
なら、
AppCompatActivity#getSupportFragmentManagerを使う

この組み合わせで使わないと、いろいろと不具合が起こります。
そして、API28(AndroidP)から、android.app.DialogFragmentとandroid.app.Fragmentは非推奨となったので、実質、後者の組み合わせで行くべきかと思います。

どっちかが新しいんだろうなくらいの感覚でテキトーにoption + enterで追加していたので、きちんと組み合わせがあったらしくちょっと感動しました。

@kasa_le さん
この度はありがとうございました。
精進します!

という感じでした。

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

[ANDROID]CPUとアーキテクチャとABIの関係

andoidアプリ開発中に、外からとってきたsoファイルを使いたいとする。(openCVとか)
大体、armeabi-v7a用とかx86用とか書いてあるけど気にせず全部jniLibs直下にコピーする。
実際、どの端末でどのsoファイルが動いてるかよく知らない。そもそもarmとかx86とか何?
そんな自分のための記事。

ARM? X86? MIPS?

android端末のCPUに適用される命令セットアーキテクチャ(または、CPUそのもの)の名称を指す。
armであれば、arm社のarmアーキテクチャが
X86なら、Intel社のIntel 8086、またその後方互換のアーキテクチャ、
MIPSなら、ミップス・コンピュータシステムズ(現ミップス・テクノロジーズ)社のアーキテクチャがそれぞれ適用されているCPUであるということになる。

命令セットアーキテクチャが違うと?

C/C++言語で書いたコードは、ターゲットとする命令セットアーキテクチャに対してコンパイルし、専用の実行バイナリファイルを作成する。
つまり、他の命令セットアーキテクチャのためにコンパイルされたバイナリファイルは動かない。なので、openCVなどのオープンソースはどのCPUを積んだ端末でも動くよう、それぞれの命令セットアーキテクチャに合わせたsoファイルを用意している。
image.png
OpenCV4Android SDK
androidにおいては、その命令セットアーキテクチャの違いをABIという定義で管理する。

ABIとは?

Application Binary Interfaceの略
正式な定義は、
"ABI は、アプリケーションのマシンコードが実行時にシステムとやり取りする方法を詳細に定義したもの"
Android Developers->NDK ABI管理

適当な覚え方は、
端末のCPUと命令セットに応じて定義するディレクトリの名称。soファイルを入れるところ。
image.png

端末Aでは動いたのに、端末Bでは"Fatal Exception: java.lang.UnsatisfiedLinkError"でアプリが落ちたみたいなとき、端末Bに対応するABIのsoファイルを入れてなかったりする。

端末ごとの調べ方(手元に調べたい端末がない時)

仮にNexus10でsoファイルを含むアプリを動かしたいとする。
1. Nexus10のCPUを調べる。
https://ja.wikipedia.org/wiki/Nexus_10
⇒ARMのCoretex-A15ベースのExynos 5250

2.Coretex-A15のアーキテクチャを調べる。
https://ja.wikipedia.org/wiki/ARM%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3
⇒v7-a
image.png

3.対応するABIを調べる。
https://developer.android.com/ndk/guides/abis
⇒armeabi-v7a
image.png

なので、"armeabi-v7a"においたsoファイルが動いてることになる。

端末ごとの調べ方(手元に調べたい端末がある時)

"adb shell"でつないで、"$ getprop | grep cpu"を叩くだけ。
[ro.product.cpu.abi]: [armeabi-v7a]
ABIが表示されすぐにわかる。

サポートしているABIのsoファイル入れてないのになぜか動いてるけど。。。

ARMであれば、armeabi → armeabi-v7a → arm64-v8aの順に新しい。
基本的に下位互換性があるので、Nexus10であれば、armeabi-v7aにsoファイルがなくても、armeabiのsoファイルでアプリは動く。

参考記事

Cプログラミング入門
【本当に32ビット?】Android端末の CPU の ABI を特定する

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

Android Studio を Applications にコピーするときに「一部の項目へのアクセス権がないため、操作を完了できません。」と表示されてもあきらめない

環境

内容

スクリーンショット 2019-03-07 11.17.08.png

Android Studio 3.3.2 をインストールするときに

スクリーンショット 2019-03-07 11.18.20.png

こんなエラーが出た。

Applications へコピーしている最中、古いバージョンの Android Studio を起動しているのが原因かと思い終了させて再度チャレンジしてみたが変わらず。
(Android Studioをバージョンアップするとビルドが通らなくなったことがあるので、あえて Android Studio のアップデートではなくインストールしている。)

ググると

  • アクセス権が設定されていないのでは?
  • ルートユーザーでログインしてる?
  • HDD が壊れているかも?

といった内容が出てきたので確認してみたがどれも大丈夫そう。
(最後のは そんなハズないし壊れていたら困る… と思って確認していない)

他に情報がないかググりつつ、エラーを無視して何度もコピーしつづけた。

スクリーンショット 2019-03-07 11.45.12.png
スクリーンショット 2019-03-07 11.18.20.png

んー

スクリーンショット 2019-03-07 11.17.13.png
スクリーンショット 2019-03-07 11.18.20.png

んーー

スクリーンショット 2019-03-07 11.45.21.png
スクリーンショット 2019-03-07 11.18.20.png

おや?
地味にプログレスバー伸びてる??

スクリーンショット 2019-03-07 11.45.33.png
スクリーンショット 2019-03-07 11.18.20.png

これは伸びてるな!

スクリーンショット 2019-03-07 11.45.59.png

いけたーーー!!

まとめ

あきらめずに想いを伝え続ければ、想いは伝わる。
(なぜ少しずつコピーできたのか、調べてみたがわからずだった。。)

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

あの素晴らしいlicense-tools-pluginをもう一度

:cooking: はじめに

Qiita初投稿になります。6年程会社でプログラマとして仕事をし、今はフリーランスでAndroidアプリを中心にJava/Kotlinでゴリゴリ開発してます。今回、Androidアプリで使っているライブラリの表示をする必要がありまして、以前採用させていただいたクックパッドさんの license-tools-plugin を使おうとしたのですが、プラグイン自体のアップデートやIDE(Android Studio)の更新もあり、すんなり導入することができませんでした。公式が クックパッド開発者ブログ で解説していますが、導入方法とハマったポイントを少し長いですが共有します。間違いや補足などがあればコメントにて教えてください。

:cooking: 開発環境

  • PC: MacBook Air (11-inch, Early 2014)
  • OS: macOS Mojave 10.14.3
  • IDE: Android Studio 3.3.2

:cooking: license-tools-pluginとは

クックパッドさんが開発したGradleの神プラグインです。プロジェクトで使用しているライブラリのライセンスをYAMLで管理、HTMLもしくはJSONに一覧を出力してくれます。ライセンスの追記漏れもチェックしてくれる優れものです。GitHubのリポジトリは こちら です。

:cooking: 導入

Gradleのプラグインなので、プロジェクトルート直下の build.gradle に追記します。記事執筆時の最新バージョンは 1.7.0 でした。

build.gradle[root]
buildscript {
    ...

    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.cookpad.android.licensetools:license-tools-plugin:1.7.0' // 追加
    }
}

アプリ内で使用する為に、app直下の build.gradle の冒頭に追記をします。

build.gradle[app]
apply plugin: 'com.cookpad.android.licensetools' // 追加

動作する為に JDK8 以上が必須とのことなので、こちらも build.gradle に互換性を追記しておきます。 JDK8 はAndroid Studio 3.3.2であれば、ビルトインとしてあらかじめ用意されているはずです。

build.gradle[app]
android {
    ...

    // 追加
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

全ての追記が終わったら Sync Project を行い、エラーが出ていなければOKです。

:cooking: 使い方

ライブラリの依存関係を確認

ライブラリの依存関係を確認します。今回はデモの為に、Androidアプリの開発でよく使うものを追加しました。

build.gradle[app]
dependencies {
    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    // Android Support Library
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:cardview-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    // Square
    implementation 'com.squareup.moshi:moshi:1.8.0'
    implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'
    implementation 'com.squareup:otto:1.3.8'
    implementation 'com.squareup.picasso:picasso:2.71828'
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'

    // ReactiveX
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.7'
}

ライセンス情報の作成

依存関係の記述が終わったら、Android Studioのサイドバーにある Gradle > [project-name] > :app > Tasks > verification から、 checkLicenses タスクを実行します。コンソールにライセンス情報の一覧が出力されるので、app直下に licenses.yml を作成して内容をコピペします。

licenses.yml
- artifact: android.arch.core:common:+
  name: Android Arch-Common
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.squareup.okhttp3:okhttp:+
  name: OkHttp
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: org.jetbrains.kotlin:kotlin-stdlib:+
  name: org.jetbrains.kotlin:kotlin-stdlib
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://kotlinlang.org/
- artifact: com.squareup.okio:okio:+
  name: Okio
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: com.android.support:support-annotations:+
  name: Android Support Library Annotations
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:design:+
  name: Material Components for Android
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-utils:+
  name: Android Support Library core utils
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:interpolator:+
  name: Android Support Library Interpolators
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html

# 以下、長いので省略

ライセンス情報の編集

その後 licenses.yaml の情報を編集していきます。具体的には #COPYRIGHT_HOLDER##LICENSE# と言ったプレースホルダ部分を編集します。例えば

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: com.squareup.retrofit2:retrofit:+
  name: Retrofit
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: io.reactivex.rxjava2:rxjava:+
  name: RxJava
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#

のような一覧が出力された場合は

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: The Android Open Source Project
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://developer.android.com/topic/libraries/support-library/
- artifact: com.squareup.retrofit2:retrofit:+
  name: Retrofit
  copyrightHolder: Square, Inc.
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://github.com/square/retrofit/
- artifact: io.reactivex.rxjava2:rxjava:+
  name: RxJava
  copyrightHolder: RxJava Contributors
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://github.com/reactivex/rxjava/

となります。基本的にGitHubの情報を元に調べ編集していきます。Android Open Source Project(AOSP)は ここ にライセンスに関する内容が記載されています。全ての情報の編集が終わったら、再度 checkLicenses タスクを実行し、エラーが発生しなければOKです。

ライセンス情報の表示

最後に Gradle > [project-name] > :app > Tasks > other から、 generateLicensePage タスクを実行し、出力されたHTMLファイルをアプリ内の WebView に読み込ませます。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    webView.loadUrl("file:///android_asset/licenses.html")
}

screen.gif

:cooking: ハマったこと

checkLicensesタスクが失敗する

checkLicenses タスクを実行した際に missing libraries in licenses.yml というエラーで失敗してしまう。初回実行時には licenses.yaml が存在しないので、コンソールに表示されたライセンスの情報一覧をコピペして作成すればいいのですが、2回目以降であれば不足している情報がコンソールに出力されていますので、YAMLファイルに追記します。また、使用しているライブラリが、内部的に別のライブラリに依存している場合(例えば appcompat-v7 は内部的に support-annotationssupport-compat に依存している)、その関係性も監視対象になります。そういったライブラリは

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: The Android Open Source Project
  license: The Apache License, Version 2.0
- artifact: com.android.support:support-annotations:+
  skip: true
- artifact: com.android.support:support-compat:+
  skip: true

と記述することで、一覧から除外することができます。また、グループ一式で除外する場合は

licenses.yaml
- artifact: com.android.support:+:+
  skip: true

の様にワイルドカードで指定したり、

build.gradle[app]
licenseTools {
    ignoredGroups = ['com.android.support']
}

の様に記述することで可能となります。

generateLicensePageタスクが失敗する

コンソールでエラーの内容を確認すると checkLicenses タスクでコケていることがほとんどかと思います。 checkLicensesタスクが失敗する を参考に解決します。

必要な項目が不足している

licenses.yaml に必要な項目が不足している場合にも、プラグインのタスクが失敗します。必須項目は以下の通りです。

  • artifact
  • name
  • copyrightHolder/ author/ authors/ noticeのうちどれか1つ

オプションとして付与できる項目は

  • year
  • skip
  • forceGenerate

などがあります。詳しくは公式の GitHubリポジトリ を参照してください。

:cooking: 最終的な着地点

ここまでのハマった箇所を踏まえて、デモ用のアプリとしてGitHubに demo-licenses をアップしておきます。参考になるかわかりませんが、躓いていらっしゃる方の助けに少しでもなれたらなと思います。表示させないライセンスについてですが、最終的には ignoredGroupsskip を併用することで解決しました。 ignoredGroups は、名前の通り使用しているライブラリの groupId のみにしか対応しておらず artifactId では除外できませんでした。 groupId + artifactId でもプラグインの監視対象から除外できれば良いのになぁ。

:cooking: まとめ

結果的に調べたりなんなりで作業自体のコストはかかってしまいましたが、1から全て自前で実装するよりは遥かに良いです。今後、同様な実装が発生した場合にはこの記事を参考にスピーディに対応したいですね。また、先ほどの groupId + artifactId で除外するPull Requestなどを本家に出してみたいと思っています。ライセンスの表示は、Androidアプリを開発していく上で必要なことではありますが、開発が進むにつれて管理が億劫になったり、時間を割くことが難しいかもしれません。そんな痒いところに手が届くようなライブラリを開発してくださったクックパッド様には感謝しかないです :pray:

:cooking: どうでも良い話

ライセンスの綴りとして licenselicence があります。大きな違いとして、イギリスでは動詞の場合に『license』名詞の場合に『licence』を使い分けますが、アメリカでは特に区別をせず日常的に『license』が使われるみたいです。結構タイプミスするので気をつけたいです。

:cooking: 参考文献

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

【Android】画像をカルーセル表示するときにハマった話

RecyclerViewを使うと簡単に画像をカルーセル表示することができます。
ですが、ImageViewの扱いでハマった部分があったので、その際に調査した内容をまとめました。

TL;DR

ImageViewのAdjustViewBoundstrueをセットすることで、ImageViewのサイズを画像サイズに合わせて調整することができます。

前提条件

カルーセル、画像のサイズおよび表示方法に関する条件は以下の通りです。

  1. カルーセルの高さは固定とする
  2. カルーセルに表示する各画像のサイズはすべて異なる可能性がある
  3. 各画像の高さはカルーセルの高さと一致するように拡大縮小する
  4. 各画像のアスペクト比は維持する
  5. 各画像はクロップしない

ハマった内容

以前作成したScaleTypeと表示画像の対応表を参考に、ScaleTypeは一旦FIT_CENTER(デフォルト値のためxml上の指定なし)、またlayout_widthwrap_contentlayout_height100dpとしました。

RecyclerViewの各要素に表示する画面のxmlは以下の通りです。

item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/imageView"
  android:layout_width="wrap_content"
  android:layout_height="100dp" />

正直どのScaleTypeも今回の条件を満たさないと感じつつも、layout_widthwrap_contentにすることで、FIT_CENTERであれば画像の高さをカルーセルの高さと一致させ、それに合わせて画像全体が含まれるようにアスペクト比を維持しながら幅を調整してくれるのでは?というわずかな希望を抱きながら試してみました。

結果は以下のようになりました。案の定、希望通りの表示になりませんでした。
before.png
これを見て、画像が小さいときの結果(左)は納得できるのですが、画像が大きいときの結果(右)に関しては何で左右に余白ができるのかが理解できませんでした。

解決方法

冒頭でも触れましたが、ImageViewのAdjustViewBoundstrueにすることで解決できました。これは文字通り「Viewの境界を調整する」ための属性です。
公式ドキュメントはこちらにあります。

AdjustViewBoundstrueにすると、結果は以下のようになりました。上が先ほどの結果(デフォルト値はfalse)で、下が今回の結果です。
after.png
これで、ImageViewのサイズを画像サイズに合わせて調整することができました。

AdjustViewBoundsをセットしたときの内部処理

結果としてはこれで問題ないのですが、画像が大きいときの結果に関してはこれだけでは理解ができなかったので、ImageViewの処理を追ってみました。
ImageView.javaのソースコードはこちらにあります。

Androidにおいて、Viewが表示されるまでの大まかな流れは以下の通りになります。今回の肝となるのは1.のonMeasure()です。

  1. サイズを決める(onMeasure())
  2. 場所を決める(onLayout())
  3. 描画する(onDraw())

これに関してはこちらの記事を参考にさせていただきました。


では、ここからはImageViewのonMeasure()の処理を追っていきます。

最初は各変数を宣言しているだけだったので割愛します。
以下の処理が1つ目のポイントになります。

ImageView.java#LL.1088-1089
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

widthMeasureSpecheightMeasureSpeconMeasure()の引数で、そこからMeasureSpecModeを取得しています。
ただ、widthMeasureSpecheightMeasureSpecがどのように決定されるのかは正直よく分かりませんでした。

MeasureSpecは親Viewから子Viewに対して課される制約を表しており、以下の3種類のModeがあります。公式ドキュメントはこちらにあります。

MeasureSpec.Mode 制約条件
UNSPECIFIED 親Viewによって子Viewのサイズが決定されない
EXACTLY 親Viewによって子Viewの正確なサイズが決定される
AT_MOST 親Viewによって子Viewの最大のサイズが決定される

したがって、widthSpecModeMeasureSpec.UNSPECIFIEDheightSpecModeMeasureSpec.EXACTLYとなります。



そして、以下の処理が2つ目のポイントになります。
AdjustViewBoundstrueの場合のみ以下の処理が行われます。

ImageView.java#LL.1104-1109
if (mAdjustViewBounds) {
  resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
  resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
  desiredAspect = (float) w / (float) h;
}

resizeWidthresizeHeightは幅・高さのリサイズを行うか否かを制御する変数です。
desiredAspectは画像のアスペクト比(wは幅、hは高さ)を表す変数です。

今回の場合、widthSpecModeMeasureSpec.UNSPECIFIEDheightSpecModeMeasureSpec.EXACTLYなので、AdjustViewBoundstrueのときはresizeWidthのみがtrueとなります。



そして、以下の処理が3つ目のポイントになります。
長いので詳細は省略しますが、おおむね以下のような処理が行われています。

  1. AdjustViewBoundstrueのとき
    ImageViewのサイズを画像サイズに合わせて調整する
  2. AdjustViewBoundsfalseのとき
    ImageViewの幅を画像の幅と一致するよう調整する
ImageView.java#LL.1120-1190
int widthSize;
int heightSize;
if (resizeWidth || resizeHeight) {
  // Get the max possible width given our constraints
  widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
  // Get the max possible height given our constraints
  heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
  if (desiredAspect != 0.0f) {
    // See what our actual aspect ratio is
    final float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom);
    if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
      boolean done = false;
      // Try adjusting width to be proportional to height
      if (resizeWidth) {
        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
        // Allow the width to outgrow its original estimate if height is fixed.
        if (!resizeHeight && !sCompatAdjustViewBounds) {
          widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
        }
        if (newWidth <= widthSize) {
          widthSize = newWidth;
          done = true;
        }
      }
      // Try adjusting height to be proportional to width
      if (!done && resizeHeight) {
        ...
      }
    }
  }
} else {
  w += pleft + pright;
  h += ptop + pbottom;
  w = Math.max(w, getSuggestedMinimumWidth());
  h = Math.max(h, getSuggestedMinimumHeight());
  widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
  heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);



ここで、else句の最後にあるresolveSizeAndState()MeasureSpec.UNSPECIFIEDのときは引数に指定した画像サイズそのものを返していたので、ImageViewの幅が画像の幅と同じサイズで表示されるということが分かりました。スッキリ。
View.javaのソースコードはこちらにあります。

View#LL.23463-23483
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
  final int specMode = MeasureSpec.getMode(measureSpec);
  final int specSize = MeasureSpec.getSize(measureSpec);
  final int result;
  switch (specMode) {
    case MeasureSpec.AT_MOST:
      if (specSize < size) {
        result = specSize | MEASURED_STATE_TOO_SMALL;
      } else {
        result = size;
      }
      break;
    case MeasureSpec.EXACTLY:
      result = specSize;
      break;
    case MeasureSpec.UNSPECIFIED:
    default:
      result = size;
  }
  return result | (childMeasuredState & MEASURED_STATE_MASK);
}

サンプルアプリ

上記の画像キャプチャを撮影したアプリです。
GitHub - AdjustViewBoundsChecker

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

BottomNavigationViewをiOSのTabBarっぽくする

概要

MaterialDesignから提供されているBottomNavigationViewを用いることでiOSのTabBarのような下タブのナビゲーションを作成することが容易に可能になりました.
そこでiOSライクにAndroidでも下タブのアプリを開発してやるぜー!と思っていると選択範囲しかアイテム名が表示されなかったりとBottomNavigationView 独特のクセが目立ちました.
もちろんこれがMaterialDesignガイドラインが示している解だとはわかっているものの下タブといえばiOSのアレをどうしても想像してしまうためクセは気になります.
この記事ではそれらのクセ完全に断ち切って消してiOSライクな下タブを目指します

アイテムの色を選択状態によって変える

リソースファイルにcolorフォルダなどを作成し以下のファイルを追加します.
選択・非選択時の色は各自変えてください.
選択状態の色がプライマリーカラーにする場合は以下の操作は不要で一番下のcheckedtrueをセットしてやるだけで可能です.

nav_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:color="@color/colorPrimary" />
    <item android:state_checked="false" android:color="@android:color/darker_gray"/>
</selector>

次にBottomNavigationViewが記述されているレイアウトファイルに以下の文を追加します

activity_main.xml
app:itemIconTint="@color/nav_color"
app:itemTextColor="@color/nav_color"

最後にコード上から選択時に選択状態を直接変更すれば,選択に合わせてアイテムの色を変更できます.

MainActivity.java
menuItem.setChecked(true);

アイテムの名前を常に表示させる

デフォルトの設定ではアイテム数が増えるとアイテムの名前は選択状態のものだけが表示されていて,常時表示にはなっていません.
これを常時表示にするにはBottomNavigationViewに以下のパラメータを指定することで解決できます.

activity_main.xml
app:labelVisibilityMode="labeled"

選択時のアイコン拡大アニメーションを無効化

このアニメーションは選択・非選択の状態においてアイテム内のアイコン・テキストの大きさが変動するために起きています.そもそも標準のフォントサイズは若干大きいのでiOSと同じ10spに統一します.
dimes.xmlに以下の文を追加します.

dimes.xml
<dimen name="design_bottom_navigation_text_size" tools:override="true">10sp</dimen>
<dimen name="design_bottom_navigation_active_text_size" tools:override="true">10sp</dimen>

これで選択・非選択でも文字の大きさが固定され選択中のアイテムが大きくなるアニメーションなどは起こらなくなります.

BottomNavigationViewが手前に来るのを防ぐ

どういうわけかデフォルトではBottomNavigationViewは画面下に固定されている状態ではなく,EditText時などにはFloatingActionButtonの様にNavigationViewごとキーボード上に浮上してきます.
それを防ぐのが以下のコードです.

AndroidManifest.xml
<activity android:name=".MainActivity"
          android:windowSoftInputMode="adjustPan"></activity>

ManifestファイルのBottomNavigationViewが入っているActivityの部分をこのように書き換えることで,BottomViewNavigationが文章編集時でも所定の位置にいてくれます.

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