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

【Flutter】Flutter製アプリをリリースしてみた、あとARとかも使ってみた話

アプリ概要

ゴルフのオリンピックゲームの計算やボールとピンまでの距離のAR測定,1−18までの乱数を生成することができる、ゴルフ幹事向けのアプリです。

App Store はこちら
Google Play Store はこちら

image.png

ファイル名ファイル名ファイル名

使用した技術

  • bloc
  • Provider
  • rxDart
  • Admob
  • Firebase Analytics
  • ARKit(iOSのみ)

ソースコードはこちら

https://github.com/Tetsukick/enGolf

AR技術のご紹介

iPhoneのみARKitを活用したプラグイン(arkit_flutter)が公開されていたので、そちらを活用して、タップした2点間の距離を測れるアプリを作成いたしました。

AR距離測定画面のソースコードを記載しておきます。
Sampleを80%ぐらい流用して簡単に開発できました。

import 'package:arkit_plugin/arkit_plugin.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;

import 'dart:io';

class ARMeasureScreen extends StatefulWidget {
  @override
  _ARMeasureScreen createState() => _ARMeasureScreen();
}

class _ARMeasureScreen extends State<ARMeasureScreen> {
  ARKitController arkitController;
  vector.Vector3 lastPosition;

  @override
  void dispose() {
    arkitController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return Platform.isIOS ? Scaffold(
      body: Container(
        child: ARKitSceneView(
          enableTapRecognizer: true,
          onARKitViewCreated: onARKitViewCreated,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        backgroundColor: Colors.lightGreen,
        onPressed: () {
          arkitController.remove('point');
          arkitController.remove('text');
          arkitController.remove('line');
          lastPosition = null;
        },
      ),
    ) :
    Scaffold(
      body: Center(
        child: Text('Androidには対応しておりません\n現在開発中のためしばらくお待ち下さい。'),
      ),
    );
  }

  void onARKitViewCreated(ARKitController arkitController) {
    this.arkitController = arkitController;
    this.arkitController.onARTap = (ar) {
      final point = ar.firstWhere(
            (o) => o.type == ARKitHitTestResultType.featurePoint,
        orElse: () => null,
      );
      if (point != null) {
        _onARTapHandler(point);
      }
    };
  }

  void _onARTapHandler(ARKitTestResult point) {
    final position = vector.Vector3(
      point.worldTransform.getColumn(3).x,
      point.worldTransform.getColumn(3).y,
      point.worldTransform.getColumn(3).z,
    );
    final material = ARKitMaterial(
        lightingModelName: ARKitLightingModel.constant,
        diffuse: ARKitMaterialProperty(color: Colors.blue));
    final sphere = ARKitSphere(
      radius: 0.006,
      materials: [material],
    );
    final node = ARKitNode(
      name: 'point',
      geometry: sphere,
      position: position,
    );
    arkitController.add(node);

    if (lastPosition != null) {
      final line = ARKitLine(
        fromVector: lastPosition,
        toVector: position,
      );
      final lineNode = ARKitNode(
          name: 'line',
          geometry: line
      );
      arkitController.add(lineNode);

      final distance = _calculateDistanceBetweenPoints(position, lastPosition);
      final point = _getMiddleVector(position, lastPosition);
      _drawText(distance, point);
    }
    lastPosition = position;
  }

  String _calculateDistanceBetweenPoints(vector.Vector3 A, vector.Vector3 B) {
    final length = A.distanceTo(B);
    return '${(length * 100).toStringAsFixed(2)} cm';
  }

  vector.Vector3 _getMiddleVector(vector.Vector3 A, vector.Vector3 B) {
    return vector.Vector3((A.x + B.x) / 2, (A.y + B.y) / 2, (A.z + B.z) / 2);
  }

  void _drawText(String text, vector.Vector3 point) {
    final textGeometry = ARKitText(
      text: text,
      extrusionDepth: 1,
      materials: [
        ARKitMaterial(
          diffuse: ARKitMaterialProperty(color: Colors.red),
        )
      ],
    );
    const scale = 0.001;
    final vectorScale = vector.Vector3(scale, scale, scale);
    final node = ARKitNode(
      name: 'text',
      geometry: textGeometry,
      position: point,
      scale: vectorScale,
    );
    arkitController.add(node);
  }
}

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

[Navigation] Single Activity構成時のアナリティクスのスクリーン計測Tips

Navigationコンポーネントを使い、アプリをSingle Activity構成にした際のアナリティクス(Firebase Analytics)のTipsを紹介します。

存在感の強い MainActivity

アナリティクスを使う場合、Activityは自動で計測されるため、特に意識せずとも画面遷移がトラッキングされ便利です。
https://firebase.google.com/docs/analytics/screenviews

しかしながら、自動計測はActivityのみのため、Navigationコンポーネントを使ったSingle Activity構成の場合に、そのままだと 存在感の強いMainActivity が出来上がってしまいます。
スクリーンショット_2020-05-28_11_11_51.png

addOnDestinationChangedListener を使うとシュッと対応できる?

存在感の強いMainActivityを無くすために、各Fragmentにトラッキング用の仕組みを入れても良いかと思いますが、Navigationの画面遷移時に呼ばれる NavController#addOnDestinationChangedListener を使うことでシュッと対応することができます。

https://developer.android.com/reference/androidx/navigation/NavController#addOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener)

サンプルコード

addOnDestinationChangedListener を使ったサンプルコードです。

MainActivity.kt
val navController: NavController = ...

navController.addOnDestinationChangedListener { _, destination, _ ->
    // ① 型チェック
    if (destination is FragmentNavigator.Destination) {
        // ② Fragmentのクラス名を取得
        val screenName = destination.className.split(".").last()
        // ③ 送信
        FirebaseAnalytics.getInstance(this).setCurrentScreen(this, screenName, screenName)
    }
}

ポイントを解説します。

① 型チェック

addOnDestinationChangedListenerdestination は型が NavDestination となっており、そのままだとクラス名が取得できません。

そこで、 FragmentNavigator.Destination かどうかをチェックし、スマートキャストでクラス名( className )を取得できるようにします。

ちなみにNavigationでDialogFragmentを使っている場合はこれだと対応出来ないので、 DialogFragmentNavigator.Destination かどうかのチェックも必要になりそうです。

② Fragmentのクラス名を取得

FragmentNavigator.Destination#classNamecom.example.ExampleFragment のようにパッケージ名も含んでいるため、 雑に split(".").last() とすることでFragmentのクラス名だけを抽出しています。

③ 送信

②で抽出した screenNamesetCurrentScreen() で送信します。
setCurrentScreen (Activity activity, String screenName, String screenClassOverride)

以上で、Single Activity構成時でも画面遷移時にいい感じにトラッキングができるようになりました。

まとめ

簡単ではありますが、Navigationコンポーネントを使い、アプリをSingle Activity構成にした際のアナリティクスのTipsを紹介しました。

addOnDestinationChangedListener 便利 :relaxed:

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

recyclerViewにステータスバー分のパディングを入れる

最初に

このページはステータスバーを透過させた際にステータスバーのメニューと
リストの内容が被ってしまうことがあるため備忘録として記させていただきます

実装方法

MainFragment.kt
 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter.onViewCreated()
        recyclerView.requestApplyInsets()
        recyclerView.setOnApplyWindowInsetsListener{v, insets ->
            v.setPadding(0,insets.systemWindowInsetTop,0,0)
            return@setOnApplyWindowInsetsListener insets
        }
    }

