20200220のAndroidに関する記事は9件です。

flutterのInkwellでリップルエフェクトを実現しようとして詰まった

Containerに色を付けたら、内部のInkwellの波紋表現が表示されなくなった

flutterの標準的なWidgetである"Container"。これのBoxDecorationを使えば、簡単にContainerの色や形を変えることが出来る。

Container(
   decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(10)
        ),
   child: Text("テスト文字列"),
)

Screenshot_1582200738 (2).png

InkWellはマテリアルコンポーネントの一つで、触ると波紋状のエフェクトが出る今風のデザインだ。

InkWell(
   child: Text("テスト文字列"),
   onTap:(){},
)

Screenshot_1582201236 (2).png

単体ではうまく機能したが、いざContainerにInkWellをぶち込んで見ると、なぜかRippleEffectが表示されない

Container(
   decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(10)
        ),
   child: InkWell(
          child: Text("テスト文字列"),
          onTap: (){},
   ),
)

どうやらリップルエフェクトを合成するとき、なぜか外側のContainerの色が優先されるらしい。

解決法

その1

BoxDecorationのBackgroundBlendModeを変更してみる

Container(
   decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(10),
          backgroundBlendMode: BlendMode.color,
        ),
   child: InkWell(
          child: Text("テスト文字列"),
          onTap: (){},
   ),
)

これはうまく行かない。一応波紋は出るが、さらに外側の背景の色も混ざってコンテナの色がおかしくなる。
Screenshot_1582200767 (2).png

Container()をInk()に変える

Ink(
   decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(10),
        ),
   child: InkWell(
          child: Text("テスト文字列"),
          onTap: (){},
   ),
)

これでうまくいった。
Screenshot_1582200936 (2).png

その他

backgroundBlendModeはたくさん設定があるのでそれを使えば普通にうまく行った可能性はある。
でもめんどくさかったので……

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

Androidの画面分割と折りたたみでアクティビティの再生性が起こる

余談

(お急ぎの方はまとめまで飛ばしてください。)
目を疑った。
今、目の前で、android:screenOrientation を指定して画面回転を抑制しているActivityの画面回転が起きている。私は絶望に打ちひしがれながら、画面を回転させ続けた。

そもそもなぜこんな事になったのか?

ことの始まりは、targetSdkVersionを29にあげる上での調査だった。
https://developer.android.com/about/versions/10/behavior-changes-10?hl=ja
このドキュメントの中に 折りたたみ式のサポートの項目がある。項目の中で 画面分割についても触れられていた。
画面分割の機能は知っていたものの、個人端末がiPhoneなためほとんど利用していなかった。だからこれを期に、折りたたみと画面分割時の挙動を見てみることにしたのだ。その後に何が待ち受けているかも知らず・・・・

画面分割はAndroid7以降の端末であればどれでも利用できるため、すでにインストール済みのエミュレータで良かった。だが折りたたみについては、Foldableのエミュレータをインストールする必要がある。(Android Studio 3.5であれば利用可能)

インストールが終わり、私はエミュの画面をパキパキさせ始めた。
うん、実に可愛らしい挙動だ。折りたたみの度に伸びたり縮んだりするさまは見ていてとても癒やされる。
Screenshot_1582160923.png

パキパキしながら私はあることに気づいた。
アクティビティが再生成されている?

私は頭を抱えた。というのも、最近参画したプロジェクトのアプリは画面縦固定で、アクティビティの再生性を考えていない画面があってもおかしくない状況だったからだ。

ひとまず冷静になろう。私は自分に言い聞かせ、画面を分割した。

なんだと・・・?
今、アクティビティが再生成されなかったか?画面を分割しただけで!?
私は思わずonCreateの直後にブレークポイントを貼った。そしてすぐさまデバッグをかける。ビルドの時間が待ち遠しい。

ようやくビルドが終わり、画面を分割する。
やはり再生性されている。そして私はおもむろに、分割された画面をリサイズした。
半ば予想通りではあったが、ここでもアクティビティが再生成された。そこで私の頭の中に、ろくでもない考えが浮かんだ。
この分割された画面を横にしたら、片方だけが回転するのか?
考えを浮かべながら、私はゆっくりと画面を横に向ける。

目を疑った。
今、目の前で、android:screenOrientation を指定して画面回転を抑制しているActivityの画面回転が起きている。私は絶望に打ちひしがれながら、画面を回転させ続けた。

まとめ

下記の状況ではアプリの設定関係なくアクティビティの再生性が起こります。
1. 画面分割時
2. 画面分割リサイズ時
3. 画面分割中の画面回転時(正方形の画面を除く)
4. 折りたたみ端末の折りたたみ時

