20200712のAndroidに関する記事は4件です。

[Android]Epoxy が StickyHeader に対応しているらしいので試してみた

はじめに

RecyclerView の Adapter の実装の部分を楽にしてくれるライブラリの Epoxy ですが、
どうやら Sticky Header にも対応してくれているらしいです。今回は Epoxy で Sticky Header をどのような感じで利用できるのか紹介したいと思います。

準備する

Sticky Header が利用できるようになったのは 3.10.0 からみたいです。なので次の内容を build.gradle に記述して、3.10.0 以上の Epoxy を使えるようにします。

Image from Gyazo

// kapt 有効化
apply plugin: 'kotlin-kapt' 

android {
      
    // databinding 有効化
    dataBinding {
        enabled = true 
    }
      
}

dependencies {
      
    def epoxy_version = "3.11.0"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:${epoxy_version}"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
      
}

実装する

EpoxyModel を作成する

まずは EpoxyModel を作成していきます。今回は StickyHeader になる HeaderLayout と StickyHeader にならない ContentLayout と分けて作成していきます。

HeaderLayout

Image from Gyazo

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="title"
            type="String" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{title}"
            android:textColor="@android:color/white"
            android:textSize="32sp"
            android:background="@color/colorPrimary"
            tools:text="Sticky Header" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

ContentLayout

Image from Gyazo

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="title"
            type="String" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{title}"
            tools:text="Content" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

package-info.java
そしてこのままだと Epoxy は EpoxyModel を生成してくれません。ですので package-info.java を作成して Epoxy が EpoxyModel を生成してくれるようにしてやります。

@EpoxyDataBindingLayouts({R.layout.content_layout, R.layout.header_layout})
package jp.kaleidot725.sample;
import com.airbnb.epoxy.EpoxyDataBindingLayouts;

EpoxyController を作成する

次に EpoxyController を作成してやります、今回は単純に先程作成した HeaderLayout と ContentLayout を交互に表示する StickyHeaderController というクラスを作成してやります。

data class Content(val uuid: String, val value: String)
data class Header(val uuid: String,val value: String)

class StickyHeaderController : Typed2EpoxyController<List<Header>, List<Content>>() {
    override fun buildModels(headers: List<Header>, contents: List<Content>) {
        headers.forEach { header ->
            headerLayout {
                id(header.uuid)
                title(header.value)
            }

            contents.forEach { content ->
                contentLayout {
                    id(content.uuid)
                    title(content.value)
                }
            }
        }
    }
}

EpoxyRecyclerView をセットアップする

あとは EpoxyRecyclerView を定義してセットアップしてやります。

MainActivity

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.airbnb.epoxy.EpoxyRecyclerView
        android:id="@+id/epoxy_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {

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

        val stickyHeaderController = StickyHeaderController()
        epoxy_recycler_view.adapter = stickyHeaderController.adapter
        stickyHeaderController.setData(createHeaders(10), createContents(100))
    }

    private fun randomUUIDString(): String {
        return UUID.randomUUID().toString()
    }

    private fun createHeaders(max: Long): List<Header> {
        return (0..max).map { count -> Header(randomUUIDString(), "Header $count") }
    }

    private fun createContents(max: Long): List<Content> {
        return (0..max).map { count -> Content(randomUUIDString(), "Content $count") }
    }
}

ここまで実装したものだと通常の Epoxy と同じで StickyHeader の動作になりません。 StickyHeader として使うには次の実装を追加してやる必要があります。

Image from Gyazo

StickyHeader として動作するようにする

StickyHeader として動作させるには次の 2つの実装を追加してやります。

  1. EpoxyRecyclerView の layoutManager として StickyHeaderLinearLayoutManager をセットしてやる
  2. EpoxyRecyclerView の adpter としてセットされる EpoxyController の isStickyHeader を override してやる

EpoxyRecyclerView の layoutManager として StickyHeaderLinearLayoutManager をセットしてやる

