20190310のAndroidに関する記事は13件です。

ML-Kit For Firebase でTensorFlow Lite使用サンプルを動かす

概要

ML-Kit For Firebase のTensorFlow Lite使用のサンプルを動かしたかったので
導入手順のメモを残しておきます

ML-Kit For Firebaseは以下のサンプルも試せますが
画像分類、文字認識、顔検出、バーコードスキャン、ランドマーク認識

今回は上記以外にも、TensorFlow Lite形式をMobile端末に置き、使えるとのことで
このサンプルを動かします

実行環境は以下のとおり
Windows10 Pro
Android Studio Version3.2.2
AndroidアプリVersion9.0

サンプルアプリ動作手順

1.開発環境準備
Android Studio未導入の場合は以下URLからダウンロードしインストール
https://developer.android.com/studio/?hl=ja

2.サンプルアプリを取得
ML-Kitサンプルアプリを用意
以下URLから「Clone or Download」をクリックし「quickstart-android.zip」をダウンロード
https://github.com/firebase/quickstart-android

上記を解凍すると「quickstart-android-master」フォルダができるので、任意の場所に配置

Android Studioから「File>Open」で上記で配置した「quickstart-android-master」フォルダ配下の「mlkit」フォルダをプロジェクトとして選択

3.Firebaseプロジェクト登録
ML-Kitを使用するには「google-services.json」ファイルをプロジェクトに設定する必要があるため
以下のURLからFirebaseプロジェクトの登録を実施し、Jsonファイルを生成
https://console.firebase.google.com/
※Googleアカウント登録の必要あり

以下でプロジェクトを生成

・プロジェクトの追加
以下情報を設定
プロジェクト名:任意の名称
アナリティクスの地域:日本
「次へ」ボタンを押下

・AndroidアプリにFirebaseを追加
Androidパッケージ名に「com.google.firebase.samples.apps.mlkit」を設定し「アプリを登録」ボタンを押下
「google-services.json」ボタンを押下しJSONファイルをダウンロード

上記で生成したJSON「google-services.json」をプロジェクトの「app」フォルダ配下に配置

4.サンプルアプリを実行
「Android Studio」から「Run」>「Run’app’」を押下しアプリを実行
※実機でも、バーチャルマシンでも実行可能(最小API levelが16との記載ありのため Android 4.4 KitKat以上であれば動作するはず?)

アプリが立ち上がったら、コンボボックスから認識対象画像を選択し「FIND OBJECTS」ボタンを押下すると
画像のオブジェクトの種類と認識確度が高いもの上位3つが表示

sampleImg.png

参考文献
https://codelabs.developers.google.com/codelabs/mlkit-android-custom-model/#0

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

Android KotlinとRxBinding2

はじめに

Jake Whartonが作成したRxBinding2というライブラリをご存知でしょうか。
Viewでのイベントに対して様々な条件などを付与しsubscribeすることでViewでのアクションを制御することができます。
RxBinding2をkotlinで使う場合、kotlin用に拡張されているライブラリを利用する必要があります。

RxBinding2をkotlinで使う際の情報が少なかったので苦労しました...
基本的な利用方法を記載しているので、参考になればと思います。

  • Kotlin 1.3.21
  • Rxbinding-kotlin 2.2.0

RxBinding2の導入

kotlin拡張用のライブラリを導入する必要があるので、-kotlinを忘れないようにします。

app/build.gradle
// ...

dependencies {

    // ...
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
    def rx_binding2_version = '2.2.0'
    implementation "com.jakewharton.rxbinding2:rxbinding-kotlin:$rx_binding2_version"
    implementation "com.jakewharton.rxbinding2:rxbinding-recyclerview-v7-kotlin:$rx_binding2_version"
    implementation "com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:$rx_binding2_version"
    implementation "com.jakewharton.rxbinding2:rxbinding-support-v4-kotlin:$rx_binding2_version"
    implementation "com.jakewharton.rxbinding2:rxbinding-design-kotlin:$rx_binding2_version"
    implementation "com.jakewharton.rxbinding2:rxbinding-leanback-v17-kotlin:$rx_binding2_version"
}

利用方法

それぞれのライブラリの利用方法です。
一例を踏まえながら書き方を記載します。

rxbinding-kotlin

ViewにあるButtonに対してclickした際の挙動はこのように書くことができます。

val binding = DataBindingUtil.setContentView<ActivityHogeBinding>(this, R.layout.activity_hoge)
binding.demoButton
        .clicks()
        .filter { clickCount < 1 }
        .subscribe {
            clickCount++
            Log.d("debug", "click!!")
        }

rxbinding-recyclerview-v7-kotlin

ViewにあるRecyclerViewに対するアクションを制御できます。
RecyclerViewでスクロールが一番下までいったらロードする実装がこちらです。

val binding = DataBindingUtil.setContentView<ActivityHogeBinding>(this, R.layout.activity_hoge)
val gridLayoutManager = GridLayoutManager(this, 2)
// 実装例1
binding.hogeRecyclerView
        .scrollEvents()
        .filter { gridLayoutManager.itemCount - 1 <= gridLayoutManager.findLastVisibleItemPosition() }
        .subscribe { loadMore(item) }
// 実装例2
RxRecyclerView
        .scrollEvents(binding.companyRecyclerView)
        .filter { gridLayoutManager.itemCount - 1 <= gridLayoutManager.findLastVisibleItemPosition() }
        .subscribe { loadMore(item) }
fun loadMore(item: Item) {
    if (this.itemList.last().id != item.id) {
        Log.d("debug", "loadMore")
    }
}

rxbinding-appcompat-v7-kotlin

AppCompat-v7 Libraryで導入できるViewに対するアクションを制御できます。
ToolBarのitemのclickだとこのようになります。

// 実装例1
toolbar.itemClicks().subscribe{ Log.d("debug", "itemClicks") }
// 実装例2
RxToolbar.itemClicks(toolbar).subscribe{ Log.d("debug", "itemClicks") }

rxbinding-support-v4-kotlin

