20191203のAndroidに関する記事は16件です。

zxing-android-embedded を使ってみる (Kotlinで)

はじめに

この前、授業成果物として簡易的なQRコード読み取りアプリケーションを制作したときに使ったライブラリについてメモを残しておきます。

今回使用したライブラリ

できること

簡単に QR 読み込み機能のアプリケーションが作成できます。
デモのキャプチャ

使ってみる

公式ドキュメントもあり書かれている通りに進めれば、動作できると思います。
あまり面倒な作業は必要なく、インポートするだけで実行可能です。

Gradle ファイルにライブラリを記載する

build.gradle (app) に以下の内容を追記します。

repositories {
    jcenter()
}

dependencies {
    implementation 'com.journeyapps:zxing-android-embedded:4.0.0'
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'com.google.zxing:core:3.3.0'
}

android {
    buildToolsVersion '28.0.3'
}

ハードウェアアクセラレーションを有効にする

AndroidManifest.xmlandroid:hardwareAccelerated="true" を追記します。

<application android:hardwareAccelerated="true" ... >

QR 読み取り用のアクティビティを追記する

QR コード読み取り部分のアクティビティが必要なので、ライブラリに存在している QR コード読み取りアクティビティを AndroidManifest.xml に追記する。

<activity android:name="com.journeyapps.barcodescanner.CaptureActivity"
          android:screenOrientation="fullSensor"
          tools:replace="screenOrientation" />

コードを書く

基本的にコピペで OK です。
適当なサンプルを置いておくので参考にどうぞ!

class QRReaderActivity : AppCompatActivity() {

    internal var qrScanIntegrator: IntentIntegrator? = null

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

        qrScanIntegrator = IntentIntegrator(this)

        // 画面の回転をさせない (今回は縦画面に固定)
        qrScanIntegrator?.setOrientationLocked(false)

        // QR 読み取り後にビープ音がなるのを止める
        qrScanIntegrator?.setBeepEnabled(false)

        // スキャン開始 (QR アクティビティ生成)
        qrScanIntegrator?.initiateScan()
    }

    // 読み取り後に呼ばれるメソッド
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        // 結果の取得
        val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)

        if (result != null) {
            // result.contents で取得した値を参照できる
            Toast.makeText(this, result.contents, Toast.LENGTH_LONG).show()
        }

        else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }
}

これで QR コードを読み取るアプリを簡単に作成することができます!

まとめ

正直、自作するのだるいですからね。サボれるところはサボってしまいましょう。
今回は QR コードを読み込む最低限の機能しかないアプリケーションを作成しましたが、サンプルを見た感じだともっと細かく作ることもできそうだなあと思いました。楽に QR コードを読み込むアプリケーションを作成したい人は使ってみてはどうでしょうか
ありがとうございましたー 🥳

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

Androidアプリのデバッグ中に "Source code does not match the bytecode"というエラーが出る

エラー内容

Android Studioでアプリをデバッグ中に、以下の画像に示す
Source code does not match the bytecode
というエラーが出て正常にデバッグができない際の対処法です。

ZLHg5.png

原因

Gradleで参照しているコードが最新版でない。

解決方法

その1

以下のコマンドを実行して、Gradleの依存関係をリフレッシュする。

./gradlew build --refresh-dependencies

参考

https://stackoverflow.com/questions/39990752/source-code-does-not-match-the-bytecode-when-debugging-on-a-device

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

Android 2.xなどで基本認証が通らないときのTIPS

話題:ベーシック認証のトラブル

Android 4.x以降では発生する頻度が下がったが、Android 2.x時代に頻発したトラブル。

基本認証で保護したページを、他の端末からは閲覧できるのに、
Android 2.xの端末からだけは閲覧できない

何回id/pwを入力しても、幾度でも尋ね直してくる無間地獄。

解決方法

前提

URI ユーザ名 パスワード
https://foo.net user password

の場合

解決方法

https://user:password@foo.net

これだけ。
もしSSL未対応なら

http://user:password@foo.net

でも全然OK。

いったんこのURIでアクセスした後は既に認証が通っている状態になるらしく

https://foo.net
http://foo.net

とアドレス欄の内容を修正してから再アクセスしてもid/pwを尋ね直されることは、もう、ない。

注意点・リスク

  • idとpwの文字列が(リクエストURIとして)インターネット上を流れていく、ということの危険性を理解し、割り切ったうえで使用せねばなりません。
  • ひとたびhttps://user:password@foo.netでアクセスすると、サイト内の遷移リンク、たとえば<a href="directory">link</a>を押下しても、https://user:password@foo.net/directory/という具合にホスト名の前にuser:passwordズ~ッとついて来続けます(外れません)。
    • つまり、その試験中のスクリーンショットを開発チーム内で共有したりすると、ついでにid/pwを喧伝しているようなことになります。危険

*あとがき

ベーシック認証をURLに直接書く
を参照にしました。というかこのテクニック自体は他の場面で以前から利用していたけれど、 Android 2.xでのトラブルに効く、と教えてくれたのは仕事場の同僚のおかげです。感謝。

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

Android Data Binding(Codelabs for the 2019 Android Dev Summit: Mountain View, October 23-24, 2019.)

Android Dev Summit 2019
Codelabs for the 2019 Android Dev Summit: Mountain View, October 23-24, 2019.

Android Data Bindingの日本語訳

1.はじめに

Data Binding Library

Data Binding LibraryはAndroid Jetpack libraryの一つであり、プログラムではなく宣言形式を使用して、XMLレイアウト内のUIコンポーネントをアプリのデータソースにバインドできます。
これにより、定型コードを削減できます。 このコードラボでは、次の方法を学習します。

  • 既存のアプリでデータバインディングを設定する
  • レイアウト式を使用
  • 監視可能なデータオブジェクトの使用
  • カスタムバインディングアダプターを作成

ビルドするもの

このコードラボでは、このアプリをデータバインディングに変換します。
image.png
このアプリには、いくつかの静的データといくつかの監視可能なデータを表示する1つの画面があり、データが変更されると、UIが自動的に更新されます。
データはViewModelによって提供されます。Model-View-ViewModelはデータバインディングで非常にうまく機能するプレゼンテーションレイヤーパターンです。以下に図を示します。
image.png
アーキテクチャコンポーネントライブラリのViewModelクラスにまだ慣れていない場合は、公式ドキュメントをご覧ください。要約すると、ビューにUIの状態(アクティビティ、フラグメントなど)を提供するクラスです。向きの変更に耐え、アプリの残りのレイヤーへのインターフェイスとして機能します。

必要なもの

Android Studio 3.4以降

2.データバインディングなしでアプリを試す

この手順では、コードラボ全体のコードをダウンロードしてから、簡単なサンプルアプリを実行します。

$ git clone https://github.com/googlecodelabs/android-databinding

または、リポジトリをzipファイルとしてダウンロードできます:

  • 1 Zipをダウンロード
  • 2 コードを解凍します
  • 3 Android Studio(バージョン3.4以降)でプロジェクトを開きます image.png

プロジェクトが開いたら、ツールバーのimage.pngをクリックしてアプリを実行します。
ビルドが完了し、アプリがデバイスまたはエミュレーターにデプロイされると、デフォルトのアクティビティが開き、次のようになります。
image.png

この画面にはいくつかのデータが表示され、ユーザーはボタンをクリックしてカウンターを増分し、プログレスバーを更新できます。SimpleViewModelを使用します。それを開いて見てみましょう。

Ctrl + Nを使用して、Android Studioでクラスをすばやく見つけます。 Ctrl + Shift + Nを使用して、名前でファイルを見つけます。
Macでは、Command + Oでクラスを見つけ、Command + Shift + Oでファイルを見つけます。

SimpleViewModelクラスは以下を公開します。

  • "name"と"lastName"
  • "likes"の数
  • 人気のレベルを説明する値

SimpleViewModelを使用すると、ユーザーはonLike()メソッドで"likes"の数を増やすことができます。
最も興味深い機能はありませんが、この演習にはSimpleViewModelで十分です。
一方、PlainOldActivityクラスにあるUI実装には、いくつかの問題があります。

  • findViewById()を複数回呼び出します。これは遅いだけでなく、コンパイル時にチェックされないため安全ではありません。 findViewById()に渡すIDが間違っていると、実行時にアプリがクラッシュします。
  • onCreate()で初期値を設定します。 自動的に設定される適切なデフォルトを設定する方がはるかに良いでしょう
  • XMLレイアウト宣言のButton要素で"android:onClick"属性を使用しますが、これも安全ではありません。onLike()メソッドがアクティビティに実装されていない(または名前が変更されている)場合、アプリは実行時にクラッシュします。
  • たくさんのコードがあります。アクティビティとフラグメントは非常に急速に成長する傾向があるため、できるだけ多くのコードをアクティビティとフラグメントから移動することをお勧めします。また、アクティビティとフラグメントのコードはテストと保守が困難です。

Data Binding Libraryを使用すると、アクティビティからロジックを再利用可能かつテストしやすい場所に移動することにより、これらの問題をすべて修正できます。

3.データバインディングを有効にして、レイアウトを変換します

このプロジェクトでは既にデータバインディングが有効になっていますが、自分のプロジェクトで使用する場合、最初のステップは、それを使用するモジュールでライブラリを有効にすることです。

build.gradle
android {
...
    dataBinding {
       enabled true
    }
}

次に、レイアウトをデータバインディングレイアウトに変換します。

通常のレイアウトをデータバインディングレイアウトに変換するには:

  1. <layout>タグでレイアウトをラップします
  2. レイアウト変数を追加する(オプション)
  3. レイアウト式を追加する(オプション)

plain_activity.xmlを開きます。これは、ConstraintLayoutをルート要素とする通常のレイアウトです。
レイアウトをデータバインディングに変換するには、ルート要素を<layout>タグでラップする必要があります。また、名前空間定義(xmlns:で始まる属性)を新しいルート要素に移動する必要があります。
image.png
レイアウトは次のようになります。

plain_activity.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools">
   <data>

   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">

       <TextView
...

<data>タグにはレイアウト変数が含まれます。
レイアウト変数は、レイアウト式を記述するために使用されます。レイアウト式は要素属性の値に配置され、@ {expression}形式を使用します。 ここではいくつかの例を示します。

// 複雑なレイアウト式のいくつかの例
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