Android10で公式の折りたたみサポートが始まりました。これからこれらの状況が増えていくことを考えると、未対策アプリは今のうちにどうするか考えておくのが良さそうです。

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

無線でiPhoneの画面をテレビに表示させる方法

無線でiPhoneの画面をテレビに表示させるには?

Androidスマホなら、その画面をスマートテレビに映すのは簡単でしょう。
「iPhoneの画面をテレビに映す」と聞いたら、LightningケーブルやHDMIアダプターを利用するという方法が頭に浮かぶでしょう。
しかし、ケーブル、アダプターいっぱいでは面倒でしょう。
この記事は無線でiPhoneをテレビにミラーリングする方法についてご紹介します。

事前準備

iPhone
画面ミラーリングアプリ(ここでLetsViewという無料アプリを例として)
Adroidテレビ

操作手順

  1. まずはLetsViewアプリをiPhoneとテレビにダウンロードして、インストールします。
    QzpcVXNlcnNccml4aWxcQXBwRGF0YVxSb2FtaW5nXERpbmdUYWxrXDYxNDM4ODUwOF92MlxJbWFnZUZpbGVzXDE1ODIxODQ1NTgwNTRfNDgxRjM0QTgtMTNEMS00NDYzLTk3ODctMjQxOUJCNEM3OUJDLnBuZw==.png

  2. iPhoneとテレビを同じWi-Fiネットワークに接続します。

  3. 両方ともアプリ起動します。

  4. iPhoneのコントロールセンターを開いて、「画面ミラーリング」>「LetsView+テレビの名前」を選択して、ミラーリングを開始します。
    QzpcVXNlcnNccml4aWxcQXBwRGF0YVxSb2FtaW5nXERpbmdUYWxrXDYxNDM4ODUwOF92MlxJbWFnZUZpbGVzXDE1ODIxODQ2ODk5NDJfMTk4ODZEMkMtNDNFMC00YzNlLUEwQzctODMxMUIwOTIwODlELnBuZw==.png

  5. iPhoneの画面はテレビに出力されます。

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

Push認証とNotificationサービス(1)

はじめに

スマートフォンへのPush通知は普段から皆さんが接する機会が多いと思います。
また、Push通知のためのクラウドサービスも昨今充実してきたと感じます。
今回はそのクラウドサービスとPush通知の仕組みを利用して、Push認証を構成する仕組みの理解や手順の把握を目的とした記事を掲載しようと思います。
Push認証を実現するソフトウェアとしてOpenAMを利用します。
クラウドサービスについては、AWSのSimple Notification Service(SNS)、GoogleのFirebase Cloud Messaging(FCM)を利用します。
今回はAndroidのスマートフォンで実演します。iOSについてものちほど掲載する予定です。 
第1回としてPush認証の動作を紹介します。

(1)デバイス登録

とにもかくにも動きをみてましょう。
はじめにOpenAMへユーザーのデバイス登録をおこないます。

flow1.png

ブラウザ上でOpenAMに認証した後にバーコードが表示され、それをスマートフォンのAuthenticatorアプリがカメラ機能で読み取ります。アプリが読み取ったOpenAMのAPI情報やチェレンジコード、共有鍵から、生成したデバイストークンをOpenAMに送信します。OpenAMはAWS SNS上に該当デバイスのエンドポイントを登録します。エンドポイントの登録が成功すればOpenAMが利用するデータストアにも情報を登録します。

実際の画面のスナップショットです。
※開発中の画面です。

①デバイストークン生成
こちらはAuthenticatorアプリをインストールした際に生成しています。

②ID/PWD認証
PC上のブラウザです。
pushreg1.png

③バーコード表示
pushreg4.png

④バーコード取込
スマートフォン上の画面です。
pushreg6.png
pushreg7.png

⑤デバイス登録
PC上のブラウザに登録完了された画面が表示されます。
※この画面が表示されたときには既に⑥、⑦も完了しています。
pushreg8.png

⑥エンドポイント登録
AWS SNS上のFCMのプラットフォームにデバイスのエンドポントが表示されます。
awssns5.png

⑦Push情報登録
OpenAMのダッシュボードには登録されたPush Deviceが表示れます。
pushreg10.png

(2)Push認証

デバイス登録が出来たら実際に認証してみます。

flow2.png

PC上のブラウザで認証が必要なアプリケーションのページにアクセスすると、OpenAMがID入力画面を表示します。
IDを入力してログインすると、Push認証の待受け画面を表示します。
OpenAMからAWS SNS、Google FCM経由でスマートフォンのAuthenticatorアプリに通知が飛びます。
スマートフォン上で認証を許可すると、PC上のブラウザでは待受け画面からアプリケーションのページに遷移します。