StickyHeader を利用するにはまず RecyclerView の layoutManager に StickyHeaderLinearLayoutManager をセットしてやる必要があります。この StickyHeaderLinearLayoutManager が StickyHeader である EpoxyModel を見つけてくれるようになっていて、RecyclerView のスクロール状況に応じて StickyHeader を貼り付けてくれます。

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

        val stickyHeaderController = StickyHeaderController()
        epoxy_recycler_view.adapter = stickyHeaderController.adapter
        epoxy_recycler_view.layoutManager = StickyHeaderLinearLayoutManager(applicationContext)
        stickyHeaderController.setData(createHeaders(10), createContents(100))
    }
}

EpoxyRecyclerView の adpter としてセットされる EpoxyController の isStickyHeader を override してやる

StickyHeaderLinearLayoutManager では EpoxyController の isStickyHeader を利用して StickyHeader を識別するような仕組みになっています。ですので isStickyHeader を override して自身が定義した EpoxyModel を StickyHeader として認識されるようにします。(isStickyHeader の引数として Position が渡されます、なので Position にある EpoxyModel のクラスを取得して、StickyHeaderとして扱いたいクラスであるか判定してやります。)

class StickyHeaderController : Typed2EpoxyController<List<Header>, List<Content>>() {
        
    override fun isStickyHeader(position: Int): Boolean {
        return adapter.getModelAtPosition(position)::class == HeaderLayoutBindingModel_::class
    }
}

この実装を加えると EpoxyModel が StickyHeader として扱われ RecyclerView に StickyHeader が表示されるようになります。

Image from Gyazo

おわりに

という感じで Epoxy でも StickyHeader が使えるようになっているらしいです。使い方をまとめると次のようになりますね。

  • 通常の利用方法と同じで EpoxyModel と EpoxyController を実装してやる
  • RecyclerView の adapter に EpoxyController.Adapter、layoutManager に StickyHeaderLinearLayoutManager をセットしてやる
  • StickyHeaderLinearLayoutManager が StickyHeader である EpoxyModel を識別できるように EpoxyController.isStickyHeader を実装してやる。

参考文献

StickyHeader は現時点(2020/07/12)でまだドキュメントが整備されていないので、詳しく知りたい方は次のリンクを見てみると良いかなと思います。

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

個人的にFlutterで何回も調べがちなことまとめ

個人的な備忘としてまとめておきます
最近弊社で開発してるAndroidアプリがFlutter製ということもあり、
開発する中で何度も調べることがもっと増えてくると思うので逐次更新していきます。

ふたりのみ Android for Flutter(会員向け)
クラフトビール飲み比べ定期配送サービス ふたりのみ とは

環境

  • Flutter 1.17.4
  • Dart 2.8.4

Container の装飾周り

角丸にする

Container(
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(4.0),
  ),
);

枠線を描く

Container(
  decoration: BoxDecoration(
    border: Border.all(color: Colors.blueAccent)
  ),
);

Navigator での画面遷移周り

画面遷移

Flutter 画面遷移 でいつも調べてます
Navigator は iOS と同じならスタック(後のせ先取り)なので push はお皿を積むイメージ

Navigator.push(
  context,
  MaterialPageRoute<Null>(
    settings: RouteSettings(name: "/detail"),
    builder: (BuildContext context) => Container();
  )
);

前の画面に戻る

pop はお皿を取るイメージ
一枚だけ上からとります

Navigator.pop(context);

rootまでpopする

積んだお皿全部退けます

Navigator.popUntil(context, ModalRoute.withName('/'));

TextField の装飾周り

枠線を描く

iOS ちっくなTextFieldにしたいときよく使う

TextField(
  decoration: InputDecoration(
    border: OutlineInputBorder(),
  ),
);

ラベルとヒントをつける

プレースホルダー的なやつを設定したいときあるけどよく忘れる

TextField(
  decoration: InputDecoration(
    labelText: 'ラベル',
    hintText: 'ヒント',
  ),
);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KlakNDIをAndroidに対応させる

Klak.gif

はじめに

NDI®はNetwork Device Interfaceの略でNewTek社が開発したプロトコルです。簡単に言うとLanケーブルやWifiを使ってリアルタイムに映像の伝送ができます。NewTek社はSDKを公開してくれているので、開発者は自由にNDIを利用したソフトウェアを開発できます。