setOnApplyInstesListener内のinsets.systemWindowInsetTopでステータスバー分の高さを取得してパディングに設定します
ステータスバー分下げるだけの場合はsetOnApplyWindowInsetsListener内のみの
処理で十分ですが、ここで一つ注意していただきたいのがタブなどでフラグメントを
制御している場合、遷移後にsetOnApplyWindowInsetsListener内に処理が入ってくれずパディングが設定されないことがあるので
requestApplyInsets()を書いてあげることにより一度リセットをしてくれるため
再度中身の処理に入ってくれます

最後に

前回ステータスバーの透過方法を書かせていただいたのですが仕事で作っている
アプリでrecyclerViewに実装した際にパディングが実装されない不具合に直面した
ため書かせていただきました

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

Android App Bundleをコマンドラインで署名して生成する方法

はじめに

初めてAndroidアプリのリリースビルドを行ったのですが、いくつかつまづいたところがあったので、体系的にまとめることにします。

Android Studioで行うとかんたんだと公式ドキュメントに記載されていますが、今後CD環境を構築することを考慮し、コマンドラインでビルドすることにしました。

「Android App Bundle」とは?

拡張子が( .aab )であることもあり、本記事では「AAB」と略します。

アプリのコードとリソースをモジュールにまとめる署名付きのバイナリファイルです。
AABをPlay Consoleにアップロードすることで、ユーザーに提供するさまざまなAPKを自動で生成します。