①ID入力
pushauth1.png

②Push情報参照
これはOpenAMが先ほどのデータストアに登録された情報をIDを元に取りだします。

③Push認証待受け表示
pushauth2.png

④トピック通知
OpenAMからAWS SNSのトピックというNotificationのポイントに送られます。

⑤プラットフォーム通知
AWS SNSからGoogle FCMのプラットフォームに通知がおこなわれます。

⑥デバイス通知
pushauth3.png

⑦認証応答
AcceptかRejectで応答します。
pushauth6.png

⑧認証完了
待受け画面が解除されてサンプルアプリケーションの画面が表示されています。
pushauth9.png

おわりに

まずはPush認証がどういうものか理解してもらえたでしょうか。
ワンタイムパスワードの認証と似ているかと思いますが、違いとしてはログイン画面でのパスワードの入力が不要となります。
それからスマートフォンでの認証応答でボタンを押すだけなので、わざわざOTPコードを入力する必要がありません。
また、スマートフォン上のNotificationをタップすればアプリが自動的に表示されるためUXも向上します。
ただし、Push認証の場合は通知をするためのクラウドサービスの準備が必要となります。
次回では、OpenAMやAWS SNS、Google FCMの設定方法について紹介したいと思います。

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

【Android】GoogleマップアプリのようなBottomSheet(画面下部のメニュー)を作る

概要

Androidアプリで、Google MapのアプリのようなBottomSheetを作ってみました。

google_map_collapsed.png google_map_expanded.png
Googleの「マップ」アプリのBottomSheet

bottom_sheet_collapsed.png bottom_sheet_expanded.png
作成したBottomSheet

主に以下のような特徴を実現しました。

  • BottomSheet領域をスワイプ、またはBottomSheet上部のバー領域をタップすることで展開・収納できる
  • FAB(Floating Action Button)がBottomSheetに追従して移動する

Google MapのBottomSheetは、収納時下にスワイプすると隠れる・展開時上にスワイプすると全画面になる、などの挙動もありますが今回はやっていません。

BottomSheetとは

マテリアルデザインのガイドラインでは、BottomSheetについて以下のような説明がされています。

Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen.

BottomSheetを作成する

BottomSheetを作成します。

画面レイアウト

fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#BED6FF"
    tools:context=".HomeFragment">

    <include
        layout="@layout/bottom_sheet"
        android:id="@+id/bottomSheet" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

CoordinatorLayoutを使ってBottomSheetの挙動を実現します。

BottomSheet

bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    android:background="@drawable/bottom_sheet_background"
    android:elevation="30dp"
    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
    app:behavior_peekHeight="60dp">

    <LinearLayout
        android:id="@+id/bottomSheetBarArea"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

       <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:gravity="center"
            android:background="#00000000">

            <View
                android:id="@+id/bottomSheetBar"
                android:layout_width="40dp"
                android:layout_height="6dp"
                android:background="@drawable/bottom_sheet_bar" />

       </LinearLayout>

    </LinearLayout>

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="BottomSheet"
        android:textColor="@android:color/black"
        android:textSize="18sp" />

</LinearLayout>

drawable
bottom_sheet_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="0dp"
        android:topLeftRadius="6dp"
        android:topRightRadius="6dp" />
    <solid android:color="@android:color/white"/>
</shape>
bottom_sheet_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#808080"/>
    <corners android:radius="5dp"/>
</shape>

BottomSheetのレイアウトはLinearLayoutで作成しています。中身は好きなように要素を配置してください。

  • layout_height
    今回は展開時のBottomSheetの高さを指定しています。
  • background
    上部のみ角丸の背景(bottom_sheet_background)を設定しています。
  • layout_behavior
    CoordinatorLayoutの子要素にBottomSheetBehaviorを適用することで、その要素をBottomSheetのように動かせます。これだけで、スワイプで展開・収納できます。
  • behavior_peekHeight
    収納時の高さを指定できます。setPeekHeight(peekHeight)でスクリプトでも設定できます。

今回は設定していませんが、app:behavior_hideable="true"でBottomSheetを隠せるようにできます。
bottom_sheet_hideable.png

上部のバー領域

タップすると展開・収納できる領域を作成します。バー部分だけではタップできる領域が狭すぎるため、バー以外の上部をタップした時にもイベントが発生するように、透明なバー領域を作成しています。

bottom_sheet_bar_area.png
黒い領域がタップ領域です。

スクリプト

今回は、バー領域をタップした時にもBottomSheetを展開・収納できるようにClickListenerを追加しました。