そのNDI SDKがAndroidに対応しました。どのバージョンから対応したのかは未確認ですが現状の最新版であるv4.5.3にはAndroid向けのSDKが存在します。ツイッターとか見てる限り最近のことのようです。個人的にAndroidとWindows/MacのUnity製アプリ間でリアルタイムに映像の送受信をしたかったのでこれを試してみました。

NDI SDKはUE4向けプラグインを公式でサポートして公開していますが、Unityに関しては特にサポートはありません。ですが、keijiroさんがUnity向けのプラグインKlakNDIを公開してくださっています。これを使いたいと思います。しかしKlakNDIに含まれているNDI SDKはv4.1でAndroid向けのSDKは含まれておらず非対応です。そこで自前でNDI SDK の Android向けのネイティブプラグインを組み込んで使えるようにしてみました。この記事はその記録です。

ソースコードはGitHubで公開しています。

https://github.com/tarakoKutibiru/Unity-NDI-Plugin-Android

直面している問題点

AndroidからUnity内のカメラ映像の送信はできましたが、受信はできませんでした。原因については調査中ですが、SDK自体が未対応の可能性が高そうです。

NDI SDK 4.5に添付された公式のドキュメントには、NDI SDKはiOSにおいて映像の送信には対応しているが受信は未対応であることが明言されています。それに対してAndroidには特に映像の受信が未対応であるという明確な記述はありません。しかし公式のフォーラムには

ARM CPUのエンコードには対応しているが、デコードには対応していないため、Android/iOSは映像の送信のみで、受信は対応していない。

という情報がありました。

NDI SDK本体のダウンロード

new_tek.png
NDIの公式サイトでユーザー登録するとリンクが記述されたメールが届くのでそこからイントールします。Android,Windows,MAC,LINUXなどがあるので、必要に応じてインストールしてください。v4.5とv.4.1間で映像の伝送ができるかは未確認なので、とりあえず更新しておいたほうが無難だと思います。

余談ですが本体のSDKにドキュメントやサンプルコードが付属してくるので、このへんを読むとすごく参考になります。特にC#の実装は参考になります。NDILibDotNet2っていうラッパークラス郡があるんですが、これは便利なのでUnityに取り込んでしまっても良いと思います。

Windowsの場合はSDKが

C:\Program Files\NewTek\NDI 4 SDK (Android)\Lib\armeabi-v7a\libndi.so

にインストールされるのでこれを

KlakNDI\Packages\jp.keijiro.klak.ndi\Plugin\Android\libndi.so

のように配置します。

使いたいAndroid端末のABIがarmeabi-v7aじゃない場合は対応するSDKを使って下さい。

Config.csの修正

Config.csを修正してAndroidのSDKを読み込むようにします。

KlakNDI\Packages\jp.keijiro.klak.ndi\Runtime\Interop\Config.cs
namespace Klak.Ndi.Interop
{
    static class Config
    {
  #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
        public const string DllName = "Processing.NDI.Lib.x64";
  #elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
        public const string DllName = "libndi.4";
  #elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX
        public const string DllName = "ndi";
  #elif UNITY_ANDROID
        public const string DllName = "ndi";
  #else
        public const string DllName = "__Internal";
  #endif
    }
}

WifiManagerの作成

公式のドキュメントによるとAndroidでNDIを利用するにはNSDManagerのインスタンスを取得して保持する必要があるそうです。

Because Android handles discovery differently than other NDI platforms, some additional work is needed. The NDI library requires use of the “NsdManager” from Android and, unfortunately, there is no way for a third-party library to do this on its own. As long as an NDI sender, finder, or receiver is instantiated, an instance of the NsdManager will need to exist to ensure that Android’s Network Service Discovery Manager is running and available to NDI.
This is normally done by adding the following code to the beginning of your long running activities:
At some point before creating an NDI sender, finder, or receiver, instantiate the NsdManager:
You will also need to ensure that your application has configured to have the correct privileges required for this functionality to operate.

DeepLを使った翻訳文は以下です。