レイアウト式言語は多くの機能を提供しますが、ビュー内に複雑なロジックをネストしないようにすることが最善です。 複雑な式は、レイアウトの読み取りと保守を難しくします。

レイアウト式を使用してレイアウトファイル内のコンポーネントをバインドすることにより、次のことができます。
- アプリのパフォーマンスを改善する
- メモリリークとNULLポインタ例外を防ぐのに役立ちます
- UIフレームワークの呼び出しを削除して、アクティビティのコードを合理化します

ここではいくつかの例を示します。

// viewmodelのnameプロパティをtext属性にバインドします
android:text="@{viewmodel.name}"
// viewmodelのnameVisibleプロパティをvisibility属性にバインドします
android:visibility="@{viewmodel.nameVisible}"
// ビューがクリックされたときに、viewmodelのonLike()メソッドを呼び出します
android:onClick="@{() -> viewmodel.onLike()}"

ここで言語の完全な説明をご覧ください。
それでは、いくつかのデータをバインドしましょう!

4.最初のレイアウト式を作成します

とりあえず、いくつかの静的データバインディングから始めましょう。
- <data>タグ内に2つの文字列レイアウト変数を作成します。

    <data>
        <variable name="name" type="String"/>
        <variable name="lastName" type="String"/>
    </data>
  • "android:id"がplain_nameのTextViewを探し、"android:text"属性にレイアウト式を追加します
        <TextView
                android:id="@+id/plain_name"
                android:text="@{name}" 
        ... />

レイアウト式は@記号で始まり、中括弧{}で囲まれます。
名前は文字列であるため、データバインディングはTextViewでその値を設定する方法を認識します。 後で、さまざまなレイアウト式のタイプと属性を処理する方法を学習します。

  • "android:id"がplain_lastNameのTextViewでも同じことを行います。
        <TextView
                android:id="@+id/plain_lastname"
                android:text="@{lastName}"
        ... />

これらの操作の結果は、plain_activity_solution_2.xmlにあります。
ここで、アクティビティを変更して、データバインディングレイアウトを正しく拡張する必要があります。

5.インフレーションを変更し、アクティビティからUI呼び出しを削除します

レイアウトの準備はできていますが、アクティビティのいくつかの変更が必要です。 PlainOldActivityを開きます。
データバインディングレイアウトを使用しているため、インフレーションは別の方法で行われます。
onCreateで、以下を置き換えます。

setContentView(R.layout.plain_activity)

val binding : PlainActivityBinding =
    DataBindingUtil.setContentView(this, R.layout.plain_activity)

この変数の目的は何ですか? <data>ブロックで宣言したレイアウト変数を設定するために必要になります。 バインディングクラスは、ライブラリによって自動的に生成されます。
生成されたクラスがどのように見えるかを確認するには、PlainActivitySolutionBindingを開いて、見て回ってください。

  • これで、変数値を設定できます。
    binding.name = "Your name"
    binding.lastName = "Your last name"

以上です。 ライブラリを使用してデータをバインドしました。
古いコードの削除を開始できます

  • updateName()メソッドを削除します。これは、新しいデータバインディングコードがIDを見つけてテキスト値を設定しているためです。
  • onCreate()のupdateName()呼び出しを削除します。

これらの操作の結果は、PlainOldActivitySolution2で確認できます。
これでアプリを実行できます。 Adaの名前があなたの名前に置き換わっていることがわかります。

6.ユーザーイベントの処理

これまで、ユーザーにデータを表示する方法を学習しましたが、データバインディングライブラリを使用して、ユーザーイベントを処理し、レイアウト変数でアクションを呼び出すこともできます。
イベント処理コードを変更する前に、レイアウトを少しクリーンアップできます。

  • 最初に、単一のViewModelの2つの変数を置き換えます。これは、プレゼンテーションのコードと状態を1か所に保持するため、ほとんどの場合に使用する方法です。
plain_activity.xml
    <data>
        <variable
                name="viewmodel"
                type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
    </data>

変数に直接アクセスする代わりに、viewmodelプロパティを呼び出します。

  • 両方のTextViewのレイアウト式を変更します。
plain_activity.xml
        <TextView
                android:id="@+id/plain_name"
                android:text="@{viewmodel.name}"
... />
        <TextView
                android:id="@+id/plain_lastname"
                android:text="@{viewmodel.lastName}"
... />

また、「Like」ボタンのクリックの処理方法を更新します。

  • "android:id"がlike_buttonのボタンを探して置き換えます
plain_activity.xml
android:onClick="onLike"

plain_activity.xml
android:onClick="@{() -> viewmodel.onLike()}"

前のonClick属性は、ビューがクリックされたときにアクティビティまたはフラグメントのonLike()メソッドが呼び出される安全でないメカニズムを使用していました。その正確な署名を持つメソッドが存在しない場合、アプリはクラッシュします。
新しい方法は、コンパイル時にチェックされ、ラムダ式を使用してビューモデルのonLike()メソッドを呼び出すため、はるかに安全です。

Android Studioの[ビルド]メニューで[プロジェクトの作成]をクリックして、データバインディングエラーを確認します。 問題が発生すると、アプリのビルドが妨げられ、ビルドログにエラーが表示されます。

これらの操作の結果は、plain_activity_solution_3.xmlにあります。

次に、不要なものをアクティビティから削除します。

1.リプレイス

    binding.name = "Your name"
    binding.lastName = "Your last name"

    binding.viewmodel = viewModel

2.バイパスされているため、アクティビティのonLike()メソッドを削除します。

これらの操作の結果は、PlainOldActivitySolution3にあります。
アプリを実行すると、ボタンは何もしないことがわかります。 これは、updateLikes()をもう呼び出していないためです。 次のセクションでは、それを適切に実装する方法を学びます。

7.データの監視

前のステップで、静的バインディングを作成しました。
ViewModelを開くと、nameとlastNameが単なる文字列であることがわかりますが、これらは変更されないため問題ありません。 ただし、LIKEはユーザーが変更します。

var likes =  0

この値が変更されたときにUIを明示的に更新する代わりに、監視可能にします。

データバインディングを使用すると、監視可能な値が変更されると、バインドされているUI要素が自動的に更新されます。

可観測性を実装する方法は複数あります。監視可能なクラス監視可能なフィールド、または優先的な方法であるLiveDataを使用できます。 その完全なドキュメントはこちらです。

ObservableFieldsはよりシンプルなので使用します。

リプレイス

    val name = "Grace"
    val lastName = "Hopper"
    var likes = 0
        private set // This is to prevent external modification of the variable.

新しいLiveDatasの場合

    private val _name = MutableLiveData("Ada")
    private val _lastName = MutableLiveData("Lovelace")
    private val _likes =  MutableLiveData(0)

    val name: LiveData<String> = _name
    val lastName: LiveData<String> = _lastName
    val likes: LiveData<Int> = _likes

次もリプレイス

    fun onLike() {
        likes++
    }

    /**
     * Returns popularity in buckets: [Popularity.NORMAL],
     * [Popularity.POPULAR] or [Popularity.STAR]
     */
    val popularity: Popularity
        get() {
            return when {
                likes > 9 -> Popularity.STAR
                likes > 4 -> Popularity.POPULAR
                else -> Popularity.NORMAL
            }
        }

    // popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
    val popularity: LiveData<Popularity> = Transformations.map(_likes) {
        when {
            it > 9 -> Popularity.STAR
            it > 4 -> Popularity.POPULAR
            else -> Popularity.NORMAL
        }
    }

    fun onLike() {
        _likes.value = (_likes.value ?: 0) + 1
    }

ご覧のとおり、LiveDataの値はvalueプロパティで設定されており、変換を使用して1つのLiveDataを別のLiveDataに依存させることができます。 このメカニズムにより、ライブラリは値が変更されたときにUIを更新できます。
LiveDataはライフサイクルを認識するオブザーバブルなので、使用するライフサイクル所有者を指定する必要があります。 これはバインディングオブジェクトで行います。

PlainOldActivityを開き(PlainOldActivitySolution3のように見えるはずです)、バインディングオブジェクトでライフサイクルの所有者を設定します。

binding.lifecycleOwner = this

プロジェクトをリビルドすると、アクティビティがコンパイルされていないことがわかります。 アクティビティから「likes」に直接アクセスしていますが、これはもう必要ありません。

    private fun updateLikes() {
        findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()
        findViewById<ProgressBar>(R.id.progressBar).progress =
            (viewModel.likes * 100 / 5).coerceAtMost(100)
...

PlainOldActivityを開き、アクティビティとその呼び出しのすべてのプライベートメソッドを削除します。 これで、アクティビティは簡単になりました。

class PlainOldActivity : AppCompatActivity() {

    // Obtain ViewModel from ViewModelProviders
    private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding : PlainActivityBinding =
            DataBindingUtil.setContentView(this, R.layout.plain_activity)

        binding.lifecycleOwner = this

        binding.viewmodel = viewModel
    }
}

SolutionActivityでこれらの操作の結果を見つけることができます。
一般に、アクティビティからコードを移動することは、保守性とテスト容易性に優れています。
LIKEの数を示すTextViewを監視可能な整数にバインドしましょう。 plain_activity.xmlで

        <TextView
                android:id="@+id/likes"
                android:text="@{Integer.toString(viewmodel.likes)}"
...

ここでアプリを実行すると、LIKEの数が予想どおりに増加します。
image.png

これまでに行われたことを要約しましょう。
1. "name"と"last name"は、ビューモデルから文字列として公開されます。
2. ボタンのonClick属性は、ラムダ式を介してビューモデルにバインドされます。
3. LIKEの数は、監視可能な整数を介してビューモデルから公開され、テキストビューにバインドされるため、変更時に自動的に更新されます。

これまで、"android:onClick"や"android:text"などの属性を使用してきました。 次のセクションでは、他のプロパティについて学習し、独自のプロパティを作成します。

8.バインディングアダプターを使用してカスタム属性を作成する

文字列(または監視可能な文字列)を"android:text"属性にバインドすると、何が起こるのかは明らかですが、どのように起こっていますか?
Data Binding Libraryを使用すると、ほとんどすべてのUI呼び出しは、バインディングアダプターと呼ばれる静的メソッドで実行されます。
ライブラリには、大量のバインディングアダプタが用意されています。 こちらをご覧ください。 次に、android:text属性の例を示します。

    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        // Some checks removed for clarity

        view.setText(text);
    }