HomeFragment.kt
class HomeFragment : Fragment() {
  private lateinit var bottomSheet: View
  private lateinit var bottomSheetBarArea: View
  private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
  override fun onCreateView(inflater: LayoutInflater,
                          container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)

    val view = inflater.inflate(R.layout.fragment_home, container, false)
    bottomSheet = view.findViewById(R.id.bottomSheet)
    bottomSheetBarArea = view.findViewById(R.id.bottomSheetBarArea)
    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
    setup()
    return view
  }

  private fun setup() {
    bottomSheetBarArea.setOnClickListener {
      if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
      } else if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED)
      }
    }
  }
}

BottomSheetBehavior.from(view: V)でBottomSheetBehaviorを取得します。
そして、bottomSheetBarAreaをタップした時の処理を、BottomSheetBehaviorの状態によって分岐しています。

  • 収納されている時(STATE_COLLAPSED)には展開(STATE_EXPANDED)する
  • 展開されている時には収納する

これで、スワイプだけでなくタップでも展開・収納できます。

別の要素を追従させる

BottomSheetの動きに合わせてFAB(Floating Action Button)を上下させます。今回は2つのFABをLinearLayoutで囲って一緒に動かしています。
bottom_sheet_fab.png

fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#BED6FF"
    tools:context=".HomeFragment">

    <include
        layout="@layout/bottom_sheet"
        android:id="@+id/bottomSheet" />

    <LinearLayout
        android:id="@+id/fabContainer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_gravity="top"
        app:layout_anchor="@id/bottomSheet"
        app:layout_anchorGravity="top|end">

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
            app:backgroundTint="#FF0000" />

    </LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • layout_gravity
    topを指定してBottomSheetと被らないようにしています。
  • layout_anchor
    アンカーを指定します。今回はBottomSheetに追従させたいのでBottomSheet要素を指定しています。
  • layout_anchorGravity
    アンカーに対するgravityを設定します。今回はBottomSheetの右上に配置したいのでtop|endを指定しています。

まとめ

  • CoordinatorLayoutBottomSheetBehaviorでBottomSheetを作成しました。
  • アンカーを指定して別の要素をBottomSheetに追従するようにしました。

参考

https://qiita.com/napplecomputer/items/5b3d1225533a59488ac3

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

TextViewのdrawableにsizeを設定してみる