Android Studio 3.2 以降で使えます。

AABのメリット

従来のAPK形式と比べ、以下のメリットがあります。

メリットは他にもあります。詳細は公式ドキュメントをご参照ください。
https://developer.android.com/platform/technology/app-bundle?hl=ja

APKを効率的に管理できる

1つのAABからさまざまなAPKを生成できるため、APKを1つずつ署名やアップロードする必要がなくなります。

アプリのサイズが小さくなる

Google PlayのDynamic DeliveryはAABを使い、各デバイスの設定に合わせて最適化したAPKをビルドして配信します。
ユーザーは自分のデバイスでのみ使われるコードとリソースのみダウンロードされるようになり、アプリのサイズが小さくなります。

ビルド時間が短縮される

Gradleなどによるビルドは、モジュール式アプリ向けに最適化されているため、大規模なモノリシックアプリに比べてビルド時間を大幅に短縮できます。

環境

  • OS:macOS Mojave 10.14.6
  • Android Studio: 3.6.1
  • Kotlin:1.3.72
  • Gradle:6.3
  • Gradle plugin:3.6.3

AABの生成手順

AABの生成手順を説明します。

秘密鍵の生成

まず、署名に使う「Keystore(キーストア)」と呼ばれる秘密鍵を生成します。

公式ドキュメントに記載されている通りのコマンドを実行します。
https://developer.android.com/studio/build/building-cmdline?hl=ja

$ keytool -genkey -v -keystore {Keystore name} -keyalg RSA -keysize 2048 -validity 10000 -alias {Alias name}

私はDroidKaigi 2020のアプリを参考に、キーストア名を release.keystore 、エイリアス名を対象アプリ名(私の場合は uhooipicbook )にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L34
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L46

$ keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias uhooipicbook

秘密鍵はウィザード形式で生成します。

キーストアのパスワードを入力してください:  

キーストアのパスワードを入力します。
後ほど使うので、忘れないようにしてください。

新規パスワードを再入力してください: 

確認のため、キーストアのパスワードを再度入力します。

姓名は何ですか。

自分の名前をフルネームで入力します。
私は uhooi と入力しました。

組織単位名は何ですか。

所属組織の単位名を入力します。
私は個人開発なので personal と入力しました。

組織名は何ですか。

組織の名前を入力します。
私は THE Uhooi と入力しました。

都市名または地域名は何ですか。

都市名または地域名を入力します。
私は Setagaya-ku と入力しました。

都道府県名または州名は何ですか。

日本の場合、都道府県名を入力します。
私は Tokyo と入力しました。

この単位に該当する2文字の国コードは何ですか。

国コードを入力します。
私は JP と入力しました。

CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JPでよろしいですか。

確認のため、今まで入力した内容が表示されます。
修正する場合はEnterキーを押下し、問題ない場合は y を入力します。

10,000日間有効な2,048ビットのRSAの鍵ペアと自己署名型証明書(SHA256withRSA)を生成しています
        ディレクトリ名: CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JP
<uhooipicbook>の鍵パスワードを入力してください
        (キーストアのパスワードと同じ場合はRETURNを押してください): 

鍵のパスワードを入力します。
こちらも後ほど使うので、忘れないようにしてください。

新規パスワードを再入力してください: 

確認のため、鍵のパスワードを再度入力します。

[release.keystoreを格納中]

Warning:
JKSキーストアは独自の形式を使用しています。"keytool -importkeystore -srckeystore release.keystore -destkeystore release.keystore -deststoretype pkcs12"を使用する業界標準の形式であるPKCS12に移行することをお薦めします。

こちらの警告が気になりますが、私は無視してしまいました。対応方法をご存じの方は教えていただけると嬉しいです。

これで現在のフォルダに release.keystore が存在したら秘密鍵の生成は完了です。

秘密鍵の格納