またはandroid:background one

    @BindingAdapter("android:background")
    public static void setBackground(View view, Drawable drawable) {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            view.setBackground(drawable);
        } else {
            view.setBackgroundDrawable(drawable);
        }
    }

データバインディングには魔法はありません。 コンパイル時にすべてが解決され、生成されたコードを読むことができます。
進行状況バーで作業しましょう。 私たちはそれをしたい:

  • LIKEがなければ見えない
  • 5つのLIKEでいっぱいになる
  • いっぱいになったら色を変更

image.png

このためのカスタムバインディングアダプターを作成します。
utilsパッケージのBindingAdapters.ktファイルを開きます。それらをどこで作成しても、ライブラリはそれらを見つけます。Kotlinでは、Kotlinファイルの最上位に関数を追加するか、クラスの拡張関数として静的メソッドを作成できます。

最初の要件、hideIfZeroのバインディングアダプターを探します。

    @BindingAdapter("app:hideIfZero")
    fun hideIfZero(view: View, number: Int) {
        view.visibility = if (number == 0) View.GONE else View.VISIBLE
    }

このバインディングアダプタ
- "app:hideIfZero"属性に適用されます。
- すべてのビューに適用できます(最初のパラメーターはビューであるため、このタイプを変更することで特定のクラスに制限できます)
- レイアウト式が返すものでなければならない整数を取ります。
- 数値がゼロの場合、View GONEを作成します。 それ以外の場合は可視。

plain_activityレイアウトで、進行状況バーを探し、hideIfZero属性を追加します。

    <ProgressBar
            android:id="@+id/progressBar"
            app:hideIfZero="@{viewmodel.likes}"
...

アプリを実行すると、ボタンを初めてクリックしたときにプログレスバーが表示されます。 ただし、値と色を変更する必要があります。
image.png

これらの手順の結果は、plain_activity_solution_4.xmlにあります。

9.複数のパラメーターを持つバインディングアダプターを作成する

進捗値については、最大値とLIKEの数をとるバインディングアダプターを使用します。BindingAdaptersファイルを開き、これを探します。

/**
 *  Sets the value of the progress bar so that 5 likes will fill it up.
 *
 *  Showcases Binding Adapters with multiple attributes. Note that this adapter is called
 *  whenever any of the attribute changes.
 */
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
    progressBar.progress = (likes * max / 5).coerceAtMost(max)
}

属性のいずれかが欠落している場合、このバインディングアダプタは使用されません。 これはコンパイル時に発生します。 このメソッドは、3つのパラメーター(適用されるビューに加えて、アノテーションで定義された属性の数)を取ります。
requireAllパラメーターは、バインディングアダプターを使用するタイミングを定義します

  • trueの場合、すべての要素がXML定義に存在する必要があります。
  • falseの場合、欠落している属性はnull、ブール値の場合はfalse、プリミティブの場合は0になります。

次に、属性をXMLに追加します。

        <ProgressBar
                android:id="@+id/progressBar"
                app:hideIfZero="@{viewmodel.likes}"
                app:progressScaled="@{viewmodel.likes}"
                android:max="@{100}"
...

progressScaled属性をLIKEの数にバインドし、リテラル整数をmax属性に渡しているだけです。 @{}形式を追加しないと、データバインディングは正しいバインディングアダプターを見つけることができません。

これらの手順の結果は、plain_activity_solution_5.xmlにあります。
アプリを実行すると、プログレスバーが期待どおりにいっぱいになる様子がわかります。

10.バインディングアダプタの作成の練習

習うより慣れよう。作成

  • LIKEの値に応じてプログレスバーの色を調整し、対応する属性を追加するバインディングアダプター
  • 人気度に応じて異なるアイコンを表示するバインディングアダプタ
  • 黒のic_person_black_96dp
  • 薄いピンクのic_whatshot_black_96dp
  • 濃いピンクのic_whatshot_black_96dp

ソリューションは、BindingAdapters.ktファイル、SolutionActivityファイル、およびsolution.xmlレイアウトにあります。

image.png

11.きっと成功します!

おめでとうございます!コードラボを完了したので、データバインディングレイアウトの作成方法、それに変数と式を追加する方法、監視可能なデータを使用し、カスタムバインディングアダプターを介してカスタム属性でXMLレイアウトをより意味のあるものにします。
次に、より高度な使用法のサンプルと全体像のドキュメントを確認してください。

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

[Android] 加速度センサのサンプリング周波数が知りたいとき

端末依存で最大・最低・間隔が決まるので,確認用のコードを書いた。
メモです。

Logcatに周期が[ns]で出力される

package com.example.getrotationtest

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.opengl.Matrix
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.math.BigDecimal
import java.util.*
import kotlin.properties.Delegates
import kotlin.io.*
import kotlin.math.PI

class MainActivity : AppCompatActivity(), SensorEventListener {
    private var mManager: SensorManager by Delegates.notNull<SensorManager>()
    private var mSensor: Sensor by Delegates.notNull<Sensor>()
    private var maSensor: Sensor by Delegates.notNull<Sensor>()
    private var mgSensor: Sensor by Delegates.notNull<Sensor>()

    private var prevtime = 0L

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

        mManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

        mSensor = mManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
        maSensor = mManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        mgSensor = mManager.getDefaultSensor(Sensor.TYPE_GRAVITY)

    }

    override fun onSensorChanged(event: SensorEvent) {
        when (event.sensor.type) {
            Sensor.TYPE_ACCELEROMETER -> {
                Log.d("tag", (event.timestamp - prevtime).toString())
                prevtime = event.timestamp
            }
        }
    }


    //センサー精度が変更されたときに発生するイベント
    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }

    //アクティビティが閉じられたときにリスナーを解除する
    override fun onPause() {
        super.onPause()
        //リスナーを解除しないとバックグラウンドにいるとき常にコールバックされ続ける
        mManager.unregisterListener(this)
    }

    override fun onResume() {
        super.onResume()
        //リスナーとセンサーオブジェクトを渡す
        //第一引数はインターフェースを継承したクラス、今回はthis
        //第二引数は取得したセンサーオブジェクト
        //第三引数は更新頻度 SensorManagerから選択するか,usで指定する
        mManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_FASTEST)
        mManager.registerListener(this, maSensor, 238000)   // 加速度のセンサ
        mManager.registerListener(this, mgSensor, SensorManager.SENSOR_DELAY_FASTEST)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ Android ] 戻るボタン無効化

Androidで端末の戻るボタンを無効にする方法について述べる。

方法

とても簡単でActivityクラスのonBackPressed()をオーバライドし、処理を書かなければ完了である。

    @Override
    public void onBackPressed() {
    }

参考サイト

以下のサイトがとても参考になった。
https://minpro.net/onbackpressed-android

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

【Android/Kotlin】円を拡大するアニメーションの実装方法

基本画面

 2019-12-03 12.20.32.png

  • FrameLayoutをmatch/matchで設定しています。
  • 円のカスタムビューをvisible=GONEで設定しています。
  • 星のImageViewを2種類おいています。
  • 星をクリックすると、円が拡大描画されていきます。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/back_color"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center">

        <com.kiyota.scaleanimationapp.CircleCustomView
            android:id="@+id/main_circle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone"></com.kiyota.scaleanimationapp.CircleCustomView>

        <ImageView
            android:id="@+id/star_pink"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_gravity="center"
            android:src="@drawable/ic_star_pink"></ImageView>

        <ImageView
            android:id="@+id/star_blue"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_gravity="center"
            android:layout_marginTop="100dp"
            android:src="@drawable/ic_star"></ImageView>
    </FrameLayout>

</androidx.appcompat.widget.LinearLayoutCompat>
CircleCustomView.kt
class CircleCustomView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    /**
     * サークルの色
     */
    private val paint = Paint()
    /**
     * サークルの座標位置
     */
    private val rect = RectF()
    /**
     * サークルの円周の長さ
     */
    private var angle = 360F
    /**
     * 中心座標(x)
     */
    private var x: Int = 0
    /**
     * 中心座標(y)
     */
    private var y: Int = 0
    /**
     * 初回フラグ
     */
    var isInit = true

    /**
     * コンパニオンオブジェクト
     */
    companion object {
        /**
         * サークルの開始位置
         */
        private const val angleTarget = 0F
    }

    /**
     * コンストラクタ
     */
    init {
        // サークルの幅
        this.paint.isAntiAlias = true
        this.paint.style = Paint.Style.FILL_AND_STROKE
        // サークルの色
        this.paint.color = (context.resources.getColor(R.color.star_color))
    }

    /**
     * ビュー描画メソッド
     *
     * @param canvas 描画領域
     */
    @SuppressLint("CanvasSize")
    public override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 初回は中心スタート
        if (isInit) {
            // サークル開始地点(中央揃え)
            this.x = canvas.width / 2
            this.y = canvas.height / 2
            isInit = false
        }
        // サークルの背景色(透明)
        canvas.drawColor(this.context.resources.getColor(R.color.color_back_circle))
        // サークルの半径()大きさ
        var radius = 40
        val left = this.x - radius
        val top = this.y - radius
        val right = this.x + radius
        val bottom = this.y + radius
        // サークルの領域設定
        this.rect.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
        // サークルの描画
        canvas.drawArc(this.rect, angleTarget, this.angle, false, this.paint)
    }

    /**
     * 中心座標変更メソッド
     */
    fun changeCenter(x: Int, y: Int) {
        // サークルの中心座標
        this.x = x
        this.y = y
    }

    /**
     * サークル色変更メソッド
     *
     * @param index 押されたボタンの番号
     */
    fun changeColor(index: Int) {
        when (index) {
            0 -> {
                this.paint.color = context.resources.getColor(R.color.colorAccent)
            }
            1 -> {
                this.paint.color = context.resources.getColor(R.color.star_color)
            }
            else -> {
                this.paint.color = context.resources.getColor(R.color.colorPrimary)
            }
        }
    }
}

画面の中心で円を拡大する場合(ピンク)

  • 中心座標(540 , 792)
  • pivot(0.5f , 0.5f)  2019-12-03 12.00.20.png
MainActivity.kt
package com.kiyota.scaleanimationapp

class MainActivity : AppCompatActivity(), Animation.AnimationListener {