class TextViewWithDrawable @JvmOverloads constructor(
    context: Context, 
    attributeSet: AttributeSet? = null, 
    defStyleInt: Int = 0
) : AppCompatTextView(context, attributeSet, defStyleInt) {
    companion object {
        @JvmStatic
        @BindingAdapter(value = ["drawableStart", "drawableEnd", "drawableTop", "drawableBottom"], requireAll = false)
        fun TextViewWithDrawable.setDrawable(drawableStart: Drawable? = null, drawableEnd: Drawable? = null, drawableTop: Drawable? = null, drawableBottom: Drawable? = null) {
            val start = drawableStart?.setTextSizeBounds(this)
            val end = drawableEnd?.setTextSizeBounds(this)
            val top = drawableTop?.setTextSizeBounds(this)
            val bottom = drawableBottom?.setTextSizeBounds(this)
            setCompoundDrawables(start, top, end, bottom)
        }

        private fun Drawable.setTextSizeBounds(textView: TextView): Drawable = apply { setBounds(0, 0, textView.textSize.toInt(), textView.textSize.toInt()) }
    }

TextViewのCustomViewを作成して、BindingAdapterを作ってあげます。
textViewのtext sizeに応じてdrawableのsizeを調整するようにしています。

使う側では、databindingなので、

<TextViewWithDrawable
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="4dp"
                android:layout_marginEnd="16dp"
                android:drawablePadding="2dp"
                android:ellipsize="end"
                android:lines="1"
                android:text="あああ"
                android:textColor="#444444"
                android:textSize="11sp"
                app:drawableStart="@{@drawable/icon}"/>

と、指定します。({}で囲うのを忘れない。)

ただし、CustomViewを作るとき、@JvmOverloadsを使うとThemeが反映されない場合があるみたいなので注意です。(今回は@JvmOverloadsを使用していますが)
https://medium.com/@mmlodawski/https-medium-com-mmlodawski-do-not-always-trust-jvmoverloads-5251f1ad2cfe

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

Camera2を使ってみた話、そして"なんちゃってサーモグラフィー"を作った話

先日こんな記事を見つけました。
screencapture-imagingsolution-blog-fc2-blog-entry-171-html-2020-02-20-06_55_40.png
よいですね。エモいです。これがスマホでできたらきっと楽しいです。android端末しか持ってないのでAndroid Studioでゴリゴリコードを書いていきましょう。

完成品はここです。

作ってみよう

まずレイアウトから決めます。といってもこれだけです。

main_activity.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"
    android:orientation="vertical">

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

画面いっぱいにTextureViewが1個だけです。シンプルですね。このTextureViewにサーモグラフィー(偽)を表示するわけです。

次にmanifestの方にちょっとだけ書き足します。カメラを使うためと、アプリを横向きで固定するためです。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.thermo">

    <uses-permission android:name="android.permission.CAMERA" /> // こうするとカメラが使える

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:screenOrientation="landscape"> // 横向きで固定
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

いよいよActivityのコードを書いていきます。Kotlinの出番です。
まずはMainActivityのプロパティの皆さんです。

MainActivity.kt
private val cameraManager by lazy {
    getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
private val cameraId by lazy {
    // たいていの機種では0番がアウトカメラのはず
    cameraManager.cameraIdList[0]
}
private val previewSize by lazy {
    cameraManager
        .getCameraCharacteristics(cameraId)
        .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
        .getOutputSizes(ImageReader::class.java)[19] // ここの番号はテキトー
}
private lateinit var drawingSurface: Surface

cameraManagerとcameraIdは見ての通りなのですが、previewSizeについて少し補足します。
カメラからの画像はImageReaderというクラス経由で手に入るのですが、このImageReaderがサポートしている解像度は決まっています。その解像度が入ったArrayが上記のgetOutputSizesで手に入るわけです。

で、この解像度をアプリ起動後に変更する仕組みも一応作ったのですが、話がややこしくなるので今回は解像度は決め打ちしちゃいます。

drawingSurfaceについては後で出てくるのでその時に説明します。お次はonCreateです。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (ContextCompat.checkSelfPermission(this@MainActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
        setContentView(R.layout.activity_main)
        findViewById<TextureView>(R.id.textureView).surfaceTextureListener = surfaceTextureListener
        cameraManager.openCamera(cameraId, stateCallback, null)
    } else {
        requestPermissions(arrayOf(CAMERA), REQUEST_CAMERA_PERMISSION)
    }
}

こんな感じの処理ですね。

  • カメラの使用権限がある
    • textureViewにリスナをセット、カメラをオープン
  • 権限がない
    • 権限をもらいに行く

とりあえず権限がなかった場合から見ていきましょう。requestPermissions関数でカメラの権限を付与していいかユーザーに尋ねます。こんな感じのやつが出てきます。
Screenshot_20200220-120157.png
ここでのユーザーの選択はonRequestPermissionsResultをオーバーライドすると取得できます。

MainActivity.kt
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    when (requestCode) {
        REQUEST_CAMERA_PERMISSION -> {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                setContentView(R.layout.activity_main)
                findViewById<TextureView>(R.id.textureView).surfaceTextureListener = surfaceTextureListener
                cameraManager.openCamera(cameraId, stateCallback, null)
            } else {
                finishAndRemoveTask()
            }
        }
    }
}

こんな感じですね。

  • 権限がもらえた
    • textureViewにリスナをセット、カメラをオープン
  • もらえなかった
    • アプリを終了

というわけで、権限をもらえた場合はさっきの分岐と合流することになります。そんなわけでまずはtextureViewのリスナについて見ていきましょう。

MainActivity
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
    override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
        surface ?: return
        drawingSurface = Surface(surface)
        configureSurfaceTexture(surface, width, height)
    }
    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
        surface ?: return
        configureSurfaceTexture(surface, width, height)
    }
    override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {}
    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
        return false
    }
    fun configureSurfaceTexture(surface: SurfaceTexture, width: Int, height: Int) {
        val magnitude = (previewSize.width / width.toDouble()).coerceAtLeast(previewSize.height / height.toDouble())
        surface.setDefaultBufferSize((width * magnitude).toInt(), (height * magnitude).toInt())
    }
}

textureViewが使用可能になるとまずonSurfaceTextureAvailableが呼び出されます。ここでさっきのdrawingSurfaceを初期化しています。このdrawingSurfaceにいろいろ(後述)するとそれが画面に反映されるという寸法です。
その次にconfigureSurfaceTextureという関数を呼んでいます。たった2行の短い関数ですね。この関数でsurfaceTextureの大きさを調整しています。デフォルトの状態では大きすぎ、描画に時間がかかるからです。

次にカメラをオープンするところを見ていきましょう。

MainActivity.kt
cameraManager.openCamera(cameraId, stateCallback, null)