生成した秘密鍵を対象モジュールのルートフォルダに格納します。
私の場合、 app フォルダ直下に格納しました。
スクリーンショット_2020-05-28_15_06_37.jpg

秘密鍵をバージョン管理から無視

秘密鍵は外部に漏れてはいけないため、パブリックリポジトリで開発している場合はバージョン管理の対象外にします。

Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。

.gitignore
+ *.keystore

デバッグ用の秘密鍵をコミットしたい場合は、ワイルドカードで指定した上で例外として定義すると安全です。

.gitignore
*.keystore
!app/debug.keystore

DroidKaigi 2020のアプリを参考にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/.gitignore#L250-L251

build.gradle に署名の設定を追加

モジュールレベルの build.gradle に署名の設定を追加します。

プライベートリポジトリの場合、キーストアと鍵のパスワードを直接記述してもいいと思います。
パブリックリポジトリの場合はパスワードが流出したら問題なので、何らかの方法で隠蔽する必要があります。
私はDroidKaigi 2020のアプリを参考に、環境変数から取得するようにしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L45
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L47

/app/build.gradle
android {
+     signingConfigs {
+         release {
+             storeFile file("release.keystore")
+             storePassword System.getenv("RELEASE_KEYSTORE_STORE_PASSWORD")
+             keyAlias "uhooipicbook"
+             keyPassword System.getenv("RELEASE_KEYSTORE_KEY_PASSWORD")
+         }
+     }
    buildTypes {
        release {
+             signingConfig signingConfigs.release
        }
    }
}

パスワードの隠蔽方法は環境変数を使う以外にもあり、公式ドキュメントでは keystore.properties を使う方法が紹介されています。
https://developer.android.com/studio/publish/app-signing?hl=ja#secure-shared-keystore

AABの生成

ここまでで設定は完了です。

あとはキーストアと鍵のパスワードをエクスポートし、対象モジュールで bundle{Variant} タスクを実行することで、AABが生成されます。
私の場合、 app モジュールの Release バリアントなので、以下のコマンドを実行します。

$ export RELEASE_KEYSTORE_STORE_PASSWORD={キーストアのパスワード}
$ export RELEASE_KEYSTORE_KEY_PASSWORD={鍵のパスワード}
$ ./gradlew :app:bundleRelease

ビルドに成功すると、 {Module name}/build/outputs/bundle/{Variant} フォルダに {Module name}-{Variant}.aab が生成されます。
私の場合、 app/build/outputs/bundle/release フォルダに app-release.aab が生成されました。

エラー:キーストアファイルが対象フォルダにない

以下のエラーが発生する場合、キーストアのファイルが対象モジュールのルートフォルダに存在しません。
配置しているか確認してください。

> Keystore file '.../{Project name}/app/release.keystore' not found for
 signing config 'release'.

エラー:キーストアまたは鍵のパスワードを設定していない

以下のエラーが発生する場合、キーストアまたは鍵のパスワードの環境変数をエクスポートしていません。
環境変数を使う場合は忘れずにエクスポートしてください。

* What went wrong:
Execution failed for task ':app:signReleaseBundle'.
> A failure occurred while executing http://com.android.build.gradle.internal.tasks.Workers$ActionFacade
   > kotlin.KotlinNullPointerException (no error message)

おわりに

これでコマンドラインでAABを生成することができました!
あとはPlay Consoleにアップロードすることで、テストやリリースを行えます。

参考リンク

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

ローカライズファイルをAndroidとiOSで共通管理する

はじめに

AndroidとiOSでサービスを展開している場合、同じ文言を使っている。
プラットフォームごとに管理することで文言の誤字が発生しやすく、コストも上がる。
ローカライズファイルを効率よく管理する方法を考えました

前提条件

  • AndroidとiOSで共通のリソースを扱う
  • 差分を追いやすいこと

githubを使って管理

  • オリジナルデータとなる文言とkeyはjsonにして管理
  • githubを使うことで差分を追いやすくなる
  • CIを使うことで自動化できる

ローカライズファイルを更新するツール

  • JSONからローカライズファイルを生成するのを簡単にするためにツールを作成した
  • t-osawa-009/JSONToString

使い方

1.オリジナルデータとなるJSONファイルを追加

[
    {
      "key": "hoge",
      "key_android": "hoge",
      "key_ios": "hoge",
      "value_android": "hoge",
      "value_ios": "hoge"
    },
]