    private var index = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // クリックされたらアニメーションをスタートする
        star_pink.setOnClickListener {
            index = 0
            // 端末(Nexus5x)の中心座標が(540,792)という意味です
            startScalingXml(540, 792, index)
        }
        star_blue.setOnClickListener {
            index = 1
            startScalingXml(540, 792, index)
        }
    }

    /**
     * サークル拡大アニメーションメソッド
     *
     * @param x x座標
     * @param y y座標
     * @param index クリックされたビューの順番
     */
    private fun startScalingXml(x: Int, y: Int, index: Int) {
        main_circle.changeCenter(x, y)
        main_circle.changeColor(index)
        var scaleAnimation = ScaleAnimation(
            1.0f, 30.0f, 1.0f, 30.0f,
            Animation.RELATIVE_TO_SELF,
            0.5f, Animation.RELATIVE_TO_SELF, 0.5f
        )
        scaleAnimation.duration = 20000
        scaleAnimation.repeatCount = 0
        scaleAnimation.fillAfter = true
        main_circle.visibility = VISIBLE
        scaleAnimation.setAnimationListener(this)
        main_circle.startAnimation(scaleAnimation)
    }

    override fun onAnimationRepeat(p0: Animation?) {
    }

    /**
     * アニメーション終了時処理メソッド
     *
     * @param p0 アニメーション
     */
    override fun onAnimationEnd(p0: Animation?) {
        setColorByIndex(index)
        main_circle.visibility = GONE
    }

    override fun onAnimationStart(p0: Animation?) {
    }

    /**
     * 背景色切り替えメソッド
     *
     * @param index クリックされたビューの順番
     */
    private fun setColorByIndex(index: Int) {
        when (index) {
            0 -> {
                back_color.setBackgroundColor(this.resources.getColor(R.color.colorAccent))
            }
            1 -> {
                back_color.setBackgroundColor(this.resources.getColor(R.color.star_color))
            }
            else -> {
                back_color.setBackgroundColor(this.resources.getColor(R.color.colorPrimary))
            }
        }
    }
}

画面の左側で円を拡大する場合(水色) :失敗

  • 中心座標(270 , 792)
  • pivot(0.5f , 0.5f)

 2019-12-03 12.04.11.png

画面の左側で円を拡大する場合(水色) :成功

  • 中心座標(270 , 792)
  • pivot(0.25f , 0.5f) ※x軸の値を半分にしてあげればOK  2019-12-03 12.08.45.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material Components時代のAndroid Themeを改めて理解する

AndroidのMaterial Componentsを利用する場合、

theme.xml
<style name="Theme.App" parent="Theme.MaterialComponents.Light">
    <!-- ... -->
</style>

上記のように、Theme.MaterialComponents.Lightなどを継承させてthemeを定義しているかと思います。
Dark Themeに対応する場合はTheme.MaterialComponents.DayNightなどを継承させているでしょう。

世はまさにDark Theme時代と言っても過言ではなくなってきています。
今一度、Androidにおけるtheme/styleとは何かを簡単に整理していこうという記事になります。

Material ComponentsのTheming

デフォルトのThemeはどうなっているのか

Android Studioからプロジェクトを作ると、colorPrimaryなどのThemeが既に定義されています。
いきなりカスタムするのではなく、まずは素っ裸のTheme.MaterialComponents.DayNightを見てみましょう。

theme.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar"/>
    <style name="ThemeStudy" parent="Base.ThemeStudy"/>
</resources>

このように定義し、わかりやすいようにいくつかコンポーネントを置いてみるとこのようになります。
(レイアウトファイルの方でもtheme/styleやattributesの定義は全てなくしています。)
default_theme.png

なんとなくcolorPrimaryが紫でcolorSecondaryが青緑かな〜というのがわかりますね。
はてさてこの色はどこに定義されているのでしょう?

⌘を押しながらクリック(あるいはショートカット)で定義元を探ってみます :eyes:

values.xml
<!-- 継承しているTheme.MaterialComponents.DayNight.NoActionBarからスタート -->
<style name="Theme.MaterialComponents.DayNight.NoActionBar" parent="Theme.MaterialComponents.Light.NoActionBar"/>
 ↓
<style name="Theme.MaterialComponents.Light.NoActionBar">
 ↓
<style name="Theme.MaterialComponents.Light" parent="Base.Theme.MaterialComponents.Light"/>
 ↓
<style name="Base.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light"/>
 ↓
<style name="Base.V14.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light.Bridge">
    <!-- ... -->

    <!-- Colors -->
    <item name="colorPrimary">@color/design_default_color_primary</item> <!-- !!!! -->
    <item name="colorPrimaryDark">@color/design_default_color_primary_dark</item>
    <item name="colorAccent">?attr/colorSecondary</item>

    <!-- ... -->
</style>

なかなか深いところにいましたね。colorPrimaryはどうやら#6200EEのようです。

これらのデフォルトカラーはmaterial.ioのColor Themingにも記載されています。
ここを見るとわかるのですが、Material ComponentではAppCompatとは違ったテーマ属性が使われます。

というわけで、次は色について簡単に説明していきます。

Material Componentsのカラー属性(テーマ属性)

マテリアルデザインではベースラインとして使える色の属性が定義されていて
基本的にはこれを用いて自分のアプリに合うようカスタマイズしていくことになります。
便宜上、テーマ属性における色に関する属性をカラー属性と呼びます。
edgetoedge.gif

ref: https://material.io/design/color/the-color-system.html#color-theme-creation

各色についてはUsing The Color Theming Systemに記載されているので割愛します。

Material Components for Androidでは、Widget.MaterialComponents.~のスタイルをデフォルトで使うよう定義されています。
またこのスタイルではテーマ属性を参照して構成されています。
そのため、このベーステーマを継承させ、オーバーライドすることで簡単に各コンポーネントの色を変えることができるのです。

カラー属性を変えてみる

わかりやすいように各カラー属性を雑に変えてみました。
change_colors.png

なんとなくどのカラー属性が、どのコンポーネントのスタイルから参照されているかが掴めるかと思います。
しかし、TextInputLayoutの背景色のように、これはどのカラー属性だ?というものがいくつか出てきますね。

前述したように、各コンポーネントはテーマ属性を参照してスタイルが定義されています。次はその定義を見てみましょう。

Material ComponentsのWidget Style

Theme.MaterialComponents.DayNightなどのテーマでは、テーマ属性で各コンポーネントのスタイルを指定しています。

例として、TextInputLayoutのスタイルを見てみましょう。themeの継承をたどっていくと、textInputStyleというテーマ属性が定義されています。

values.xml
<style name="Base.V14.Theme.MaterialComponents.Light.Bridge" parent="Platform.MaterialComponents.Light">
    <!-- ... -->
    <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox</item>
    <!-- ... -->
</style>

その中身を見ると、どうやら背景色は@color/mtrl_filled_background_colorに定義されているということがわかります。

values.xml
<style name="Widget.MaterialComponents.TextInputLayout.FilledBox" parent="Base.Widget.MaterialComponents.TextInputLayout">
    <!-- ... -->
    <item name="boxBackgroundColor">@color/mtrl_filled_background_color</item>
    <!-- ... -->
    <item name="boxStrokeColor">@color/mtrl_filled_stroke_color</item>  
    <!-- ... -->
</style>

mtrl_filled_background_colorでは、Viewの状態に応じて透明度は変わるものの、colorOnSurfaceを使っていることがわかりました!
先程の図で、うすく赤い色になっていたのはcolorOnSurfaceにアルファがかかっていたからなのです。
(黒みがかっているのは内部的にsurfaceレイヤーがあり、colorSurfaceの色が見えているからです。)

mtrl_filled_background_color.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_hovered="true"/>
  <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_focused="true"/>
  <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
  <item android:alpha="0.12" android:color="?attr/colorOnSurface"/>
</selector>

同様に、boxStrokeColor@color/mtrl_filled_stroke_colorに定義がされており、TextInputLayoutにフォーカスがあたったときに色が変わる謎も解けます:thumbsup:

mtrl_filled_stroke_color.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:color="?attr/colorPrimary" android:state_focused="true"/>
  <!-- 4% overlay over 42% colorOnSurface -->
  <item android:alpha="0.46" android:color="?attr/colorOnSurface" android:state_hovered="true"/>
  <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
  <item android:alpha="0.42" android:color="?attr/colorOnSurface"/>
</selector>

このように、テーマ属性を適切に定義することで、マテリアルコンポーネントではアプリ全体に統一感のあるデザインを提供しています。

テーマ属性の参照

「テーマ属性を参照しているので」 と説明もなく言っていましたが、これはどうやるのでしょう?
私自身、今回ちゃんとThemeと向き合うまで、なにげなく使っていたけど知りませんでした。

すでに説明したコードにも出てきていますが、?attr/colorPrimaryと書くことで、現在のテーマ属性を参照できます。
この場合は、現在のテーマにおけるcolorPrimaryを参照しています。colorタグで定義したものではないことに注意です。
reference.png

現在のというのが肝です。

テーマ属性の参照を使ってコンポーネントの色を変える

例えば、ユーザのステータスに応じてcolorPrimaryを変えたいと言った場合を考えてみましょう。

単純に思いつくのは、プログラムで動的に色をコンポーネントに指定してあるげる方法です。
毎回Activity/Fragmentで処理を書くのはつらいのでBindingAdapterを作ってよしなに色を変えたりもできそう。

こういった場合、コンポーネントごとに色を変える処理を書くのは難儀なものです。

この場合、themeをまるまる変えてしまうとよいでしょう。
colorPrimary/colorSecondaryなど、必要な分だけ定義を上書きましょう。

theme.xml
<resources>

    <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorSecondary">@color/colorSecondary</item>
    </style>

    <style name="ThemeStudy" parent="Base.ThemeStudy" />

    <style name="ThemeStudy.Premium">
        <item name="colorPrimary">#f44336</item>
        <item name="colorSecondary">#ffc107</item>
    </style>

    <style name="ThemeStudy.Etc">
        <item name="colorPrimary">#36f443</item>
        <item name="colorSecondary">#07ffc1</item>
    </style>
</resources>

あとはthemeを条件に応じて指定してあげるだけです。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 条件に応じてテーマを設定
        setTheme(if (isPremium()) R.style.ThemeStudy_Premium else R.style.ThemeStudy)
        ...
    }
    ...
}

画面単位や、部分的な適用の場合はレイアウトで指定してあげると良いでしょう。
これに関しては公式のMaterial DesignサンプルのOwlが上手に使い分けているので参考になります。