Support-v4 Libraryで導入できるViewに対するアクションを制御できます。
SwipeRefreshLayoutのrefreshだとこのようになります。

// 実装例1
binding.hogeFragmentSwipeRefreshLayout
        .refreshes()
        .subscribe { Log.d("debug", "refreshes") }
// 実装例2
RxSwipeRefreshLayout
        .refreshes(binding.hogeFragmentSwipeRefreshLayout)
        .subscribe { Log.d("debug", "refreshes") }

rxbinding-design-kotlin

Design Support Libraryで導入できるViewに対するアクションを制御できます。
NavigationViewのitem選択時の挙動だとこのようになります。

// 実装例1
binding.demoNavigationView
        .itemSelections()
        .subscribe { Log.d("debug", "itemSelections") }
// 実装例2
RxNavigationView
        .itemSelections(binding.demoNavigationView)
        .subscribe { Log.d("debug", "itemSelections") }

rxbinding-leanback-v17-kotlin

Leanback-v17 LibraryはAndroidTV向けのライブラリです。
今回は利用していないので、rxbinding-leanback-v17-kotlinについてはパスします。

最後に

RxBinding2の基本的な使い方を書きました。
kotlinの拡張関数との組み合わせや、他のRx系のライブラリと組み合わせることで、
よりシンプルに書くこともできそうだと感じました。
リアクティブプログラミングはまだ全然慣れていないので、良い実装例があれば勉強したいので教えていただけると嬉しいです。

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

How to run specific instrumented test method coded by Espresso from command line

What's this?

This is the sample command to run specific Android Instrument test case coded by Espresso against Android app.

How?

Assume this the test code stored under app/src/androidTest/java/com/hoget/testCases/.

sampletest.kt
package com.hoget.instrumenttest.testcases.accountTest

......
@RunWith(AndroidJUnit4::class)
class HogetAccountTest {

@Test
    fun LoginLogoutTest() {
        // test code in here
    }
}

To run LoginLogoutTest() method

$ ./gradlew connectedAndroidTest -P android.testInstrumentationRunnerArguments.class=com.hoget.instrumenttest.testcases.accountTest.HogetAccountTest#LoginLogoutTest;

So, the template for the command is like this:

$ ./gradlew connectedAndroidTest -P android.testInstrumentationRunnerArguments.class=<App's Test Package Name>.<Test Class Name>#<TestMethodName>;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 非同期でのUIスレッド処理について

はじめに

非同期処理のUIスレッドでの処理について、いろいろあったので整理とメモをする。

1,runOnUiThread

すぐにUIスレッドで実行する。

2,post

キューになげられ、順番待ちしてからUIスレッドで実行。

3,onPostExecute

これは、AsyncTaskで非同期処理誤に呼ばれる。

ほんとにメモ程度
おわり!

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

AndroidでJUnitのコードカバレッジを取得する。

設定

AndroidStudioでカバレッジ取得について、Jacocoがサポートされてるのでそれをつかう。

やることは設定を一つgradleに加えるだけ、

buildTypes {
    debug {
        testCoverageEnabled true
    }
}

これで準備はOK!

コードカバレッジを取得する

いろいろみてみたら取得するのにgitコマンドをつかうらしい、めんどくさいので別の方法で取得する。

AndroidStudio右上の「Gradle」をクリックするをこんな感じに表示される。
上記の設定をしていると「createDebugCoverageReport」が追加されてるはず、
それをダブルクリックでカバレッジが取得できる。(※instrumentedTestの結果)
この際、エミュレータを起動しておきましょう。
image.png

そうすると、「/app/build/outputs/reports/」にカバレッジができてるはず!
カバレッジの見方についてはめんどくさいので割愛。

おわり

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

appiumとCodeceptJS(node.js)を使ってAndroid & iOSのE2Eテストの実行環境構築

なぜこの文章を書いたか

前回のQiitaで「Vagrant + Selenium + node.js(CodeceptJS)でIE, Chrome, FirefoxのマルチブラウザE2Eテスト」について書いたのですが「どうせならスマホのE2Eテストも出来るようになりたい」と考え追加ポスト。

環境

OS : macOS Mojave (10.14.3)
VirtualBox : 6.0.4
Vagrant : v2.2.3
node.js : 10.15.1 (ndenvでインストール済み)
Android Studio, Xcode などはインストール済み

インストール

appium, appium-doctor のインストール

$ npm install -g appium
$ npm install -g appium-doctor
$ ndenv rehash

appium-doctor の実行

まず実行して自分の環境に何が足りないか、設定が間違っていないかなどを確認

$ appium-doctor --ios --android

足りないものをインストール

色々と足りないのでインストール & 設定

carthage のインストール

$ brew install carthage

opencv4nodejs のインストール

$ brew install opencv@3
$ brew link opencv@3 --force
$ vi ~/.bash_profile

.bash_profileに以下を追加

export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/Cellar/openssl/1.0.2q/lib/pkgconfig/"
export PATH="/usr/local/opt/opencv@3/bin:$PATH"
export LDFLAGS="$LDFLAGS -L/usr/local/opt/opencv@3/lib"
export CPPFLAGS="$CPPFLAGS -I/usr/local/opt/opencv@3/include"
$ exec $SHELL -l
$ OPENCV4NODEJS_DISABLE_AUTOBUILD=1 npm install -g opencv4nodejs

ffmpeg のインストール

$ brew install ffmpeg

fbsimctl のインストール

$ brew tap facebook/fb
$ brew install fbsimctl --HEAD

applesimutils のインストール

$ brew tap wix/brew
$ brew install applesimutils --HEAD

idevicelocation のインストール

$ brew install usbmuxd libplist libimobiledevice libzip openssl make automake autoconf libtool pkg-config
$ brew list openssl
(省略)
/usr/local/Cellar/openssl/1.0.2q/lib/pkgconfig/ (3 files)
(省略)
$ vi ~/.bash_profile

.bash_profileに以下を追加

export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig/"
$ exec $SHELL -l
$ git clone https://github.com/JonGabilondoAngulo/idevicelocation.git
$ cd idevicelocation
$ ./autogen.sh
$ make
$ sudo make install