2.ローカライズファイルの生成を設定する.JSONToString.ymlファイルを追加

outputs:
  - key: key
    value_key: value_ios
    output: Strings/Localizable.strings
    format: strings
    sort: asc
  - key: key
    value_key: value_android
    output: Strings/strings.xml
    format: xml

3.コマンドを実行

$ JSONToString --json_path Strings.json

4.生成したファイルを各プロジェクトに追加する

"hoge" = "hoge";
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hoge">hoge</string>
</resources>

まとめ

  • githubで管理することで1元管理が可能になった
  • 運用を考えて、ツールを自作するのは有効である

参考リンク

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

Androidアプリが非SDKインターフェースを使用していないか調べる方法

分かりやすい方法としては、対象のOSをエミュレーターで起動しアプリのすべての操作を実施して以下のようなログが表示されていないか確認する方法がある。
この方法であれば発生箇所がある程度予測できるし、非SDKを使用してはならないなどの要件にも対応しやすい。

Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)

参考
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces#test-for-non-sdk

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

AndroidStudioで可変リスト表示を作る(前編)

AndroidStudioで可変リスト表示を作る(前編)

リスト表示を作ってみましょう

Android アンドロイド
Java ジャバ
Kotlin コトリン

こういうリスト表示を作ってみます。
表示が固定ではなくボタンを押すと追加されるように作ります。

画面表示に使用する文字列を登録してしまいます。

strings.xml
<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <string name="app_name">QiitaHelloSample</string>
    <string name="btAdd">追加</string>
    <string name="btDel">削除</string>
    <string name="etHint">追加する値</string>
</resources>

ListViewと追加ボタン、削除ボタンを並べたいのでレイアウトを工夫します。
activity_main.xmlに元からあるTextViewは消してしまいましょう。
スッキリしたところでデザインからLinearLayout(vertical)をドラッグして配置します。
ドラッグ先はレイアウトの画面では無く下のコンポーネントツリーにドラッグしていきます。
1.LinearLayout(vertical)
2.LinearLayout(horizontal)
3.ListView
4.Button を 2つ
5.EditText

LinerLayoutコンポーネントはvertical(縦)とhorizontal(横)がそれぞれ独立しているように見えますが実は同じでorientationの値が異なるだけです。

コンポーネントツリーのコンポーネントをドラッグして

置けたらidをそれぞれ lvMain、btAdd、btDelと付けます。
comptree.png
このようにします。
上下で位置の入れ替え、左右でツリーの階層が変わります。

1.LinearLayout(vertical)は

LinearLayout(vertical).xml
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

2.LinearLayout(horizontal)は

LinearLayout(horizontal).xml
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

3.ListViewは

ListView.xml
        <ListView
            android:id="@+id/lvMain"
            android:layout_width="wrap_content"
            android:layout_height="match_parent">

        </ListView>

4.Buttonは

Button.xml
<Button
                android:id="@+id/btAdd"
                style="@style/Widget.AppCompat.Button"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@string/btAdd" />

            <Button
                android:id="@+id/btDel"
                style="@style/Widget.AppCompat.Button"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@string/btDel" />

5.EditTextは

EditText.xml
            <EditText
                android:id="@+id/etValue"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:autofillHints="password"
                android:ems="10"
                android:hint="@string/etHint"
                android:inputType="text" />

ButtonやTextEditのlayout_weightは表示割合ですので追加ボタンと削除ボタンとテキスト入力の表示割合は1:1:1となります。値を色々変えてどのように変わるか試しましょう。

ListViewがデザイン時に配置しづらい

他のコンポーネントは置いた状態ですぐに表示されますが、ListViewは値が無いため配置がしづらく感じます。わざわざ値を用意するのも大変なのでそんな場合はListView選択状態でentriesの値を入力する所に「@」を入力すると最初から定義されている文字列リスト候補が表示されますので適当に採用します。
デザインが終わったら空白にすれば元に戻せます。

独自デザインのListViewを使う

ListViewには簡単に使える方法が用意していますがそれだとデザインが固定されます。1行に3つ以上の値を表示しようとすると用意されているものは使えませんので、ここでは独自にデザインすることが出来る方法を使います。

layout_row.png