ThemeとStyleの違い

Styleは1つのViewのみに適用され、Themeは適用したViewの子Viewにまで適用されるという違いがあります。
また、Themeはコンテキストに紐づきます。ActivityでThemeを設定していれば画面全体に適用されます。
設定していなければApplicationコンテキストで設定されたThemeが適用されます。
theme_style.png

ref: https://speakerdeck.com/nickbutcher/developing-themes-with-style-7d0571e3-6008-4ec5-99c6-aa5669520124?slide=29

ThemeOverlayとは

ThemeOverlay自体はただの命名規則です。Style/Themeの継承を利用し、この命名規則も納得の動きをします。

<style>タグではparentを指定しThemeを継承、そして<item>タグでテーマ属性などを定義し自分のThemeでオーバーライドすることが多いかと思います。
例えば、colorPrimaryを定義すると、継承元のcolorPrimaryを上書きしますね。

theme.xml
<style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorSecondary">@color/colorSecondary</item>
</style>

このとき、parentを指定せずにcolorPrimaryなどのテーマ属性を定義すると、その属性のみが上書きされ、定義されていないテーマ属性はそのViewにすでに適用されているThemeのものが使われるのです。(親のViewのThemeやApplication/Activity/Fragmentなどで適用されているTheme)
theme_overlay.png

この例では、便宜上Customというスタイルを定義していますが、命名規則を守り、ThemeOverlay.AppName.Redのように定義すると管理がしやすいでしょう。

TextInputLayoutはViewの属性としてboxBackgroundColorboxStrokeColorを持っているので、それを用いて色を変えられるには変えられます。
しかし、Material Componentsではテーマ属性を用いて丁寧にスタイルが定義されています。都度都度、ViewごとにViewの属性を設定していると管理が煩雑になっていまします。
ThemeOverlayの考え方を使って、ViewあるいはViewGroupに、colorPrimaryなどのテーマ属性を適切に設定し統一感を守っていきましょう。

ThemeOverlayの使い所

なぜThemeOverlayを使うかというと、必要なテーマ属性だけ上書きできるという利点が大きいのではないかと思います。

公式サンプルのOwlを見てみると非常にわかりやすいです。
Base.OwlではtextAppearanceHeadline1bottomNavigationStyleといったアプリ全体として共通のテーマ属性を定義しています。
そしてcolorPrimaryなどの色に関するテーマ属性は、Owl.Yellow/Owl.Blueそれぞれで定義されています。

theme.xml
<!-- ... -->
<style name="Owl" parent="@style/Base.Owl"/>

<style name="Owl.Yellow">
    <item name="colorPrimary">@color/owl_yellow_500</item>
    <item name="colorPrimaryVariant">@color/owl_yellow_400</item>
    <item name="colorSecondary">@color/owl_blue_700</item>
    <item name="colorSecondaryVariant">@color/owl_blue_800</item>
</style>

<style name="Owl.Blue">
    <item name="colorPrimary">@color/owl_blue_700</item>
    <item name="colorPrimaryVariant">@color/owl_blue_800</item>
    <item name="colorSecondary">@color/owl_yellow_500</item>
    <item name="colorSecondaryVariant">@color/owl_yellow_400</item>
</style>
<!-- ... -->

こうすることで変更にも強くなり見通しも良いですね。

(OwlではThemeOverlay.MaterialComponents.Lightを継承したThemeOverlay.Owl.Blueがありますが、Theme.MaterialComponents.Lightとの差分までは面倒なので追えてないです)

よく見るThemeOverlayでいうと、プロジェクトを作るとAppBarLayoutにデフォルトで定義されているThemeOverlay.Material.Dark.ActionBar(あるいはThemeOverlay.Material.Light.ActionBar)があります。
おなじみChris BanesさんのTheme vs Styleによると、colorControlNormalandroid:textColorPrimaryにしているだけ、とあります。(実際にコードを追うとcolorControlNormal?android:attr/textColorPrimaryと定義されています。)
これによって、AppBarLayoutのタイトルテキストと、メニューなどのアイコンの色を揃えているというわけです。(メニューなどの色はcolorControlNormalによって決まる)

まとめ

  • Material Componentsはテーマ属性によってThemingされている
  • ?atrr/を使うことでテーマ属性を参照できる
    • これをうまく使っていくの大事そう
  • Styleは1つのViewに適用され、Themeは子Viewにも適用される
  • テーマ属性を適切に設定することで統一感があり管理も煩雑にならない
  • ThemeとStyleを見ればView属性のデフォルトがどのテーマ属性を参照しているかわかる