ios-deploy のインストール

$ npm install -g ios-deploy

ios-webkit-debug-proxy のインストール

$ brew install ios-webkit-debug-proxy

JAVA_HOMEの設定

$ vi ~/.bash_profile

.bash_profileに以下を追加

export JAVA_HOME=`/usr/libexec/java_home -v 10`
export PATH="$PATH:$JAVA_HOME/bin/"
$ exec $SHELL -l

bundletool.jar のインストール

$ mkdir ~/bin/
$ cd ~/bin/
$ wget https://github.com/google/bundletool/releases/download/0.8.0/bundletool-all-0.8.0.jar
$ ln -s bundletool-all-0.8.0.jar bundletool.jar
$ chmod a+x bundletool-all-0.8.0.jar
$ vi ~/.bash_profile

.bash_profileに以下を追加

export PATH="$PATH:$HOME/bin/"
$ exec $SHELL -l

再度 appium-doctor の実行

$ appium-doctor --ios --android
(前略)
info AppiumDoctor
info AppiumDoctor Everything looks good, bye!
info AppiumDoctor

Everything looks good, bye! と出ていれば問題無し

ChromeDriver のインストール

AndroidのChromeを動作させるためにChromeDriverをインストールする
Android 9.0のChromeのバージョンに合わせて、ここではChromeDriverのバージョンをv2.44としています

詳細:
https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/web/chromedriver.md
http://chromedriver.chromium.org/downloads

$ wget https://chromedriver.storage.googleapis.com/2.44/chromedriver_mac64.zip
$ unzip chromedriver_mac64.zip
$ mv chromedriver ~/bin/chromedriver_v2_44

npmを使ってテスト環境の構築

普通にディレクトリを作ってテスト環境を構築する

npm init 他

$ mkdir sptest
$ cd sptest
$ npm init -y
$ npm install codeceptjs --save-dev

codecept.jsの初期化

$ npx codeceptjs init

  Welcome to CodeceptJS initialization tool
  It will prepare and configure a test environment for you