Androidは他のNDIプラットフォームとは異なるディスカバリーを扱うため、いくつかの追加作業が必要になります。 NDI ライブラリは Android の「NsdManager」を使用する必要があり、残念ながら、サードパーティのライブラリが独自にこれを行う方法はありません。 NDI の送信者、検出者、または受信者がインスタンス化されている限り、Android の  Network Service Discovery Manager  が実行され、NDI で利用できるようにするために、NsdManager のインスタンスが存在する必要があります。
これは通常、以下のコードを長時間実行しているアクティビティの先頭に追加することで行います。
NDI のsender、finder、またはreceiverを作成する前のある時点で、NsdManager のインスタンスを作成します。
また、アプリケーションがこの機能を動作させるために必要な正しい権限を持つように構成されていることを確認する必要があります。

ということなのでNsdManagerを取得して保持するWifiManagerクラスを適当な場所に作成します。

WifiManager.cs
using UnityEngine;
using System.Collections.Generic;

public class WifiManager
{
#if UNITY_ANDROID && !UNITY_EDITOR
    AndroidJavaObject nsdManager = null;
#endif

    private static WifiManager instance = new WifiManager();

    public static WifiManager GetInstance()
    {
        return instance;
    }

    private WifiManager() { }

    public void SetupNetwork()
    {
        // The NDI SDK for Android uses NsdManager to search for NDI video sources on the local network.
        // So we need to create and maintain an instance of NSDManager before performing Find, Send and Recv operations.
    #if UNITY_ANDROID && !UNITY_EDITOR
        using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
        {
            using (AndroidJavaObject context = activity.Call<AndroidJavaObject>("getApplicationContext"))
            {
                using (AndroidJavaObject nsdManager = context.Call<AndroidJavaObject>("getSystemService", "servicediscovery"))
                {
                    this.nsdManager = nsdManager;
                }
            }
        }
    #endif
    }
}

公式のドキュメントには適切な権限を取得しておく必要があるとのことでしたが、特にAndroidManifest.xmlにPermissionの追加をしなくても動作しました。おそらくUnityが勝手に解決してくれているのだと思います。

あとは適切なタイミングでWifiManager.GetInstance().SetupNetwork();を呼べばOKです。

例えばKlakNDIのSourceSelector.csのStartで呼べばOKです。

SourceSelector.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Linq;
using Klak.Ndi;

public class SourceSelector : MonoBehaviour
{
    [SerializeField] Dropdown _dropdown = null;

    NdiReceiver _receiver;
    List<string> _sourceNames;
    bool _disableCallback;

    // HACK: Assuming that the dropdown has more than
    // three child objects only while it's opened.
    bool IsOpened => _dropdown.transform.childCount > 3;

    void Start()
    {
        WifiManager.GetInstance().SetupNetwork();
        _receiver = GetComponent<NdiReceiver>();
    }

    void Update()
    {
        // Do nothing if the menu is opened.
        if (IsOpened) return;

        // NDI source name retrieval
        _sourceNames = NdiFinder.sourceNames.ToList();

        // Currect selection
        var index = _sourceNames.IndexOf(_receiver.ndiName);

        // Append the current name to the list if it's not found.
        if (index < 0)
        {
            index = _sourceNames.Count;
            _sourceNames.Add(_receiver.ndiName);
        }

        // Disable the callback while updating the menu options.
        _disableCallback = true;

        // Menu option update
        _dropdown.ClearOptions();
        _dropdown.AddOptions(_sourceNames);
        _dropdown.value = index;
        _dropdown.RefreshShownValue();

        // Resume the callback.
        _disableCallback = false;
    }

    public void OnChangeValue(int value)
    {
        if (_disableCallback) return;
        _receiver.ndiName = _sourceNames[value];
    }
}

PlayerSettingの変更

次にUnityの設定を変更していきます。

ビルド設定をAndroidに変更します。
するとエラーがでるのでPlayerSettingsを変更します。

build_setting.png

KlakNDIではUnityのカメラが描画している情報を取得するのにAsyncGPUReadbackを使っていますが、これはOpenGLでは動かないのでGraphics APIsをVulkanに変更します。
build_setting.png

未確認ですがAsyncGPUReadbackをOpenGLでも使えるようにするプラグインが存在するようです。
https://github.com/Alabate/AsyncGPUReadbackPlugin

結果

以上でKlakNDIをAndroidで利用できるようになったはずです:tada:
Klak.gif