それこそTheme vs Styleや、Dark Themeについても取り上げたかったんですが長くなるのでいつかの機会に。
カラー命名の良い方法などはAndroid Dev Summit動画または こちらのQiita記事を見ると良いです!
(本記事の中では<color name="colorPrimary">#6200EE</color>のように定義してますが、あまりよくないです。)

間違ったところや疑問点などがアレば歓迎しますぜひ教えて下さい!
参考にしたリンクなども書いておくのでThemeをみんなで攻略しましょう!

Links

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

AndroidStudioのProject Viewで、パッケージをまとめずに全階層表示する

環境

Android Studio 3.5

連結されたパッケージ表示

プロジェクト内にファイルやパッケージを追加する時、AndroidStudioのProject Viewから直接追加している人も多いかと思います。
この時、設定によっては以下のようにパッケージが連結されて表示されています。

スクリーンショット 2019-12-03 9.51.22.png

model直下にrequestしかないため、個別に表示する必要がないと判断されていますね。

パッケージを全階層表示

これだと、modelの下にresponseというpackageを追加したい時に少し面倒です。
そんな時はこのようにして全ての階層を表示させましょう。

Projectから
スクリーンショット 2019-12-03 9.51.22.png

メニューを開いて、Compact Middle Packages のチェックを外す
スクリーンショット 2019-12-03 10.02.54.png

階層ごとに表示されるようになりました
スクリーンショット 2019-12-03 10.04.54.png

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

Firebaseのサーバーレスアプリ開発について(Android編)

Firebaseサーバーレスアプリのニーズ

2~3年前からサーバーレスのアプリ開発(Android/iOS)が増えています。
特にFirebaseを使っているスタートアップをよく見かけます。

アプリの競争が激しい中、今はいいアプリを作るだけで売れる時代は終わっているかも知りません。
特に資金力の弱いスタトアップだと限られた資金で短期間に成果をあげることは難しいでしょう
資金調達の為にもより早くアプリを見せないといけないです。
そういったニーズの答えがFirebaseのサービスだと思います。

Firebaseサーバーレスのメリット&デメリット

メリット&デメリットはあくまで私の個人意見です。
以下の内容以外にもいっぱいあると思います。

メリット

  • サーバー開発期間がゼロ
  • サーバー管理が不要で開発に集中できる
  • 無料枠でも結構使える
  • Firestoreだとほぼリアルタイムで更新するのでチャット機能も簡単に実現できる

デメリット

その他

Firebaseサーバーレスの実装サンプル

本投稿ではサーバーレスの実装がどれだけ簡単にできるかをコードを中心に伝えたいと思います。
導入方法については次回に紹介します。

1.Firebase Authentication

会員登録とログイン関係の全ての機能を提供しています。
サーポートする認証方式は以下の画像を参考にしてください。
FirebaseでサーポートしてないLINEなどの認証は別の方法で実装ができます。
それについては次回に紹介します。

サーポートする認証方式は色々ありますが、
本投稿では「メール/パスワード」の認証方式だけをkotlinで書いて見ました。
- 会員登録、プロフィール変更、パスワード変更、ログイン、ログアウト、退会

a.会員登録
会員登録
    fun signup() {
        val auth = FirebaseAuth.getInstance()
        val email = "test@bizreach.co.jp"
        val password = "12345678"
        auth.createUserWithEmailAndPassword(email, password)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
会員登録後のAuthentication管理画面

b.プロフィール変更
プロフィール変更
    fun changeProfile() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return

        val name = "山田"
        val profileUrl = "https://example.com/jane-q-user/profile.jpg"
        val profile = UserProfileChangeRequest.Builder()
            .setDisplayName(name)
            .setPhotoUri(Uri.parse(profileUrl))
            .build()
        user.updateProfile(profile)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
c.パスワード変更
パスワード変更
    fun changePassword() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return
        val newPassword = "abcdefgh"
        user.updatePassword(newPassword)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
d.ログイン
ログイン
    fun login() {
        val auth = FirebaseAuth.getInstance()
        val email = "test@bizreach.co.jp"
        val password = "abcdefgh"
        auth.signInWithEmailAndPassword(email, password)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
e.ログアウト
ログアウト
    fun logout() {
        FirebaseAuth.getInstance().signOut()
    }
f.退会
退会
    fun withdrawal() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return
        user.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
退会後のAuthentication管理画面

2.Cloud Firestore

NoSQLデータベースで、データの保存・変更・削除などの機能が簡単に使えます。
サンプルコードではToDo一覧、ToDo詳細機能をベースにデータを構成して実装して見ました。

a.ToDo登録
ToDo登録
    data class Todo(val title: String, val done: Boolean = false, val createdAt: Timestamp = Timestamp(Date()))

    fun addTodo() {
        val db = FirebaseFirestore.getInstance()
        val col = db.collection("todo_list")

        col.add(Todo("todo1"))
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }

        col.add(Todo("todo2"))
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
ToDo登録後のFirestore管理画面

b.ToDo編集
ToDo編集
    fun changeToDone() {
        val db = FirebaseFirestore.getInstance()
        val doc = db.collection("todo_list")
            .document("Hi2mlwN3lguMmfu76ZqX")

        doc.update("done", true)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
ToDo編集後のFirestore管理画面

c.ToDoの一覧取得
ToDoの一覧取得
    fun getTodoList() {
        val db = FirebaseFirestore.getInstance()
        db.collection("todo_list")
            .get()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result ->
                /*成功*/
                result?.forEach {
                    val title = it["title"] as String
                    val done = it["done"] as Boolean
                    val createdAt = it["createdAt"] as Timestamp
                    val todo = Todo(title, done, createdAt)
                    Log.d("TAG", "todo_list: documentId=${it.id} todo=$todo")
                }
            }
    }
---- logcat
//todo_list: documentId=1aPGHKce0Fn4Osu65ioW todo=Todo(title=todo2, done=false, createdAt=Timestamp(seconds=1573460794, nanoseconds=820000000))
//todo_list: documentId=Hi2mlwN3lguMmfu76ZqX todo=Todo(title=todo1, done=true, createdAt=Timestamp(seconds=1573460794, nanoseconds=801000000))

d.ToDo削除
ToDo削除
    fun removeTodo() {
        val db = FirebaseFirestore.getInstance()
        val doc = db.collection("todo_list")
            .document("Hi2mlwN3lguMmfu76ZqX")

        doc.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
ToDo削除後のFirestore管理画面

3.Cloud Storage

ファイルの保存、変更、削除、URL参照ができます。
例えば会員のプロフィール写真などを保存して画面に表示する際に使います。
※内部的にはAWSのS3を使っています。

a.プロフィール画像のアップロード
プロフィール画像のアップロード
    fun uploadProfile() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")

        val bitmap = BitmapFactory.decodeResource(resources, R.drawable.profile)
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        val data = baos.toByteArray()

        profileRef.putBytes(data)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
プロフィール画像のアップロード後のStorage管理画面

b.プロフィール画像URLの取得
プロフィール画像URLの取得
    fun getProfileUrl() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")
        profileRef.downloadUrl
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result ->
                /*成功*/
                Log.d("TAG", "profileUrl = $result")
            }
    }
----logcat
profileUrl = https://firebasestorage.googleapis.com/v0/b/fir-serverlessdemo.appspot.com/o/images%2Fprofile.jpg?alt=media&token=8bbb1678-76a7-470f-8773-9f3d8d8363f4
d.プロフィール画像の削除
プロフィール画像の削除
    fun deleteProfile() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")
        profileRef.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
プロフィール画像の削除後のStorage管理画面

まとめ

Firebaseを使えば認証管理、DB管理、ファイル管理がどれだけ簡単にできるか理解頂けたと思います。

伝統的なネイティブアプリの開発方法がだんだん変わっています。
昔の開発では3人(インフラエンジニア/サーバーサイドエンジニア/フロントエンジニア)でやってたのが
今はだった一人でできる時代になりました。
Firebaseを使えば質の高いアプリ(Android/iOS/Web)をたった一人で開発することができます。

以前は物理サーバーを立ててから開発することが普通でしたが、
現在ではクラウドサービス(AWS、AZUL、GCP...)を使うのが当たり前のようになっています。
それと同じ様にネイティブアプリの開発でもサーバーレスのアプリ開発が広がり、
当たり前のような時代になるかも知りません。

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

GitリポジトリのライブラリをAndroidアプリから参照する

Gitのリポジトリのライブラリを参照する方法

色々調べてトライしたけれど、ブランチ指定した場合にうまく動作するのがこの方法だったのでメモ
他の方法はmaster指定しかうまく動かなかった・・・

詳細は以下のソースコードをみてください。

[ライブラリのリポジトリ]
https://github.com/tokuyama-san/MyLibrary

build.gradle(Module)
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: 'maven.gradle'  ←ここを追加

android {
    compileSdkVersion 29


    defaultConfig {
        minSdkVersion 28
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

maven.gradle
def versionName = "0.9.1beta"
def repo = new File(rootDir, "repository")

apply plugin: 'maven'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

uploadArchives {
    repositories {
        mavenDeployer {
            repository url: "file://${repo.absolutePath}"
            pom.version = "${versionName}"      // version
            pom.groupId = 'toku.san.mylibrary'      // グループ名
            pom.artifactId = 'MyLibrarySDK' // ライブラリ名
        }
    }
}

[アプリのリポジトリ]
https://github.com/tokuyama-san/MyApplication

build.gradle(Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        google()
        jcenter()
        maven { url "https://github.com/layerhq/releases-gradle/raw/master/releases" } ←これを追加
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath group: 'com.layer', name: 'git-repo-plugin', version: '1.0.0' ←これを追加
    }
}

allprojects {
    repositories {
        google()
        jcenter()

    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
build.gradle(app)
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    defaultConfig {
        applicationId "toku.san.myapplication"
        minSdkVersion 28
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

apply plugin: 'git-repo' ←これを追加

repositories {
    git("git@github.com:tokuyama-san/MyLibrary.git", "toku.san.mylibrary:MyLibrarySDK", "master", "repository") ←これを追加
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.0' ←これを追加
}

ライブラリの更新方法

ライブラリのリポジトリですること

1.maven.gradleに記載しているバージョン(VersionName)を変更する(例. "0.9.1beta")

2.ライブラリのルートディレクトリで以下のコマンド

./gradlew uploadArchives

3.repository配下に指定したバージョンのファイルがいくつか出来上がるのでGitにコミット&プッシュする

アプリのリポジトリですること

アプリのbuild.gradleに記載されているdependenciesスコープ内の
ライブラリのバージョン部分を変更する。

implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.0'

implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.1beta'

あとはビルドすればOK

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

【Android】license-tools-pluginで出力したlicenses.htmlで、URLが長いと改行位置がずれてしまう

はじめに

アプリでライセンス表示を埋め込む際、色々な方法があるかと思います。
今回クックパッドさんが開発されたlicense-tools-pluginを使わせていただくこととなり、
実際に表示まで行なった際に表題の問題が発生してしまいました。
その原因と対応方法を自分用のメモとして残しておきたいと思います。

license-tools-pluginの詳細な使い方については、
他の方がわかりやすくまとめられている為、本記事では割愛させていただきます。
あの素晴らしいlicense-tools-pluginをもう一度

どういう現象か

以下のように、横幅(width)より、リンクの文字列が長くなってしまった場合、
少しはみ出た状態での改行になってしまいます。
ずれ.png

原因と対応方法

出力したlicenses.htmlを確認してみると、はみ出ていた部分は以下のようになっていました。

<p><a href="https://developer.android.com/topic/libraries/architecture/index.html">https://developer.android.com/topic/libraries/architecture/index.html</a></p>

HTMLをあまり詳しく知らなかったため、調査したところ、CSSの設定が足りていないことが原因であることが判明。
URLが自動改行してくれない問題

<style></style>の中に、<a>タグ用の設定として以下を追記することで、ずれなくすることができました。

<style>
  a {
  word-break: break-all;
  }
</style>

補足

尚、出力するたびに上記の設定を追記する必要があるため、面倒であるのと対応するの忘れそう…
流石に他にlicense-tools-pluginを使っている方も同じ現象に陥っているのでは…?と思い、
公式のGitHabリポジトリを確認すると、なんと以下のプルリクエストを発見。
https://github.com/cookpad/license-tools-plugin/pull/124

出力時に以下の設定を追加するようにされており、
手動で試してみたところ、こちらでもずれが発生しないことを確認できました。

  a {
  overflow-wrap: break-word;
  }

ですので、クックパッドさんがlicense-tools-pluginを次回アップデートされる際には修正されたものが上がってくるかと思います。

修正版が上がるまでは面倒ですが、以下のどちらかを手動で追記して対応するようにしましょう。
word-break: break-all;
overflow-wrap: break-word;

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

MotionLayoutのサンプルコードから学ぶMotion Editor

この記事はコネヒト Advent Calendar 2019 3日目の記事になります。

はじめに

Android Studio 4.0 Canary 4に新機能として導入されたMotion Editorについて調べてみたので、公開されているMotionLayoutのサンプルコードをベースに、どのように利用されているのか紹介します。

MotionLayoutとは?

  • ConstraintLayout 2.0に含まれている機能
  • 始まりと終わりのレイアウトをXMLで定義することによって簡単にアニメーションを実装することができる

Motion Editorとは?

  • Android Studio 4.0 CanaryからMotion Editor導入された
  • IDE内でViewの関係性やアニメーションを確認、設定できる

サンプルコードの紹介

今回利用したサンプルコードはPiotrPrusさんのMotionLayoutPlaygroundリポジトリです。こちらのリポジトリでは、いくつかMotionLayoutのサンプルがありますが、「scene13(RV with ML)」のTinder風アニメーションのMotionLayoutについて紹介します。まずは以下のキャプチャを見て挙動を確認しましょう。

motionlayout.gif

グリッドリスト上に複数のCardViewがあり、そのCardViewを左にスワイプする(like)と、リストから削除されます。反対に右にスワイプする(dislike)と先ほどと同様にリストから削除されるようにMotionLayoutで実装されています。

サンプルコードの解説

対象画面のコード一式は下記の通りです。

item_scene_13_grid.xmlの中身を見てみると、Viewの要素にCardViewとそれに対応したlike、dislike用のオーバーレイが配置されています。

item_scene_13_grid.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:clipChildren="false"
    android:clipToPadding="false"
    app:layoutDescription="@xml/ml_scene_13">

    <!-- CardView -->
    <com.google.android.material.card.MaterialCardView
        android:id="@+id/cardViewScene13"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorGrey"
        app:cardBackgroundColor="@color/colorBlue"
        app:cardCornerRadius="10dp"
        app:cardElevation="4dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/item_number"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="1" />

            <TextView
                android:id="@+id/content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:textAppearance="?attr/textAppearanceListItem"
                tools:text="test" />
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <!-- Like用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13LikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_like"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />

    <!-- Dislike用のオーバーレイ -->
    <View
        android:id="@+id/cardScene13DislikeOverlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/frame_overlay_dislike"
        app:layout_constraintBottom_toBottomOf="@id/cardViewScene13"
        app:layout_constraintEnd_toEndOf="@id/cardViewScene13"
        app:layout_constraintStart_toStartOf="@id/cardViewScene13"
        app:layout_constraintTop_toTopOf="@id/cardViewScene13" />
</androidx.constraintlayout.motion.widget.MotionLayout>

Motion Editorで処理の流れを見る

では、MotionLayoutの構成を見てみましょう。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32333339312f37323063313430372d636338312d336136322d363431312d3437613231666330613363662e706e67.png

グリッドリスト部分に当たる、item_scene_13_grid.xmlをMotion Editorを開いたキャプチャです。これまではXMLで書いていたものが、GUIで表示、設定できるようになりました!Motion Editorを見てみると、5つのConstraintSetで構成されています。

  • rest : 初期状態
  • like : いいね
  • goneRight : 右に消える
  • dislike : 好まない
  • goneLeft : 左に消える

ConstraintSetとは、Viewの状態を定義でき、例えば初めのViewの状態と終わりのViewの状態やアニメーションを定義することができます。これら複数のConstraintSetをどのように組み合わせてlike、dislikeの処理を行なっているのでしょうか?それぞれ確認してみたい思います。

like処理

ConstraintSet rest

CardViewを表示する部分に当たるConstraintSet restを選択すると、ConstraintSetの中に3つのConstraintが定義されていて、cardViewScene13、cardScene13LikeOverlay、cardScene13DislikeOverlayのConstraintがあります。さらにConstraint cardViewScene13を選択すると、それぞれプロパティが設定されています。

スクリーンショット 2019-12-01 20.47.37.png

ConstraintSet restを矢印がlikeとdislikeのConstraintSetに設定されています。まずはlikeの矢印を見てみますが、矢印はTransitionを表します。Transitionを再生すると右に回転していくことがわかります。

スクリーンショット 2019-12-01 19.30.16.png

ちなみにrestからlikeのTransition上のマークはスワイプやクリックイベントを表すマークとなっています。
スクリーンショット 2019-12-03 8.38.23.png

ConstraintSet like

次にいいねの処理に当たる、ConstraintSet likeについて見てみます。CardViewが回転されている状態であることがわかります。

スクリーンショット 2019-12-01 19.43.39.png

Constraint cardViewScene13で右にローテーションしていることがわかります。
スクリーンショット 2019-12-01 21.28.03.png

次にTransitionがlikeからgoneRightへと定義されており、Transitionを再生してみるとViewが画面外に移動し見えなくなることがわかります。autoTransitionプロパティにanimateToEndが設定されていますが、これによってlikeからgoneRight状態に自動的にアニメーション化されます。
スクリーンショット 2019-12-01 19.47.06.png

ConstraintSet goneRight

ここまでrestからlike、goneRightまでConstraintSetを組み合わせてアニメーションを実装していることがわかりました。ConstraintSet goneRightの中にはConstraintとして、cardViewScene13などが定義されており、左のプレビューからcardViewScene13が画面でかつalpha0になっていることがわかります。このようにして右スワイプしてCardViewが消えるアニメーションを実装されています。

スクリーンショット 2019-12-01 21.33.59.png

以上がlike処理になります。restなどのConstraintSet以下に破線がありますが、これはConstraintSetのderiveConstraintsFromプロパティを利用すると表示されます。likeの場合はrestから派生しているという意味になります。リストから削除されている部分については、コード側で実装されているため別途後述します。次にdislike処理についても確認します。

dislike処理

ConstraintSet dislike

likeと同様にConstraintSetでrestからdislikeが定義されています。左スワイプをされるとローテートされた状態がConstraintSet dislikeです。さらにConstraintSet dislikeのcardViewScene13を見てみると左向きにローテーションされているのがわかります。

スクリーンショット 2019-12-01 22.59.00.png

次にTransitionを見てみるとConstraintSet dislikeからConstraintSet goneLeftに設定されているのがわかります。
スクリーンショット 2019-12-01 23.04.23.png

ConstraintSet goneLeft

rest -> dislike -> goneLeft ConstraintSetとTransitionが定義されており、goneLeftでは、alpha:0で透過し、rotation:-70、layout_marginEnd:600dpで画面外にViewが移動していることがわかります。ここまでがdislikeのアニメーションになります。

スクリーンショット 2019-12-01 23.06.17.png

like、dislike後のリスト削除

likeとdislike後にリストから削除されるようになっていて、ここの部分はコード側で実装されているため解説します。
Scene13Fragmentでは、MySceneGridItemRecyclerViewAdapterを使っています。リスト削除についてですが、MotionLayout.TransitionListenerインターフェースが提供されていて、goneRight、goneLeftのトランジッションが完了したタイミングでリストから削除しています。

MySceneGridItemRecyclerViewAdapter.kt
class MySceneGridItemRecyclerViewAdapter(
    private val mValues: MutableList<DummyItem>,
    private val animationListener: (state: AnimationState) -> Unit
) : RecyclerView.Adapter<MySceneGridItemRecyclerViewAdapter.ViewHolder>() {
    // 〜省略〜

    private val removeItemListener: (Int) -> Unit = {
        Handler().postDelayed({
            mValues.removeAt(it)
            notifyDataSetChanged()
        }, 100)
    }

    inner class ViewHolder(val mView: View, removeItemListener: (Int) -> Unit) :
        RecyclerView.ViewHolder(mView) {
        val mIdView: TextView = mView.item_number
        val mContentView: TextView = mView.content
        internal var position = 0

        override fun toString(): String {
            return super.toString() + " '" + mContentView.text + "'"
        }

        init {
            (mView as MotionLayout).setTransitionListener(object : MotionLayout.TransitionListener {
                override fun onTransitionTrigger(
                    p0: MotionLayout?,
                    p1: Int,
                    p2: Boolean,
                    p3: Float
                ) {
                }

                override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
                    when {
                        p1 == R.id.rest && p3 > 0f -> animationListener(AnimationState.STARTED)
                    }
                }

                override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
                    animationListener(AnimationState.COMPLETED)
                    if (p1 == R.id.goneRight || p1 == R.id.goneLeft) {
                        Log.d("Adapter", "Remove item at position: $position")
                        removeItemListener(position)
                    }
                }

                override fun onTransitionStarted(p0: MotionLayout?, startId: Int, endId: Int) {

                }
            })
        }
    }
}

おわりに

簡単にでしたが、MotionLayoutのサンプルを用いてMotion Editorについて触れてみました。GUIによってConstraintSetとTransitionの関係性がわかりやすくなり、Transitionも手軽に再生できるのでとても便利です。Motion EditorはAndroid Studio 4.0 Canaryの新機能でもっと触っておかしいところがあればIssueを投げていけるように頑張ります。

PR

コネヒトではエンジニアを募集しています!家族向けサービスをつくりたいエンジニアの皆さん、お待ちしています。
Connehito Image
家族の課題を解決するサービスのMAUを増やすAndroidエンジニア募集!

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

Coroutines私的メモ2~Dispatchers, withContext, async/await~

私的メモ1の続きです

前提

前回同様、現在のスレッドがわかるようにloggerメソッドを仕込むことにします

fun logger(msg: String) = println("${Thread.currentThread().name} : $msg")

dispatchers

コルーチンを使う際に、どのスレッドもしくはスレッドプールにて実行・制御したいということがあります

その際にdispatcherを指定することで解決できます Dispatcherにもいくつか種類があるので使い分けたいところです

Dispatchers.Main

UIに関する処理をするメインスレッドのためのコルーチン

Dispatchers.Main.immediate

UI更新を即時行いたい際に使用される (e.g. textviewのtext更新とか)

Dispatchers.Default

共有スレッドプールを使用し、CPUのコア数に従って設定される
CPUに負荷をかけるような計算をするような処理に対して使うことが推奨されます

Dispatchers.IO

その処理に必要なコルーチンの数に応じて必要な分だけ共有スレッドプールを使用する
DBの検索等、I/Oに負荷をかけるような処理に対して使うことが推奨されます

Dispatchers.Unconfined

特定のスレッドに限定させない
一度停止させて再開した場合は別スレッドに切り替わっている可能性あり

試しにこのようなプログラムを組んでみます

fun unconfined() = runBlocking {
    launch(Dispatchers.Unconfined) {
        logger("Hello")
        delay(1000L)
        logger("World")
    }
}

実行結果はこのように、スレッドが異なっていますね

main : Hello
kotlinx.coroutines.DefaultExecutor : World

withContext

コルーチンの実行スレッドを切り替えたいときに使う
withContextの引数に上述したDispatcherを指定してあげる
そのため、切り替える必要がない単純な場合はlaunch(対象のDispatcher){}で事足りると思います

例としては下記のように、ローディングを走らせ、別のスレッドでデータを引っ張ってきて、そのデータを元にUIを更新するなどです

loading()updateList(data) はtop-levelで指定しているMainスレッドで処理されますが、データをひっぱってきている fetchDataは別スレッドでの処理になります

fun update() {
  launch(Dispatchers.Main) {
    loading()
    val data = withContext(Dispatchers.Default) { repository.fetchData() }
    updateList(data)
  }
}

async/await

コルーチンからの返り値を使って複数の結果を待ち合わせて処理したい場合に用いられます

例えば上述した updateメソッドのfetchDataが他のデータも必要な場合ですね

fun update() {
  launch(Dispatchers.Main) {
    loading()
    val feedData = async (Dispatchers.Default) { repository.fetchFeed() }
    val adData = ahync (Dispatchers.Default) { repository.fetchAd() }
    val data = feedData.await() + adData.await()
    updateList(data)
  }
}

withTimeout or withTimeoutOrNullでタイムアウトの指定もできます
ネットワーク環境が悪かったり、遅い結果でユーザ体験を損なわせないことを目的として使えそうですね

単純な例を示します

fun asyncAwaitWithTimeout() {
    runBlocking {
        logger("Hello")
        val simpleTask = async(Dispatchers.Default) {
            2 + 2
        }

        val heavyTask = async(Dispatchers.Default) {
            logger("World")
            delay(3000L)
            4 + 4
        }

        logger("Calculating...")
        val total = withTimeoutOrNull(2) { simpleTask.await() + heavyTask.await() }
        logger("total=$total")
    }
}

ただ足し算をしてその結果を返しているだけですね
ここでは、簡単なタイムアウトを2秒と定義し、時間のかからない簡単なタスクと実行に最低3秒はかかってしまう重いタスクを定義して、わざとタイムアウトさせるようにしています

結果としては以下の様になります

main : Hello
main : Calculating...
DefaultDispatcher-worker-1 : World
main : total=null

今回はwithTimeoutOrNullを使ったので、合計の12が返るのではなく、タイムアウトしたのでnullが返りました

次回はCoroutines Flowを扱う予定です

参考

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

【Android】世界一わかりやすいRecyclerViewの実装

この記事はFUN Advent Calender 2019の3日目の記事です。

昨日の記事は、【ガチ比較】登校ルートをチャリで往くでした。


前置き

Androidアプリ開発で、長ーいリストや逐次中身のデータ更新があるリストを扱いたいこと、結構あります。
そこでよく使われるのがRecyclerViewというものです。

この記事を書こうとしたきっかけとしては、大抵の人がRecyclerViewについて検索してたどり着く記事は大抵RecyclerViewを単体で扱ってくれていないというところからです。
RxJava....? DataBind....? 記事を読む人の学習コストをわざわざ上げていることがよくわかりません。分かっているなら話は別ですが。

ということで、今回はRecyclerViewをとりあえず動かせる最低限のコードをかんたんに解説していきます。プロジェクト全体はGitHubにあげていますので参考にしてください。
https://github.com/DaisenKudo/TheMostSimpleRecyclerView/tree/master

本題

材料

コード

  • MainActivity.kt : 今回はFragmentの入れ物に徹していただきます。
  • MainFragment.kt : 今回はRecyclerViewの入れ物に徹していただきます。
  • MainViewAdapter.kt : RecyclerViewのアレコレを管理してもらいます
  • MainViewHolder.kt : RecyclerViewにいれるアイテム(一つ分)のガワを定義します。
  • ItemModel.kt : RecyclerViewにいれるアイテム(一つ分)の中身を定義します。

レイアウト

  • main_activity.xml : Activityのレイアウト
  • main_fragment.xml : Fragmentのレイアウト
  • part_item_model.xml : RecyclerViewに入れたいアイテムのレイアウト

外部ライブラリ

appディレクトリに入っている方のbuild.gradleのdependenciesの中に以下を追記してください。

build.gradle
implementation 'androidx.recyclerview:recyclerview:1.1.0'

レイアウト

特にコレといって言うことはないです。
別にRecyclerViewに入れるアイテムはConstraintLayoutを使わなくてもいいです。
findViewByIdしたときの型だけ注意してください。

activity_main

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container_main_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.ui.main.MainActivity" />

fragment_main

fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/container_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

part_item_model

part_item_model
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="30dp">

    <TextView
        android:id="@+id/tv_item_model"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

コード

コードで結構陥りやすいのが、「Unresolved reference: R」というエラーです。
import文内のio.github.qlain.themostsimplerecyclerviewを適宜
(プロジェクトのPackage Nameに読み替えてください。Android Studioなら自動でimportを書いてくれますが、たまーにやってくれないことがあるので。

Model

RecyclerView内に入れるデータをまとめておくためのクラスです。data classにしてしまってもいいかもしれません。
今回はStringだけを格納していますが、Bundleのように決められたものしか入れられないってわけではないので、柔軟なリストが作れます。

ItemModel.kt
package io.github.qlain.themostsimplerecyclerview.model

class ItemModel {
    var text: String = ""
}

MainViewHolder

RecyclerViewで使いたいパーツを定義しておきます。
RecyclerViewのアイテム1つごとにViewAdapterによってインスタンス化(アイテムごとにMainViewHolderが1対1で紐付けられる)されます。
ここらへんかどうなっているかを知りたければ、公式リファレンスとか読むといいかもです。
道具として使うだけなら分からなくてもOKです。

MainViewHolder.kt
package io.github.qlain.themostsimplerecyclerview.model

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R

class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val textView: TextView = itemView.findViewById(R.id.tv_item_model)
}

MainViewAdapter

RecyclerViewのアレコレを管理します。
RecyclerView内のアイテム更新とかはここでやりましょう。
Fragmentからメソッドを呼び出す形でもいいと思います。

MainViewAdapter.kt
package io.github.qlain.themostsimplerecyclerview.view.ui.main

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R
import io.github.qlain.themostsimplerecyclerview.model.ItemModel
import io.github.qlain.themostsimplerecyclerview.model.MainViewHolder

class MainViewAdapter(
    private val list: List<ItemModel>,
    private val listener: ListListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    interface ListListener {
        fun onClickItem(tappedView: View, itemModel: ItemModel)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.part_item_model, parent, false)
        return MainViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        holder.itemView.findViewById<TextView>(R.id.tv_item_model).text = list[position].text
        holder.itemView.setOnClickListener {
            listener.onClickItem(it, list[position])
        }
    }

    override fun getItemCount(): Int = list.size
}

MainActivity

MainFragmentの表示だけです。

MainActivity.kt
package io.github.qlain.themostsimplerecyclerview.view.ui.main

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.github.qlain.themostsimplerecyclerview.R

class MainActivity : AppCompatActivity() {

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

        supportFragmentManager.beginTransaction().replace(
            R.id.container_main_activity,
            MainFragment()
        ).commit()
    }
}

MainFragment

定義したRecyclerViewを表示させます。
今回はgenerateItemList()で花丸ちゃん、おまんじゅうn個目だよというテキストを生成しています。
val recyclerAdapter = MainViewAdapter(generateItemList(), object : MainViewAdapter.ListListener {の行のgenerateItemList()を差し替えると動的にリストを変更することもできます。

ConstraintLayoutなのにLinearLayoutManager...????と思うかもしれませんが気にしないでください。大丈夫です。

ちなみに、onDestroyViewの処理を実行しないと、メモリリークの原因になります。(ActivityよりRecyclerAdapterが長生きしてしまう恐れがある)

MainFragment.kt
package io.github.qlain.themostsimplerecyclerview.view.ui.main

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.qlain.themostsimplerecyclerview.R
import io.github.qlain.themostsimplerecyclerview.model.ItemModel

class MainFragment : Fragment(){
    private var recyclerView: RecyclerView? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        this.recyclerView = view.findViewById(R.id.container_recycler_view)

        this.recyclerView?.apply {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(context)
            itemAnimator = DefaultItemAnimator()
            adapter = MainViewAdapter(
                generateItemList(),
                object : MainViewAdapter.ListListener {
                    override fun onClickItem(tappedView: View, itemModel: ItemModel) {
                        this@MainFragment.onClickItem(tappedView, itemModel)
                    }
                }
            )
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        this.recyclerView?.adapter = null
        this.recyclerView = null
    }

    //RecyclerViewの生成時に一度だけ動く
    private fun generateItemList(): List<ItemModel> {
        val itemList = mutableListOf<ItemModel>()
        for (i in 0..100) {
            val item: ItemModel = ItemModel().apply {
                text = "花丸ちゃん、おまんじゅう${i}個目だよ"
            }
            itemList.add(item)
        }
        return itemList
    }

    //RecyclerView内のアイテムがクリックされたときに動く
    private fun onClickItem(tappedView: View, itemModel: ItemModel) {
    }
}

できるもの

ちょっと画質が良くないですが、このようなものができます。
20191202_224646.gif

まとめ

再掲ですが、今回作ったコードはここに上げておきます。
わからないことはIssueTwitterで聞いてください。

ViewHolderはViewAdapterのインナークラスにしてしまってもいいと思います。
MIT Licenseとはいっていますが、誰が書いても似たようなコードになると思うので、アレコレしても怒りません。なんなら写経でも

DataBind絶対に許さん

Advent Calender、明日はnao(ki)さんです。

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

[Android / Kotlin / OkHttp] JSONをPOSTする話と拡張関数を触った話。

どうもこんばんわ

本題

インターネットで何か情報をとってくるアプリを作るのは楽しいものです。
そこでWebAPIをよくたたきますが、その時に便利なライブラリが「OkHttp」です。
HttpURLConnectionよりわかりやすく書くことができます。

なんのAPIを叩くのか

調べるの面倒なので身近なMastodonの投稿するAPIを叩こうと思います。
青い鳥と違いアクセストークンは何もしなくても設定画面の開発から生成することができます。すごい。

image.png

OkHttp導入

    implementation("com.squareup.okhttp3:okhttp:4.2.2")

POSTまで書く。

これ書く前にAndroidManifestにインターネットのパーミッション書き足してね。忘れがち

AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET"/>
MainActivity.kt
    val accessToken = "アクセストークン"
    val instance = "インスタンス名"

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


        //特にWebAPIが思いつかなかったのでMastodonへ投稿するAPIをたたく。POST
        val url = "https://${instance}/api/v1/statuses"
        //POSTするときに送るJSON
        val postJSON = JSONObject().apply {
            put("access_token",accessToken)
            put("status","ねむい")
            put("visibility","direct")
        }

        //JSONをRequestBodyにする
        val requestBody = RequestBody.create("application/json".toMediaTypeOrNull(),postJSON.toString())

        val request = Request.Builder()
            .url(url)
            .post(requestBody)
            .build()
        val okHttpClient = OkHttpClient()
        okHttpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                runOnUiThread {
                    //UIスレッド
                    //しっぱい
                    Toast.makeText(this@MainActivity,"問題が発生しました",Toast.LENGTH_SHORT).show()
                }
            }

            override fun onResponse(call: Call, response: Response) {
                runOnUiThread {
                    //UIスレッド
                    if(response.isSuccessful){
                         //成功
                        Toast.makeText(this@MainActivity,"成功しました ${response.code}",Toast.LENGTH_SHORT).show()
                    }else{
                        //しっぱい
                        Toast.makeText(this@MainActivity,"問題が発生しました ${response.code}",Toast.LENGTH_SHORT).show()
                    }
                }
            }
        })
    }