Installing to /Users/xxxx/sptest
? Where are your tests located? ./*_test.js
? What helpers do you want to use? Appium
? Where should logs, screenshots, and reports to be stored? ./output
? Would you like to extend I object with custom steps? Yes
? Do you want to choose localization for tests? ja-JP
? Where would you like to place custom steps? ./steps_file.js
Configure helpers...
? [Appium] Application package. Path to file or url http://localhost
? [Appium] Mobile Platform iOS
? [Appium] Device to run tests on emulator
Steps file created at /Users/xxxx/sptest/steps_file.js
Config created at /Users/xxxx/sptest/codecept.conf.js
Directory for temporary output files created at `_output`
Almost done! Create your first test by executing `codeceptjs gt` (generate test) command

--
Please install dependent packages locally: npm install --save-dev webdriverio@^5.2.2
$ npm install --save-dev webdriverio@^5.2.2

実際のテスト

appiumの起動

$ appium --chromedriver-executable ~/bin/chromedriver_v2_44

テストの記述

前回と同じ、github.comに行って、"GitHub"という文字列があるかどうかをチェックするだけのコードを書きます。

github_test.js

Feature('Github');

Scenario('test something', (I) => {
  I.amOnPage('https://github.com');
  I.see('GitHub');
});

iOS用の設定ファイルを作成

自動で作成されてますが、それを以下のようにいじります。

codecept.ios.conf.js

exports.config = {
  tests: './*_test.js',
  output: './output',
  helpers: {
    Appium: {
      platform: "IOS",
      desiredCapabilities: {
        "platformName": "iOS",
        "platformVersion": "12.1",
        "deviceName": "iPhone 7",
        "automationName": "XCUITest",
        "browserName": "Safari"
      }
    },
  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'test',
  translation: 'ja-JP'
}

Android用の設定ファイルを作成

同様にAndroid用も

exports.config = {
  tests: './*_test.js',
  output: './output',
  helpers: {
    Appium: {
      platform: "Android",
      desiredCapabilities: {
        automationName: "Appium",
        deviceName: "Nexus 5x API 28 for appium",
        platformVersion: "9",
        browserName: "Chrome"
      }
    },
  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'test',
  translation: 'ja-JP'
}

テスト実行

iOSにしろAndroidにしろ、問題があればappiumを起動しているターミナルに何かしらエラーが出ているので解読して下さい

iOS

以下のコマンドを実行すると、初回時に必要な設定を自動でしてくれた上でSafariが起動してテストが実行されます

$ npx codeceptjs run --steps --config=./codecept.ios.conf.js

Android

以下のコマンド実行の前にAndroid StudioからAndroidエミュレータを立ち上げておきます

Name : Nexus 5x API 28 for appium
OS : Pi (Android 9.0)

$ npx codeceptjs run --steps --config=./codecept.android.conf.js

npm runで実行できるように package.json を修正

package.json

{
  "name": "sptest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "npm run test:ios; npm run test:android",
    "test:ios": "codeceptjs run --steps --config=./codecept.ios.conf.js",
    "test:android": "codeceptjs run --steps --config=./codecept.android.conf.js"

  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "codeceptjs": "^2.0.7",
    "webdriverio": "^5.7.1"
  }
}

iOS

$ npm run test:ios

Android

$ npm run test:android

iOSとAndroidの逐次実行

$ npm run test

終わりに

これで前回のSelleniumと合わせて、ブラウザテストが一通り出来るようになったので、これを使ってガリガリE2Eテスト書いていきます

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

React NativeでURL をパースする

経緯

React Naviveを使ったアプリで、Deep LinkやPush通知のペイロードに格納されたURLを解析したいのですが、有効な方法が見つからないのでiOS/Androidデバイスのライブラリを使いそれらをパースするコードを実装しました。
覚えているうちに記事にします。
もっと簡単な方法があれば誰か教えて下さい・・・(非同期は何気に扱いにくいし)

方針

iOS, AndroidともにURLを解析するAPIがあるので単純にそれらを呼び出します。
ネイティブコードを呼び出すので、この機能の戻り値はPromiseにしています(callbackよりは使いやすいと思うので)。
APIはクエリーパラメータについては、&で結合された文字列だけが戻ってくるので、JSでテキスト処理しています。
例外処理やエラー処理は適当です(参考実装ということで)。

完全なコード

こちらにあります。
https://github.com/flipfrog/react-native-url-parse

API

こんな感じで使用します。

import URLParse from './URLParse';
:

  async componentDidMount(): void {
    const url = await URLParse.parse(this.state.urlSpec);

    this.setState({
      protocol: url.protocol, // httpsとかのプロトコルスキーマ
      host: url.host, // ホスト名
      port: url.port, // ポート番号
      path: url.path, // パス
      query: url.query, // クエリーパラメータ
      ref: url.ref, // インデックス
      queryMap: url.queryMap, // クエリーパラメータのマップオブジェクト
    });
  }
URLParse.js
import {NativeModules} from 'react-native';

export default class URLParse {
    static async parse(urlSpec: string) {
        const url = await NativeModules.RNURLParseModule.parse(urlSpec);
        if (url && url.query) {
            const expressions = url.query.split('&');
            const queryMap = {};
            for (let expression of expressions) {
                const values = expression.split('=');
                queryMap[values[0]] = (values[1] ? values[1] : null);
            }
            url['queryMap'] =  queryMap;
        }
        return url;
    }
}

iOS

iOSは、.hと.mファイル(Objective-Cを使いました)を作成するだけです。

RNURLParseModule.h
#if __has_include(<React/RCTBridgeModule.h>)
#import <React/RCTBridgeModule.h>
#else
#import "RCTBridgeModule.h"
#endif

@interface RNURLParseModule : NSObject <RCTBridgeModule>
@end
RNURLParseModule.m
#import "RNURLParseModule.h"

#if __has_include("RCTUtils.h")
#import "RCTUtils.h"
#else
#import <React/RCTUtils.h>
#endif

#import <Foundation/Foundation.h>

@implementation RNURLParseModule {
}

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(parse:(NSString *)urlSpec
                  parseWithResolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
  NSURL *url = [NSURL URLWithString:urlSpec];
  if (url == NULL) {
    NSDictionary *errorDic = @{
                               NSLocalizedDescriptionKey:@"Parse error",
                               NSLocalizedRecoverySuggestionErrorKey:@"Confirm parameter urlSpec."
                               };
    NSError *error = [[NSError alloc] initWithDomain:@"org.reactjs.native.example.URLParseSample.parse"
                                                code:-1 userInfo:errorDic];
    reject(@"Parse error", @"Parse error", error);
  } else {
    NSDictionary *info = @{
                           @"protocol": [url scheme],
                           @"host": [url host],
                           @"port": [url port],
                           @"path": [url path],
                           @"query": [url query],
                           @"ref": [url fragment]
                           };
    resolve(info);
  }
}
@end

Android

Androidは、モジュールとパッケージ定義を作成して、MainApplication.javaでインスタンス化したパッケージを返すようにします。

RNURLParseModule,java
package com.urlparsesample.extension;

import java.net.URL;
import com.facebook.react.bridge.Promise;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import java.util.HashMap;
import java.util.Map;

// package private class
class RNURLParseModule extends ReactContextBaseJavaModule {
    private static final String E_URL_PARSE_ERROR = "URL Parse error";

    RNURLParseModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "RNURLParseModule";
    }

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("IsAndroid", true);
        return constants;
    }

    @ReactMethod
    public void parse(final String urlSpec, Promise promise) {
        WritableMap info = Arguments.createMap();
        try {
            URL url = new URL(urlSpec);
            info.putString("protocol", url.getProtocol());
            info.putString("host", url.getHost());
            info.putString("path", url.getPath());
            info.putInt("port", url.getPort());
            info.putString("query", url.getQuery());
            info.putString("ref", url.getRef());
            promise.resolve(info);
        } catch (Exception e) {
            promise.reject(E_URL_PARSE_ERROR, e);
        }
    }
}
RNURLParsePackage.java
package com.urlparsesample.extension;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

public class RNURLParsePackage implements ReactPackage {

    @Override
    public List<NativeModule> createNativeModules (ReactApplicationContext context) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new RNURLParseModule(context));
        return modules;
    }

    // Deprecated RN 0.47
    // @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext context) {
        return Collections.emptyList();
    }
}
MainApplication.java(抜粋)
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
              new RNURLParsePackage() // ここに追加します
      );
    }

実行結果

先に記載したGitHubのコードを実行すると下記のように結果を表示します。

ScreenShot_S.png

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

TextInputLayout - password visibility toggleのデザイン変更

:lock: はじめに

Androidのマテリアルデザインに沿った入力フィールドを作成するのに、Android Design Support Libraryの TextInputLayout を使うことが多いかと思います。これにはパスワードの表示/非表示を切り替える機能( password visibility toggle )が備わっているのですが、デザインを変更する際に少しハマったのでメモします。

:lock: 実装

変更箇所

パスワードの表示/非表示を切り替えるボタン( password visibility toggle button )のアイコンと色をそれぞれ切り替えます。

失敗例

ic_password_toggle.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="false">
        <bitmap android:src="@drawable/ic_lock" android:tint="#9e9e9e" />
    </item>
    <item android:state_checked="true">
        <bitmap android:src="@drawable/ic_lock_open" android:tint="@color/colorAccent" />
    </item>
</selector>
activity_main.xml
<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:hintAnimationEnabled="true"
    app:hintEnabled="true"
    app:passwordToggleDrawable="@drawable/ic_password_toggle"
    app:passwordToggleEnabled="true">

    <android.support.design.widget.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hint_password"
        android:inputType="textPassword" />

</android.support.design.widget.TextInputLayout>

パッと見これで問題なさそうですが、 passwordToggleTint を指定していないので、デフォルトで塗りつぶされてしまい、色が切り替わりません。

成功例

ic_password_toggle.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_lock" android:state_checked="false" />
    <item android:drawable="@drawable/ic_lock_open" android:state_checked="true" />
</selector>
tint_password_toggle.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#9e9e9e" android:state_checked="false" />
    <item android:color="@color/colorAccent" android:state_checked="true" />
</selector>
activity_main.xml
<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:hintAnimationEnabled="true"
    app:hintEnabled="true"
    app:passwordToggleDrawable="@drawable/ic_password_toggle"
    app:passwordToggleEnabled="true"
    app:passwordToggleTint="@color/tint_password_toggle">

    <android.support.design.widget.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hint_password"
        android:inputType="textPassword" />

</android.support.design.widget.TextInputLayout>

passwordToggleDrawablepasswordToggleTint に分けてそれぞれ設定するのがミソです。

screen.gif

:lock: まとめ

公式のリファレンスはちゃんとに読みましょう(戒め)。一応 GitHub にサンプルをアップしておきます。

:lock: 参考文献

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

androidでsecret tokenをrepositoryに入れない

これ cleanしたら、gradle.properties 消える? じゃあだめだこれ


gradle.properties をgitignoreしたうえで、参照する。

// build.gradle
android {
    defaultConfig {
        resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
    }
// gradle.properties
GOOGLE_MAPS_API_KEY=AIzaYOURAPIKEY

あとは普通にこういうやつで参照できる。

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="@string/google_maps_key" />

references

Hiding API keys from your Android repository – Code Better – Medium https://medium.com/code-better/hiding-api-keys-from-your-android-repository-b23f5598b906
https://stackoverflow.com/a/51582501/104080

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

osmdroid を使ってAndroid に国土地理院の地図画像を表示する

osmdroid を使って OpenStreetMap の地図を Android に表示する
の続きです

国土地理院の地図画像

国土地理院は、測量及び地理情報を扱う行政機関です。

以前は、5万分の1地形図のような紙媒体で公開していた。
最近は、電子データ化を推進している。

地理院タイルという名称で地図画像をXYZ方式で配信している。

地理院タイル一覧
https://maps.gsi.go.jp/development/ichiran.html

利用条件
国土地理院のサーバ上にある地理院タイルを、リアルタイムで読み込み表示するウェブサイトやソフトウェア、アプリケーションを製作する場合、地理院タイルは出典の明示のみで申請不要でご利用いただけます。

ということなので、ありがたく利用されて頂く。

osmdroidのXYTileSource

XYZ方式で提供されているタイルを使用するためのクラス

前回の記事で、OSM日本のサーバーを指定しいたところを、
地理院タイルのサーバーを指定する

       // OSM日本
        final ITileSource tileSource = new XYTileSource("FietsRegionaal",  3, 18, 256, ".png",
                new String[] { "http://tile.openstreetmap.jp/" });

       // 地理院タイル
        final ITileSource tileSource = new XYTileSource("GSI",  3, 18, 256, ".png",
                new String[] { "https://cyberjapandata.gsi.go.jp/xyz/std/" });

スクリーンショット

左はOSM地図、右は地理院タイル
10_yokohama_osm.png 10_yokohama_gsi.png

空中写真
10_kannai_osm.png 10_kanai_photo.png

github にサンプルコードを公開した

https://github.com/ohwada/Android_Samples/tree/master/Osmdroid10

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

AndroidStudioでinstall_failed_verification_failureというエラーが出た時

原因

どうやら、セキュリティ系のソフト(GooglePlayストアのPlayプロテクトやその他のアンチウイルス系ソフトなど)が原因の場合が多いよう。

対処

スキャンなどをオフにする。僕の場合はPlayプロテクトが原因だったので、その対処方法。
まずGooglePlayストアアプリを開いて、メニューを出し、Playプロテクトという所を開く。
そして「端末をスキャンしてセキュリティ上の脅威を確認」という項目をOFFにする。これで僕の場合は治りました。

注意

もちろん安全性を犠牲にすることになるので、開発時以外はOnに戻すのを忘れないように。
それか、いっそ開発用のデバイスが欲しいなと、僕は思いました。

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

公開したアプリがポリシー違反で非承認となった件

昨日アップロードしたアプリ
はつ恋_180x120.png   視力回復文学:はつ恋
が「非承認」となりました。

スクリーンショット 2019-03-10 8.05.52.png

公開ステータス: 否承認
 審査の結果、お客様のアプリは、ポリシーに違反していると判断されたため、否承認となり公開されませんでした。送信したのがアップデートの場合は、引き続き以前のバージョンのアプリが Google Play に掲載されます。

問題: メタデータに関するポリシーへの違反
 誤解を招くメタデータ、無関係なメタデータ、過度のメタデータ、不適切なメタデータをアプリに設定することは認められておりません。メタデータには、アプリの説明、デベロッパー名、タイトル、アイコン、スクリーンショット、プロモーション画像などが含まれます(ただしこれらに限定されません)。
ストアの掲載情報に表示されるコンテンツは、(ログインしているか、していないかに関わらず)全ユーザーに表示されるため、すべてのユーザーに適したものである必要がございます。不適切なテキストではあるものの、それがアプリの説明に不可欠な場合は、ストアの掲載情報内ではアスタリスクなどを使用して伏せ字にしてください。画像につきましては、一般ユーザーに適したものに差し替えていただくか、ストアの掲載情報から削除していただきますようお願いいたします。

お客様のストア掲載情報のコンテンツ(説明、タイトル、アイコン、スクリーンショット、動画、プロモーション画像など)は、以下の問題に該当しております。

メタデータやロゴに不適切なテキストが含まれている(冒涜的、下品、攻撃的な表現など)

 対処方法: アプリを送信して再審査を受ける
 1.メタデータに関するポリシーを確認し、アプリを修正(アプリの説明、タイトル、アイコン、スクリーンショット、プロモーション画像がすべてのユーザーに適したものになるよう変更)。
 2.アプリが他のすべてのデベロッパー プログラム ポリシーに準拠していることを確認(再度ポリシーに違反した場合、追加の措置を取らせていただくことがございます。
 3.Play Console にログインして、アプリのアップデートを送信。

 ポリシーをご確認のうえ、今回の決定が誤りだと思われる場合は、お手数ですが弊社のポリシー サポートチームにお問い合わせください。2 営業日以内にご連絡いたします。

 アプリの説明文に問題があるということなので、修正し、再審査の依頼を行いました。
 このメールを受け取った後でもGoogle Play Console では「公開中」のままです。このメールを受け取った後でも「非公開」になっていないようです。
スクリーンショット 2019-03-10 8.13.21.png

 何が原因なのか?まずは、掲載していたアプリの説明文です。

3DVR等のように平行法を用いて文学を読むことでストレスの溜まった目をストレッチするためのアプリです。はつ恋(ツルゲーネフ作・神西清訳:青空文庫より)

・画面の見方
 まずは3m程度先を見て、すぐにスマホの画面を見るようにします。そして左右に表示された文字が一つに重なって見えるようスマホの距離を調整しましょう。文字が重なったらゆっくりとスマホを離して、はっきりと文字が見えつつできるだけ離した状態にしてください。あとは読むだけです。最初は時間がかかるかもしれませんが慣れるとすぐにできるようになります。

・ページの移動
 画面の何も表示されていないところをスワイプ(フリック)するとページ送り/戻しができます。
 中央に表示されているゲージをドラッグすると大きく移動することができます。

ポイント)
 画面を平行法で見ることで寄り目でなくなり、スマホなど近距離を長時間見て負荷のかかった目をストレッチする効果が期待できます。

ご注意)
 視力回復文学となっておりますが視力が回復することを保障するものではありません。
 長時間のご使用は避けてください。長時間見続けると目を痛めたり、気分が悪くなったり、頭痛、吐き気などを催す可能性があります。少しでも違和感を感じた時は使用することをおやめください。

ご注意)
 このアプリを電車内で使用することは避けてください。電車に座った状態でこのアプリを使用しますとあなたはアプリを眺めていても周りから見ると目線が少し先のほうを見ているように見えますので、向かいに座った人から「あの向かいの席に座っている紳士はァッッッ!スマホを眺めているフリをしてずっと私の事を見ているゥッッッ!まるで19世紀のロシア文学に酔いしれているような目つきでェェェェッッ!これはっ!紛れもなくッ!セクシュアルゥハラスゥメントゥゥゥッ!」と誤解されてしまうかもしれません。ご注意ください。

解説)
 この解説はアプリ開発者個人の意見であり、なんの医学的根拠もない妄想と理解して読んでください。
 スマホや読書などでずっと近くを見続けていると疲れ目となりますが、これは長い間近くにピントを合わせているから起こるというより、近くを見ることで寄り目の状態が続くことにより発生すると私は考えています。本を読むために作成した近距離用のメガネで本を読んでも同様に疲れ目になるという私の経験から辿り着いた考察です。
 東アジア人は他の地域に比べて近視の割合が多いとされていますが、その理由の一つとして目が細いことが挙げられるのではないでしょうか。寄り目になると角膜の上部と下部がまぶたに触れるため角膜を押さえる力が加わります。それが長時間続くことで角膜表面が歪むので一時的な乱視の状態(画像がぶれて見える)となるのではないかと私は考えております。
 この一時的な乱視の状態でもはっきりとブレないように見る方法があります。それが「さらに近づけて見る」ことです。そうです、どんどん、どんどん、スマホや本の距離が近づいていきます。そもそも、人間の目にそんな近くを見る機能はないのですが人間の目は最後の手段、眼軸の延長を行います。眼軸を延長することでより近くを見ることができるようになります。そしてその代償として遠くを見ることができなくなります。そして軸性近視と呼ばれる「絶対に治らない近視」になってしまいます。
 現在、伸びてしまった眼軸を短くする方法は見つかっていません。宇宙飛行士は宇宙空間に滞在すると眼軸が伸びるという話がある程度です。

 視力回復とまでは言いませんが、ご自分の目が今以上に悪くならないよう目に対する興味、知識(この解説は妄想やデタラメばかりなのでご自分で調べてみてください)を高めていただくことのきっかけとなれば、こんなクソアプリの説明を読んだことも時間の無駄にはならないかなと思います。
 ということで、視力回復文学をよろしくお願いいたします。

 どうでしょう?どこが問題になったのでしょうか?
 私の予想では、おそらく最後の締めに出てくる「クソアプリ」という単語が引っかかったのではないかと考えています。セクシャルハラスメントの下りも「下品」であるとされたのかもしれません。ご注意から解説までごっそり削って、再認証の依頼をしておきました。

 3回目の非承認です。
 1回目はよみあげ!の説明文に「初音ミクの声で」と書いていたことが「商標」を記入しているということで非承認になりました。その後公開されました。
 2回目は頭痛アプリ
スクリーンショット 2019-03-10 8.36.28.png

Healing sound app for headache(パッケージ ID )は、審査の結果、虚偽の振る舞いに関するポリシーに違反しているため、Google Play での公開が停止され、削除されました。
 (※パッケージ IDは引用時に削除しました)
 まぁ、これは仕方ないかな。実際、時々(笑)、治ることもあるんだけども。これはそのまま再審査の請求もしなかった。

 で、今回が通算3回目。

 昨日、非承認を受けてふと思ったんだけど。他人から見ると私は随分とふざけた人間なのだろうな。いたるところでそういう部分がふと滲み出てしまって周りの人を不愉快にするのだと思う。人生が上手くいかないのもそういうところなんだろうなと思う。自分では結構真面目なつもりなんだけれど。
 monochrome:目に優しいモノトーンのブラウザ
の説明文はとてもふざけているけどなんとか常識の範囲内に収まっているのだろうな。

 自分ではその境界線がわからないのだ。そうだな。思い起こせば私の人生はそういうことが多くあったな。まぁ、いいけど。

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

Fragment と Toolbar の歴史の話

この辺りは Android 初学者を混乱させる存在なのだけれど、人に説明していて結構骨が折れた&いい資料があるといいなと思ったので。

:skull_crossbones: 事実誤認とかあるかもです。鵜呑みにしないでね。

TL;DR

  1. AndroidX を入れる
  2. AppCompatActivity を継承して Activity を実装する
  3. Theme.AppCompat.NoActionBarTheme.MaterialComponents.NoActionBar など、NoActionBar 系のテーマを、アプリケーションのテーマの parent に指定する
  4. Layout XML 上で、androidx.appcompat.widget.Toolbar を配置する
  5. setSupportActionBar()Toolbar を ActionBar として設定する

Android にはこのような お作法 があり、新しくプロジェクトを作ると勝手にやってくれます。

この辺について疑問に思った人向けの歴史的経緯の説明です。

Android 2.X 時代

この時代にはまだ Fragment はなく、ひとつの画面を作るのにひとつの Activity を定義していました。

タブのように複数の画面をネストするような画面を実現するには、複数の Activity を管理する ActivityGroup というものを使う必要がありました。

TabActivity なんてものもあったよね」とぼんやり記憶してる人がいるくらいだと思いますが……。

タイトルバー

Android が iOS と比べて興味深かった点は、Activity をエントリポイントとして、複数のアプリケーションを跨った協調動作ができた点で、このためか Activity は自身を表すラベルをタイトルバーに表示していました。

Hello Legacy Android World!

もっとも、「見た目が悪い」「iOS とデザインを合わせたい」などの理由により、真っ先に以下のようなコードで抹消される運命でした。

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  getWindow().requestFeature(Window.FEATURE_NO_TILE);
  // Activity#requestWindowFeature() という糖衣記法もある
  setContentView(R.layout.main);
}

現在では style.xml でベーステーマに NoActionBarNoTitleBar を指定するか、または自分でアプリテーマに以下の記述をするため、

<item name="android:windowNoTitle">true</item>

コードから制御することは稀だと思いますが、同じ処理が行われます。

ここでひとつのポイントなのが、タイトルバーというものは Window の機能であった という点です。

昔はたくさんあった物理ボタン

Android 10 では back button すら消えるという噂を耳にしたのですが、昔は Back / Menu / Home / Search の4種類のボタンがありました。

Search ボタンが一級市民というところに、検索事業から始まった Google の趣向を感じ取っていたのですが、いつの間にか消えてました。

いまから Android 開発に触れる場合、「ToolBar に表示させる MenuItemActivity#onCreateOptionsMenu() で作成するのはなぜ?」 という素朴な疑問を抱くと思うのですが、オプションメニューは物理ボタンを押すと表示されるものだった というところを踏まえると、納得がいくと思います。

Android 3.X 時代

いまは Android のバージョンは単に大きい方が新しいだけでしたが、2.X は Phone 端末向け、3.X は Tablet 端末向けという意味合いがありました。

ActionBar の登場

Android 4.x時代のアプリにないと残念なActionBarとは という記事に ActionBar の解説が画像入りであります。

この記事にもあるように、ActionBar とは 2.X のタイトルバーが進化したものです。具体的にどう強化されたのかというと、

ロゴアイコンが表示される

デフォルトのタイトルバーは Activity のラベルしか表示されないため、どのアプリの Activity なのかはわかりませんでした。ActionBar はロゴアイコンを表示することができるようになりました。

この時代、ActionBar の色の変更を手軽に行うことはできず、かわりに ロゴアイコンを表示させることで、アプリの個性を表現することが推奨 されていました。

Android 5.0 からは色を使ってブランディングするという方向にシフトしたため、現在の Toolbar はロゴを表示していません。

メニューアイテムを表示できる

2.X では物理メニューボタンを使って、オプションメニューを表示させていました。換言すれば、オプションメニューが用意されているのかどうかは、メニューボタンを押すまで分からない ということです。

常時メニューが表示されるようになり、メニューに機能を持たせやすくなりました。

その他の機能

  • タブ・ドロップダウンメニューを表示する機能(setNavigationMode()
  • ActionBar を上下に分割する機能(Split Action Bar)

などが存在しました。この辺は闇に葬られたので知らなくても仔細ありません。

長らく苦しめられた ActionBar の互換性の問題

Android 2.X ではタイトルバーを消すことが多かったのですが、ActionBar はタイトルバーが進化したもののため、requestFeature(Window.FEATURE_NO_TILE) を行うと ActionBar も消えてしまう という問題がありました。

特にこの問題は、Phone 端末でも ActionBar を表示するようになった 4.0 以降に「Android らしいアプリを作りたい」という Androider を大いに悩ませる問題でした。

  • 2.X ではタイトルバー / 4.X では ActionBar を表示する
  • ActionBar モドキの View を自作する
  • ActionBarSherlock という非公式のバックポートライブラリを使う
  • 諦める

辺りが主力な選択肢だったような気がします。

冒頭の記事が、「Android 4.x時代のアプリにないと残念なActionBar」 と書いていたのは、諦めを選択して ActionBar を非表示にしていたアプリが少なくなかった からです。

のちに公式で ActionBarActivty が登場して互換性の問題は解決するのですが、2〜3 年待たされたように思います。

Fragment の登場

Tablet 端末と Phone 端末とでは、ディスプレイサイズの違いから、表示できる情報量に違いがあるため、Activity のユーザーインターフェースを複数のモジュールに 断片化 させることで、両対応を行おうという発想で登場したのが Fragment です。

公式のデベロッパーガイド の以下の画像が分かりやすいと思います。

公式の画像

実際のところ、この試みはうまくいかなかったように思いますがどうなんでしょう?

Fragment の断片化

バックポートライブラリの登場が遅かった ActionBar とは異なり、Fragment は比較的早くに Support Package 版が登場しました。

現在、Fragment のクラスは以下の 3 つが存在しています。

  • android.app.Fragment (もはや使うべきではない)
  • android.support.v4.app.Fragment (AndroidX に移行するべき)
  • androidx.fragment.app.Fragment

Android SDK に存在する本来の Fragment を Fragment(真)、Support Library や AndroidX の Fragment を Fragment(偽)と個人的に呼んでいるのですが、これらは異なるクラスなので、

  • Fragment(真)を管理する FragmentManagerActivity#getFragmentManager() で取得する
  • Fragment(偽)を管理する FragmentManagerActivity#getSupportFragmentManager() で取得する

というような違いがあったりして、この辺も初学者を的確に躓かせるトラップして秀逸だったと思います。

android.support.v4.app.Fragmentandroidx.fragment.app.Fragment も別物なので、本来は混在させることができない のですが、Jetifier というツールがコンパイル時に置き換えるという魔法的手段を使って解決してます。

Fragment を使うのは避けた方が良いのではないか?

Activity の上で動作する Fragment のライフサイクルは複雑で、実装ミスを誘発しやすく、ごく稀に起きる謎のクラッシュに悩まされたりということがあり、このため一時期は Fragment に対してネガティブな意見を見かけることがありました。

Advocating Against Android Fragments(日本語訳:【翻訳】Android Fragmentへの反対声明)が一番盛り上がった時期だったかなと思います。

この時代の古文書を読むと「Fragment は使うべきではない」という強い言葉を見かけたりもしますが、Android Architecture Components の登場によって、ライフサイクルに応じた処理の書き方に選択肢が生まれ、View とロジックの切り離しがやりやすくなった現在では、ほぼほぼ気にしなくていいと思います。

近い将来としては、SingleActivity の上に Fragment で画面を構築した上で、それを Navigation components で紐付けることで、直接 FragmentManager を扱うコードは減っていくものと思います。

Up と Back の概念

ActionBar には Up ボタンがあるのですが、これには Back ボタンは別のセマンティクスがあります

ただし、この違いは Navigation components が普及するとともに緩やかに消失していくと考えているので、特に詳述しません。

Android 5.0 時代

もう minSdk21 にしても許されますよね……?

Toolbar の登場

Android 5.0 でこの記事の本題である Toolbar が登場します。これは見た目としては ActionBar と同一です。

テーマに NoActionBar を指定した上で Toolbar を ActionBar として使用する……、という Android 初学者の急所を抉ることに特化した形をしています。

何を解決するものなのか?

本来の ActionBar とはタイトルバーが進化したもので、Window が管理しているものである という歴史的な経緯を踏まえると、なんとなく分かるのかなと思います。

ちょっと語弊のある図ですが、両者の違いはこういう感じです。

window_app.png

ユーザーがレイアウトで定義したアプリケーションの View は Window の上に表示されます。

ActionBar(真)を使うとき、その ActionBar は アプリケーションが管理している View の外側に、Window の機能として表示されます。現在では考慮する必要性がほとんどないものの、古代の端末ではタイトルバーで表示されるかもしれないし、ActionBar が持つ機能というのも、すべてのバージョンで同じわけではなく、カスタマイズ性も乏しいです。

テーマに NoActionBar を指定すると、アプリケーションは ActionBar(真)が表示されていた部分も、アプリケーションが管理する View 領域にすることができます。その上で、ActionBar(真)の代替としてレイアウトファイルに Toolbar を配置します。

これは アプリケーションの View の一部なので、Android OS のバージョンに依存することがなく 、実装もただの View なので、見栄えやスクロールなどのインタラクションと連動させるような カスタマイズが非常に容易 となっています。

Toolbar(真)について

Fragment がフラグメンテーションを起こしていたように、Toolbar も 3 種類あります。

  • android.widget.Toolbar (使うべきではないというか、存在意義がわからない)
  • android.support.v7.widget.Toolbar(AndroidX に移行するべき)
  • androidx.appcompat.widget.Toolbar

minSdk 21 の時代が訪れたとすれば、androidx.appcompat.widget.ToolbarsetSupportActionBar() するのではなく、android.widget.ToolbarsetActionBar() するという選択肢も取れるように思います。

しかしながら、外部ライブラリである androidx.appcompat.widget.Toolbar は最新のすべての機能を使えるのに対して、Android SDK の一部である android.widget.Toolbar は API レベルによっては使えない機能があるので、つねに AndroidX の Toolbar(偽)を使った方がいいです。

Theme.AppCompat.Light.NoActionBar テーマは何をしているの?

こんなの興味ある人がいるのか不明ですが、テーマの定義を見ると、以下のようになっています。

<style name="Theme.AppCompat.Light.NoActionBar">
     <item name="windowActionBar">false</item>
     <item name="windowNoTitle">true</item>
</style>

ここで windowNoTitle が、requestFeature(Window.FEATURE_NO_TITLE) を呼び出しているんだろう……と、考えるところですが、android:windowNoTitle ではなくwindowNoTitle という点に引っ掛けがあります。

AppCompat のテーマを遡ると、 L 以上の場合、もともと NoActionBar テーマを継承しているのです。

v21/values-v21.xml
<style name="Platform.V21.AppCompat.Light" parent="android:Theme.Material.Light.NoActionBar" />

L 未満の場合はというと、android:windowNoTitle を true に設定にしています。

values.xml
<style name="Platform.AppCompat.Light" parent="android:Theme.Holo.Light">
  <item name="android:windowNoTitle">true</item>
  <item name="android:windowActionBar">false</item>
  <!-- 略 -->
</style>

これが意味するところというのは、AppCompat のテーマを使った時点で、requestFeature(Window.FEATURE_NO_TITLE) が必ず呼び出され、プラットフォーム本来の ActionBar が使われることはない ということです。

このとき表示される ActionBar は、AppCompat が用意する ActionBar(偽)です。

android: プレフィクスのついていない、windowNoTitlewindowActionBar は、AppCompatDelegate が受け取り ActionBar(偽)の表示制御に使われます。

windowNoTitle windowActionBar 結果
true - 表示されない
false true 表示する

windowNoTitle を true に設定した場合、ActionBar が表示されることはないので、windowActionBar が判定されることはありません。

if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
  requestWindowFeature(Window.FEATURE_NO_TITLE); 
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
  // Don't allow an action bar if there is no title.
  requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む