このようにするとカメラが起動し、stateCallbackが実行されます。stateCallbackの中身はこんな感じです。

MainActivity
private val stateCallback = object : CameraDevice.StateCallback() {
    override fun onOpened(camera: CameraDevice) {
        val drawingThread = HandlerThread("CameraBackground").also { it.start() }
        val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.YUV_420_888, 2)
        imageReader.setOnImageAvailableListener(imageAvailableListener, Handler(drawingThread.looper))
        val previewRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
        previewRequestBuilder.addTarget(imageReader.surface)
        camera.createCaptureSession(
            SessionConfiguration(
                SessionConfiguration.SESSION_REGULAR,
                listOf(OutputConfiguration(imageReader.surface)),
                applicationContext.mainExecutor,
                object : CameraCaptureSession.StateCallback() {
                    override fun onConfigured(session: CameraCaptureSession) {
                        session.setRepeatingRequest(previewRequestBuilder.build(), null, null)
                    }
                    override fun onConfigureFailed(session: CameraCaptureSession) {}
                }
            )
        )
    }
    override fun onDisconnected(camera: CameraDevice) {
        camera.close()
    }
    override fun onError(camera: CameraDevice, error: Int) {
        camera.close()
        finish()
    }
}

カメラが起動するとonOpenedが実行されます。いろいろごちゃごちゃと書いてありますが、1行目は特に重要です。ここでスレッドを一つ立ち上げています。UIスレッドで画像処理までやるとカックカクになるので、画像処理の部分は専用のスレッドを与えるわけです。

次に先述のImageReaderを作っています。previewSizeを渡しているのがわかりますね。
さらに画像のフォーマットとしてYUV_420_888を指定しています。今回は画像の輝度がわかると便利なので。
次にImageReaderにリスナを設定しています。カメラからImageReaderに画像が流れてくるとリスナが起動するわけですね。
それ以降のコード(previewRequestBuilderがどうのこうのってやつ)はよくわかってないので省略します
ImageReaderのリスナを見ていきましょう。

MainActivity.kt
private val imageAvailableListener = object : ImageReader.OnImageAvailableListener {
    override fun onImageAvailable(reader: ImageReader?) {
        reader ?: return
        val image = reader.acquireNextImage() ?: return
        val yPlane = image.planes[0]
        displayThermo(image.width, image.height, yPlane.buffer, yPlane.rowStride, drawingSurface)
        image.close()
    }
}

はい、ここ今回の最重要箇所です。displayThermoという謎の関数にいろいろ渡していますね。ここにさっきのdrawingSurfaceも渡しています。さらにyPlaneというものも渡していますが、こいつはimageReaderが読み取った画像の輝度情報が入ったバッファです。
さてこのdisplayThermoはその名の通りサーモグラフィーっぽく表示をする関数なのですが、このアプリは毎フレームごとに全ピクセルを走査するので、この関数をKotlinで実装していたら到底間に合いません。速さが全然足りない。そこでここはネイティブコードを利用します。C++の出番だ!!!!!!!!

naitive-lib.cpp
#include<jni.h>
#include<android/native_window.h>
#include<android/native_window_jni.h>