非同期処理です。onResponseの中UIスレッドではないのでUIいじる(TextViewに値入れるなど)すると落ちます。
UIいじるときはrunOnUiThread{}などを使っていきましょう。
Android 11でAsyncTaskが非推奨になるとか。ソースはXDA Developersさんです

うまくいってれば投稿されています。
image.png

RequestBody.createが非推奨になっている。

このままだとRequestBody.createに線が入っています。非推奨に。

解決

Alt+Enterを押して置き換えましょう。

image.png

するとこうなるんですね。

val requestBody = postJSON.toString().toRequestBody("application/json".toMediaTypeOrNull())

toRequestBodyどっから出てきたの

Stringにそんなのあった?

Kotlinの拡張関数という機能を使っているみたい。

プログラミングはアプリを作るためにやっているので詳しいわけではありませんが、
こんな関数あればいいのになーって時に追加できるみたいです。

拡張関数を触ってみる

消費税込みの値段を返す拡張関数を書いてみた。シンプル。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.title="拡張関数"

        //消費税をかける拡張関数。
        val nedan = 500
        textview.text =nedan.addTax().toString()

    }

    /*
    * 消費税をかける拡張関数。
    * */
    fun Int.addTax(): Int {
        return (this*1.10).toInt()
    }

名前は適当。

return (this*1.10).toInt()

この例ではthisの部分には500が入ります。

終わりです。お疲れ様でした。

参考にしました。

https://square.github.io/okhttp/recipes/#posting-a-string-kt-java
https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/extensions.html

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