このGifはAndroid->Macですが、Android->Win10も動作確認できました。

使っているスマホはMotoG8という実売25000円くらいのローエンド端末です。あまり安定性はなくカクついています。映像の品質は端末やWifiルータの性能にも依存すると思うので、実用するなら機種の選定は必要な気がします。AndroidからPC2台に送信する実験もしてみましたが、さらにカクつくようになりました。

自分がやりたかったことは、PC上のUnityカメラの映像をAndroidに伝送して表示することだったので、今回紹介したNDI SDKでは実現できませんでした...ローカルネットワークでやりとりできれば十分だったので、NDIが使えると良かったんですけどね...WebRTCを勉強する必要があるかもですね...

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

Androidアプリから自然言語処理(DialogFlow)

はじめに

以前、AndroidアプリからDialogFlowを利用して自然言語処理機能を実装しました。
DialogFlowを実装するためのSDKがAndroid用にはなく、結構嵌ったのでメモとして投稿しました。
少しでも参考になれば幸いです。

全体構成

AndroidアプリからDialogFlowを呼び出すのために、Cloud functionを利用しています。
Cloud functionを挟むことにより、Androidアプリ+DialogFlowとの認証を簡単にしています。

dialogflow.png

事前準備

  • AndroidアプリからCloud functions for Firebaseを利用可能にしておく
  • DialogFlowを利用可能にしておく
  • DialogFlowのサービスアカウントを発行する

CloudFunctions

以下、紹介する実装のファイル構成です。

 ├── functions
     ├── index.js              // cloudFunctions実装(アプリから呼ぶ関数定義)
     ├── dialogflow-v2.js      // cloudFunctions実装(DialogFlow呼び出し)
     ├── serviceaccount.json  // サービスアカウント

CloudFunctions実装(index.js)

アプリから呼ぶ関数の定義です。

index.js
const dialogflowv2Module = require('./dialogflow-v2');

exports.callDetectIntent = functions.https.onCall((data, context) => {
  let input = data.input;
  let result = dialogflowv2Module.detectIntentQueryText(input)
      .then(r => {
        return JSON.stringify(r);
      })
      .catch(err => {
        throw new functions.https.HttpsError('callDetectIntent',err);
      });
  return result;
});

CloudFunctions実装(dialogflow-v2.js)

サービスアカウントを使って、DialogFlowのAPIの呼び出しています。

dialogflow-v2.js
"use strict";

const dialogflow = require('dialogflow');
const serviceAccount = require('./serviceaccount.json');

exports.detectIntentQueryText = async function(query) {
    return await detectIntent(query);
}

async function detectIntent(query) {

    // Define config
    const languageCode = "ja";
    const sessionId = "00000000";

    // Instantiate a DialogFlow client.
    const sessionClient = new dialogflow.SessionsClient({
        credentials: {
            private_key: serviceAccount.private_key,
            client_email: serviceAccount.client_email
        }
    });

    // Define session path
    const sessionPath = sessionClient.sessionPath(serviceAccount.project_id, sessionId);

    // The text query request.
    const request = {
        session: sessionPath,
        queryInput: {
            text: {
                text: query,
                languageCode: languageCode,
            },
        },
    };

    // Send request and log result
    const result = await sessionClient
        .detectIntent(request)
        .then(responses => {
            return responses[0];
        })
        .catch(err => {
            console.log(err);
            throw new functions.https.HttpsError('detectIntentSync',err);
        });

    return result;
};

Androidアプリ実装

Androidアプリ実装です。Cloud Functionを呼び出しています。

DialogFlow.kt
    private lateinit var functions: FirebaseFunctions

    private fun callDetectIntent(text: String): Task<String> {
        val data = hashMapOf(
            "input" to text
        )

        return functions
            .getHttpsCallable("callDetectIntent")
            .call(data)
            .continueWith { task ->
                val result = task.result?.data as String
                result
            }
    }

参考文献

https://github.com/firebase/quickstart-android/blob/8262596a009b50b6c99191001f7e5470391bcc4a/functions/app/src/main/java/com/google/samples/quickstart/functions/kotlin/MainActivity.kt#L78-L95

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