extern "C" JNIEXPORT void JNICALL
Java_com_example_thermo_MainActivity_displayThermo(
        JNIEnv *env, jobject,
        jint image_width, jint image_height,
        jbyteArray y_plane_buffer, jint row_stride, jobject drawing_surface)
{
    ANativeWindow *window = ANativeWindow_fromSurface(env, drawing_surface);
    int surface_width = ANativeWindow_getWidth(window);
    int surface_height = ANativeWindow_getHeight(window);
    ANativeWindow_acquire(window);
    ANativeWindow_setBuffersGeometry(window, 0, 0, WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer window_buffer;
    if (!ANativeWindow_lock(window, &window_buffer, NULL)) {
        int padding_x = 0, padding_y = 0;
        if (image_width * surface_height < image_height * surface_width)
            padding_x = (surface_width - image_width) / 2;
        else
            padding_y = (surface_height - image_height) / 2;
        uint8_t *src_ptr = reinterpret_cast<uint8_t *>(env->GetDirectBufferAddress(y_plane_buffer));
        uint8_t *dst_ptr = reinterpret_cast<uint8_t *>(window_buffer.bits);
        for (int y = 0; y < image_height; ++y) {
            for (int x = 0; x < image_width; ++x) {
                uint8_t luminance = *(src_ptr + y * row_stride + x);
                uint8_t *pixel = dst_ptr + ((y + padding_y) * window_buffer.stride + x + padding_x) * 4;
                pixel[0] = static_cast<uint8_t>(luminance < 128 ? 0 : luminance >= 192 ? 255 : (luminance - 128) * 4);
                pixel[1] = static_cast<uint8_t>(luminance < 64 ? luminance * 4 : luminance >= 192 ? 255 - (luminance - 192) * 4 : 255);
                pixel[2] = static_cast<uint8_t>(luminance < 64 ? 255 : luminance >= 128 ? 0 : 255 - (luminance - 64) * 4);
                pixel[3] = 255;
            }
        }
        ANativeWindow_unlockAndPost(window);
    }
    ANativeWindow_release(window);
}

この辺もよくわかってないのでざっくり説明すると、こんな風にいろいろごちゃごちゃやるとwindow_bufferというものが手に入ります。こいつはANativeWindow_Bufferという構造体のインスタンスなのですが、こいつのbitsメンバがdrawing_surfaceへの生ポインタになります。
あとはうまいことパディングを計算してポインタをあれこれした後ANativeWindow_unlockAndPostを呼ぶと画面に表示されます。めでたしめでたし。
Screenshot_20200220-125030.png

おわりに

このプログラムを作り上げた代償に私の腰がお亡くなりになりました。みなさん椅子の上に体育座りとかやめた方がいいです。ちゃんと座りましょう。
chair_boy.png

~完~

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

iOSとAndroidのダークモード検証を楽にしよう

ダークモードの検証忘れて文字が見えない!

はい、これ良くやらかしてませんか?
最近 iOSもAndroidもダークモードが追加され、デフォルトからを背景画像や文字色に使用すると、OSのダークモード設定に合わせて自動でカラーの反転がされますよね。それで、背景は白固定にしてるのに、文字色だけデフォルトにしていて文字が見えない!ということが起きます。

開発時・テスト時のダークモード検証を楽にしよう

ダークモードのテストするとき、毎度設定画面を開くのって面倒ですよね。少ないアクションで相互に切り替えれると便利です。実はiOSのAndroidもコントロールセンター(画面上からしたスワイプして出てくる画面)に下記のようにダークモードの切り替えボタンを追加できます!これすごく便利だと思いませんか?

iOS

IMG_0FD6639A30A5-1.png

Android

IMG_0FD6639A30A5-1.png

設定方法

以下、各OSでの設定方法をまとめておきます。OSのバージョンや、Androidだと端末が異なると設定方法も変わるので、環境が違う場合は自身の環境に合わせて調べて見てくださいね。

iOS (iPhone XR / 13.3.1)

設定アプリTop コントロールセンター カスタマイズ
IMG_0244.png IMG_0245.png IMG_0246.png

Android (Pixel 4 XL / Andriod 10)

クイック設定パネル カスタマイズ
Screenshot_20200220-115239.png Screenshot_20200220-115329.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Vulkan(特にMali GPU) のデバッグメモ

注意事項

Android Vulkan で不都合があると, デバッグや不都合解析に無限に時間が溶けてしまいます. 最近(2020/02)で Android 9 or later であれば, それなりに多くの confirmance tests をパスしているはずで, ドライバやライブラリは安定してきた感がありますが, まだまだリスクがあります.

まずは, GLES で済むのであれば GLES にしておきましょう.
(ちなみに, Android 9 or 10 では, GLES 2.0 は ANGLE を介して裏では Vulkan で動かしてくれるらしい?)

コードの構成

まず, なるべくコードを PC と同一にして, PC 上でデバッグしやすくしておきます.

https://github.com/SaschaWillems/Vulkan-glTF-PBR あたりを参考にするのがおすすめです.

Vulkan アプリのコードは, 基本 C++(NativeActivity)を想定します.

デバッグの方法

  • Vulkan validation layer を有効にする https://qiita.com/syoyo/items/2e6f9b999452ac6c041f
    • Vulkan API を呼び出すレイヤーでのチェック
  • Address sanitizer を on にして Android アプリビルドする
  • RenderDoc を使う https://renderdoc.org/ (基本 PC でデバッグ. Android も一応ある)
  • SwiftShader(PC)で動かす
    • ただ, シェーダ内での out-of-bounds エラーでは, シェーダ(SPIR-V)を LLVM JIT で動かしているためか, あまり有益な stack trace は出してくれない
  • (optional) 画面を出さない, offscreen レンダリングの仕組みにして, Termux で Vulkan コードのデバッグができるようにする(e.g. シェーダのテストとか, compute shader で physics とかやりたいなど)
  • (optional) https://github.com/google/gapid RenderDoc と似たようなデバッガー. ただ, シーンデータが複雑だとトレース取得のメモリが足りないのか, エラーになりあまり使い物になりませんでした(PC GPU のデバッグはなぜか動かない)

未検証

  • Android シミュレータ(SwiftShader で実行?)で動かしてみる

よくあるエラー(著者が経験したエラー)

Descriptor set あたりでリソース不足エラー

Pool で必要なリソース(Uniform buffer, shader storage buffer など, それぞれのリソースに対して)が確保されているか確認しましょう. この場合は, Mali だと Vulkan API が error をレポートしてくれました(Adreno では error 出なかった)

vkWaitForFences で VK_ERROR_DEVICE_LOST(-4)

https://www.reddit.com/r/vulkan/comments/egpt1i/error_device_lost_on_second_frame_on_large_object/

vkWaitForFences を数回後読んだ後に VK_ERROR_DEVICE_LOST(-4) に遭遇したら, Uniform buffer or Shader storage buffer あたりの out-of-bounds アクセスを疑ったほうがいいでしょう.
(実際, 著者が経験したものでは, SSBO への out-of-bounds アクセスエラーが原因でした)

robustBufferAccess

out-of-bounds アクセスなどは, robustBufferAccess でとりあえず suppress することができます.

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.101897_0200_00_en/yfx1570448101228.html

https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkPhysicalDeviceFeatures.html

robustBufferAccess を有効にするには,

VkPhysicalDeviceFeatures.rubustBufferAccess に VK_TRUE を設定して,
VkDevieCreateInfo.pEnabledFeaturesVkPhysicalDeviceFeatures 構造体変数を設定し, vkCreateDevice します.

一応, 事前に vkGetPhysicalDeviceFeatures で, rubustBufferAccess が利用できるかチェックしておきましょう(少なくとも最新 Mali では利用できました).

robustBufferAccess 有効ではエラーが出ないが, robustBufferAccess 無効にするとエラーが出る場合, out-of-bounds アクセス周りがおかしいと判定できます.

未解決 issue

Adreno で頂点モーフすると glitch が出る. 同期がうまくいってない?

Android 向けの個別対応

  • 画面の回転や, スリープなどからの復帰 => SwapChain などの作り直し(Vulkan-glTF-PBR にはサンプルコード無いので, 自前でコード書く必要あり)

SwiftShader で試す.

SwiftShader(CPU Vulkan 実装) を Debug ビルド or ASAN 有効にして実行すると, out-of-bounds アクセスでは seg fault してくれたりします. ただ, シェーダの実行は marl 経由でスレッド実行 and LLVM JIT あたりで実行しているためか, 有意義な stack trace を出してくれないので, 原因が分かりづらいです. シェーダ内で printf とかできるとうれしいところ.

Mali 向け

Mali は, 性能出すためか, 省電力化を行うためか, 仕様に忠実であり不都合が多い(不定な入力に対して undefined behavior が多い)感じがあります(Adreno と比較して).
特に, Andreno(や PC GPU)では, out-of-bounds はいい感じにエラーにならずに処理してくれますが, Mali では device lost エラーになります(これはこれで正しい振る舞いですが)

Vulkan API レベルでは, 少なくとも Android 10 + Mali だと, 仕様に忠実であるためか, Adreno より厳格にエラーレポートしてくれる気がします.

また, OS や端末などによっても挙動がことなったりしますので, 何種類かの実デバイスでテストするのが理想です(Vulkan アプリを複数実端末でバッチでテストできる仕組みほしい)

robustBufferAccess ですが, いくらかパフォーマンスは落ちる & シーンによるですが, すごい劇的な性能低下にはならず, 10% くらい遅くなるかな? という感じでした.

未検証. arm mobile studio(Mali Graphics Debugger)

最近では Graphics Analyzer と名前が変わっています.

https://developer.arm.com/tools-and-software/graphics-and-gaming/arm-mobile-studio

Mali Graphics Debugger, 以前は rooted or hikey みたいな dev board でないと使えなかった気がしますが, 最近は non-rooted(つまり普通に売っているスマホ)でも動くのですね.

問題が起きたときの駆け込み寺とか...

ネットを漁っても, Android + Vulkan はあんまり有益な情報がないことが多いです.

Tencent ncnn https://github.com/Tencent/ncnn は, 涙ぐましい努力のたまものか, Mali のバグ回避のコードがあって意外と有益です.

Twitter が意外といいかもです. Android Vulkan に詳しい人たちの目に止まれば, なにか解決策を得られるかもしれませんよ?

あとは, たまにチェックしたりしていますが, Khronos の Slack に Vulkan channel があります. ただ, 基本 PC GPU 系の人たちが集まっているので, Android 向けにはあまり有益な情報を得られないかもしれません.

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