プロジェクト構成ツリーからres -> layoutの中にはメイン画面のレイアウトしかない状態ですがここにレイアウトを追加します。マウスの右クリックから新規 -> レイアウトリソースファイルを選択し

newLayout.png

ファイル名を row.xml ※行をデザインしていますよという意味
ルート要素を LinearLayout

としてOKすると

row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

</LinearLayout>

1行分のレイアウトのベースとなるxmlファイルが作られます。
これを

row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tvDateTime"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:textSize="18sp"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/tvMsg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"/>
    </LinearLayout>

</LinearLayout>

このように変更します。デザインの説明は割愛してTextViewの2箇所にidが割り当てられています。
ここのリストとして表示させる文字列を割り当ててリストにするわけです。

ListViewデザインを確認

row.xmlをデザイン表示しても値が無いので何も表示されません。
1行分がどんな表示になるのかを確認する場合はTextViewのtextに試験的に文字を入れてしまいましょう。

BaseAdapterを継承してListViewを作る

10行分表示する独自ListViewを作ってみましょう。
独自のListViewを作るためにBaseAdapterを継承したCustomListViewクラスを定義します。
BaseAdapterには継承しないといけないgetItem、getItemId、getItemId、getCountメソッドがあるので定義します。
今回は10行分のリストにするのでgetCountの返り値を10に、getItemId、getItemIdはまだ使いませんので適当な値を返します。

getView

getViewはリスト表示が必要になった時に発生するイベントです。
p0に表示する行数が渡されます。

MainActivity.kt
class CustomListView(private val context: Activity): BaseAdapter() {
    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
        val inflater = context.layoutInflater
        val view1 = inflater.inflate(R.layout.row,null)
        var fMsg = view1.findViewById<TextView>(R.id.tvMsg)     // row.xmlレイアウトのtvMsgを参照
        fMsg.setText("Msg = $p0")   // tvMsgのtextに 引数 p0を返す
        return view1
    }
    override fun getItem(position: Int): Any {
        return "" // p0 にあるデータを返す※今回は未使用
    }
    override fun getItemId(position: Int): Long {
        return position.toLong() // 識別するためのidを返す
    }
    override fun getCount(): Int {
        return 10   // 一覧の件数
    }
}

作ったCustomListViewクラスを宣言してListViewが持つadapterに渡してあげると独自クラスが表示されます。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    val customAdaptor= CustomListView(this@MainActivity)

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

        var listView = findViewById<ListView>(R.id.lvMain)
        listView.adapter = customAdaptor
    }
}

たったこれだけのことで簡単に独自クラスを作る事が出来ます。

可変リストを作る(後編)

ListView用に使用する可変コレクションリストを作ってみます。

mutableMapOfを使って見る

まずはコレクションを作ってみます。

map.kt
 private var map = mutableMapOf<String,Any>()

このように定義しているとキーを文字列、値の型は自由に決めることが出来ます。

定義したmapに値を与えます。

map.kt
map = mutableMapOf("key" to "Jan","eng" to "January")
キー String 値 Any
key jan
eng January

要素の数に制限はありません。

map.kt
map = mutableMapOf("key" to "Jan","eng" to "January","jp" to "ジャニュアリー","id" to 1)
キー String 値 Any
key jan
eng January
jp ジャニュアリー
id 1

mutableMapOfを使うとキーと値の組み合わせでデータを管理することが出来ますが、表計算で言うところの横方向のデータだけの管理となります。
これを縦方向にも使おうとするとMutableListを使うことになります。

MutableListを使う

map.kt
private var map = mutableMapOf<String,Any>()
private var mapList : MutableList<MutableMap<String,Any>> = mutableListOf()

MutableList<MutableMapの後ろに続くは mutableMapOfを定義したものと同じものを定義します。

= mutableListOf()MutableListの初期化をしています。
ではさっそく使って見ます。

map.kt
map = mutableMapOf("key" to "Jan","eng" to "January","jp" to "ジャニュアリー","id" to 1)
mapList.add(map)
map = mutableMapOf("key" to "Feb","eng" to "February","jp" to "フェブラリー","id" to 2)
mapList.add(map)

mapListは

index key eng jp id
0 jan January ジャニュアリー 1
1 Feb February フェブラリー 2

このような中身になっています。
Januaryを取り出そうと思うと
mapList[0].get("eng")と書きます。
フェブラリーを取り出そうと思うと
mapList[1].get("jp")と書きます。

CustomListView.kt
    private var mapList:MutableList<MutableMap<String,Any>> = mutableListOf()
    private var map = mutableMapOf<String,Any>()

定義したクラス内でAddメソッドとDelメソッドを作ります。

CustomListView.kt
    fun Add(msg : String){
        map = mutableMapOf("msg" to msg)
        mapList.add(map)
    }
    fun Del(idx : Int){
        mapList.removeAt(idx)
    }

前編で作ったCustomListViewに組み込むと

CustomListView.kt
class CustomListView(private val context: Activity): BaseAdapter() {

    private var mapList:MutableList<MutableMap<String,Any>> = mutableListOf()
    private var map = mutableMapOf<String,Any>()

    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
        val inflater = context.layoutInflater
        val view1 = inflater.inflate(R.layout.row,null)
        var fMsg = view1.findViewById<TextView>(R.id.tvMsg)     // row.xmlレイアウトのtvMsgを参照
        fMsg.setText(mapList[p0].get("msg").toString())   // tvMsgのtextに 引数 p0を返す
        return view1
    }
    override fun getItem(position: Int): Any {
        return "" // p0 にあるデータを返す※今回は未使用
    }
    override fun getItemId(position: Int): Long {
        return position.toLong() // 識別するためのidを返す
    }
    override fun getCount(): Int {
        return mapList.count()   // 一覧の件数
    }
    fun Add(msg : String){
        map = mutableMapOf("msg" to msg)
        mapList.add(map)
    }
    fun Del(idx : Int){
        mapList.removeAt(idx)
    }
}

これをメインアクティビティに実装してボタンを押されたときの処理も作ります。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    val customAdaptor= CustomListView(this@MainActivity)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Show()
    }
    fun onButtonAdd(view : View){
        val et = findViewById<EditText>(R.id.etValue)
        val msg = et.text.toString()
        customAdaptor.Add(msg)
        Show()
    }
    fun onButtonDel(view : View){
        customAdaptor.Del(0)
        Show()
    }
    fun Show(){
        var listView = findViewById<ListView>(R.id.lvMain)
        listView.adapter = customAdaptor
    }
}

CustomListViewでうまくカプセル化できたのでメインの方のコード量は非常に少なくなりました。

リストに日時を追加

追加ボタンを押されたときの日時を表示して見ましょう。

CustomListView.kt
    fun Add(msg : String){
        val dt = LocalDateTime.now()
        map = mutableMapOf("msg" to msg,"dt" to dt)
        mapList.add(map)
    }

Addメソッドに処理を追加します。
変数 dtに現在日時を代入します。
コレクションとして キー dt にその値を代入します。

メインの方に日時を表示する処理を追加します。

MainActivity.kt
    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
        val inflater = context.layoutInflater
        val view1 = inflater.inflate(R.layout.row,null)
        var fMsg = view1.findViewById<TextView>(R.id.tvMsg)     // row.xmlレイアウトのtvMsgを参照
        var fDt  = view1.findViewById<TextView>(R.id.tvDateTime)
        fMsg.setText(mapList[p0].get("msg").toString())   // tvMsgのtextに 引数 p0を返す
        fDt.setText(mapList[p0].get("dt").toString())   
        return view1
    }

たったこれだけの追加ですが日時が表示されるようになりました。
でも日時の表示が日本風ではありません。

MainAcivity.kt
    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
        val inflater = context.layoutInflater
        val view1 = inflater.inflate(R.layout.row,null)
        var fMsg = view1.findViewById<TextView>(R.id.tvMsg)     // row.xmlレイアウトのtvMsgを参照
        var fDt  = view1.findViewById<TextView>(R.id.tvDateTime)

        val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
        val s = mapList[p0].get("dt").toString()
        val dt = LocalDateTime.parse(s)
        val dts = dt.format(formatter)
        fMsg.setText(mapList[p0].get("msg").toString())   // tvMsgのtextに 引数 p0を返す
        fDt.setText(dts)   // tvMsgのtextに 引数 p0を返す
        return view1
   }

文字列を日時型に変換したりさらにそれを日時の形式を指定した変換になっているので処理が複雑になりましたがこれで理想的な表示になりました。

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