20210220のAndroidに関する記事は10件です。

Androidでしてはいけない再帰をする

ふと思った
onCreate()みたいなところで自分をまた呼び出すようにしたらどうなるだろう?」
結果はわかりきっているがやってみた

実際にやってみた

onCreate()と言っておいてonResume()に書いていますが気にしない気にしない

LaboratoryActivity.kt
class LaboratoryActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_laboratory)
    }

    override fun onResume() {
        super.onResume()
        Log.d("korehataihennnamisuwokennsyousurutamenomonodesu,Androidkunn...taete!", "onResume")

        startActivity(Intent(this, LaboratoryActivity::class.java))
    }
}

結果

MainActivityにおいてあるボタンからLaboratoryActivityに行くと...

LaboratoryActivityの画面は描画されませんが、しっかりと仕込んでおいたログが何回も出ています
少しすると画面が描画されました(なにか限界を迎えた感じがした)
もちろん、UIのボタンは反応しません

だんだん再帰の速さが遅くなってきたので、止めました

まとめ

やる前からわかっていましたが、やっぱりフリーズしました

おまけ 「何もできない!」

やっても意味ないし、やってはいけないのですが、やるときは気をつけましょう
さっき だんだん再帰の速さが遅くなってきたので、止めました なんて言いました
でも、実際はそんなあっさり止まりません、というか本当に止まりません

画面は何も反応しません タスク一覧は出ません ホーム画面にいけません
「ん〜 詰んだ?」いいえ諦めません
なぜかというと

$ adb shell

は全然反応します
そこでam force-stop xxxxxだったりam kill xxxxxだったりしましたが、だめでした
画面は凍ったままです
なんとか通知とかを出すくらいはできたので、電源メニューを出して再起動をしましたが、再起動しませんでした
じゃあどうしよう...

結果的には

$ adb reboot

で大丈夫でした
普通に再起動しました

ちょっと焦った

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

Kotlin : Realmを用いてcsvファイルをセットする

はじめに

Realmのデータベースを用いたデモアプリを作成していたとき、データベースが空の状態で登録や削除を行っていましたが、予めcsvファイルをセットしておく方法も試したので、忘れないように残そうと思います。

build.gradleの設定

projectレベルのbuild.gradleを開き、以下の文をclasspathに加えます。

classpath "io.realm:realm-gradle-plugin:10.2.0"

次にapplicationレベルのbuild.gradleを開き、以下のpluginを適用します。

id 'realm-android'

ここで、言語がkotlinである場合は以下のpluginを適用する必要があります。このpluginは上記の'realm-android'より上に配置してください。

id 'kotlin-kapt'

詳しい方法はこちら

Realmの初期化

次にRealmの初期化を行います。
初期化したいことを書くクラスにApplicationクラスがあり、このApplicationクラスを継承した新しいクラスを作成し、その中で初期化を行います。

NewActivity.kt
package com.example.databasecsv

import android.app.Application
import io.realm.Realm

class NewActivity : Application() {

    override fun onCreate() {
        super.onCreate()

        //Realmの初期化
        Realm.init(this)
    }
}

モデルクラスの作成

データベースの枠組みをクラスとして設定します。

先程と同様に新しいクラスを作成し、RealmObjectクラスを継承しクラスをオープンにします。
作成したクラス内でデータベースのフィールド(列)を設定します。

ExDB.kt
package com.example.databasecsv

import io.realm.RealmObject

open class ExDB : RealmObject() {

    //フィールドの設定
    var exQuestion : String? = null
    var exAnswer : String? = null

}

準備

  • セットするCSVファイルを用意
    このときファイルの名前はモデルクラス(ExDB)と同じに、フィールドもモデルクラスで宣言したもの(exQuestionやexAnswer)と同じにします。

  • Realm Studioをインストール
    上のリンクからダウンロードしてできたら以下のようにしてセットしたcsvファイルからrealmファイルを作成
    2021-02-20 (16).png

  • realmファイルができないとき(Error : Failed to import data)
    2021-02-20 (21).png
    保存したcsvファイルをメモ帳で開き文字コードをUTF-8にしてからrealmファイルを作るとうまくいくことがあります

realmファイルをAndroid Studioに取り込む

2021-02-20 (6).png
Assetsフォルダを作成し、ここに先程のrealmファイル(test.realm)を配置します。
配置したらRealmの初期化の下に以下の設定を追加します。

NewActivity.kt
val config = RealmConfiguration.Builder().assetFile("test.realm").build()
Realm.setDefaultConfiguration(config)

リスト表示

MainActivity.kt
package com.example.databasecsv

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import io.realm.Realm
import io.realm.RealmResults
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit var realm : Realm
    lateinit var results : RealmResults<ExDB>

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

    override fun onResume() {
        super.onResume()

        realm = Realm.getDefaultInstance()

        results = realm.where(ExDB::class.java).findAll().sort(getString(R.string.db_field_question))

        val wordlist = ArrayList<String>()

        results.forEach{
            wordlist.add(it.exAnswer + " : " + it.exQuestion)
        }

        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, wordlist)
        listView.adapter = adapter

        }

    override fun onPause() {
        super.onPause()

        realm.close()
    }

strings.xml
<string name="db_field_question">exQuestion</string>

Error

2021-02-20 (23).png
上記のようなエラーが出た場合は以下の文を追加することで消えました。

strings.xml
<application
        //省略
        android:name=".NewActivity">
        //省略
</application>

Execution failed for task ':app:kaptDebugKotlin'.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
java.lang.reflect.InvocationTargetException (no error message)

このエラーがなかなか消えなかったのですが、kotlinのバージョンを1.4.20>1.3.50に下げることで消えました。
(間違った方法でしたら申し訳ありません。こちらを参考にしました)

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

kivyに関する備忘録

Androidアプリを作っている際にpythonでGUIを作成したいなと思ったので、kivyというライブラリを使ってGUIを作成することにしました!
今回はメモ代わりに書いているので、正直書いている項目に順番はありません。

また、今回はWidgetクラスウィジェットを区別して書くことにします。カタカナでウィジェットになっている場合は、ButtonやLabelなどのウィジェットだと思ってください。

開発環境

  • Visual Studio Code 1.53.2
  • MacOS Darwin x64 20.3.0
  • python 3.7.7(anaconda)

参考記事

こちらの記事がわかりやすかったので、参考にさせていただきました。
Kivy 超入門(6):動的配置 – float レイアウト
Python: Kivy と Matplotlib でデータセットの確認ツールを書いてみる
Kv Languageを使ったKivyの動かし方
Kv言語の基本
Python: Kivy で Matplotlib のグラフをプロットする
Python3入門〜Kivy による GUI アプリケーション開発,サウンド入出力,ウェブスクレイピング〜

GUIを構築するためのプログラミング言語

まず、最初にAndroidアプリを作る時には、Androidアプリを作るための統合開発環境である、Android Studioというものが良く使われているみたいです。

しかし、これで作る場合は、言語はJavaかKotlinというもので作る必要があるのですが、自分はPythonで書く方が慣れているため、なんとかpythonで作る方法を探してみたところ、見つかったのが、このkivyというライブラリです。

kivyの役割

kivyライブラリを使って、GUIを作成する際は、

①pythonファイルだけを作成して、そこにkivyライブラリを読み込んで、GUIを作っていく方法
②pythonファイルとkvファイルの2つを作成して、pythonファイルには機能だけを、kvファイルにはレイアウトだけを加えていき、GUIを作成していく方法(この場合もpythonファイルにはkivyライブラリを読み込む)

②の場合は、HTMLとCSSのような書き方になっておりますね。

日本語表記するための最強のライブラリ

KV Languageを使って日本語表記を行おうと思っている方は多いと思います。
しかし、kvはデフォルトでは英語表記になっておりますので、日本語表記にするために色々面倒くさそうなことをしなければありません。

そこで活躍するのが、 japanize_kivyになります!

!pip install japanize-kivy
import japanize_kivy

これを行うだけで、他には何もしなくても日本語で表示することが可能です。

Python: インポートするだけで Kivy が日本語を表示できるようになる japanize-kivy を作った

Widgetとは

まず、Kivyを勉強していて思ったのが、Widgetクラスを親クラスとして継承して、Buttonなどを作っている人もいれば、継承せずに作っている人もいるという不思議。

どうやら、WidgetクラスとはButtonLabelなどの総称のことではないかと自分は理解しました。
なので、Widgetクラスを継承しておけば、他を包括しているので、万能!みたいな感じかなと。

ルートウィジェットとは

ウィジェットのことは少しわかったのですが、次によくわからなかった部分が、ルートウィジェットになります。

ズバリ結論から言いますと、ルートウィジェットというのは、全てのウィジェットの1番基盤の部分になります。

少しわかりにくいと思いますので、具体例を見ながら確認していきましょう。

Application.py
from kivy.app import App
from kivy.uix.widget import Widget

class MyWidget(Widget):
    pass

class MyApp(App):
    def build(self):
        return MyWidget()

if __name__ == '__main__':
    MyApp().run() 

こちらのpythonファイルの方で、Widgetクラスを継承して、新しいMyWidgetクラスを作成しています。

my.kv
<MyWidget>:
    BoxLayout:
        orientation: 'vertical'

        Label:

        GridLayout:
            rows:1
            cols:4

            Button:

            Button:

            Button:

            Button:

上の場合ですと、ルートウィジェットというのは、MyWidgetになります。
ルートウィジェットというのはアプリケーションのなかに、必ず1つだけしか存在しないという特徴があります。

  • MyWidgetという1番大きなWidgetを定義する
  • その中にBoxLayoutという2番目に大きなWidgetを定義する
  • BoxLayoutの中に、LabelGridLayoutButtonなどの細かいウィジェットが入っている

このような構造になっております。

MyWidgetが親だとすると、BoxLayoutは子ども、LabelGridLayout
Buttonは孫みたいな感じになりますね。

selfとrootの違い

先程のコードのkvファイルのみを用いて説明をしたいと思います。

my.kv
<MyWidget>:
    BoxLayout:
        orientation: 'vertical'

        Label:

        GridLayout:
            rows:1
            cols:4

            Button:

            Button:

            Button:

            Button:

まず、selfとは個々のウィジェット自身を指します。
GridLayoutの部分にself.を書けば、GridLayout自身を指しますし、Button
の部分に書けば、Button自身を指します。

次にrootですね。
正直、rootに関してはあまり確信を持てていない状態です。
個人的な認識としては、rootウィジェットを指していると考えております。

確信を持てていない理由

なぜ、確信を持てていないのかというと、例えば、下で説明するような画面の切り替えを行う場合に、rootは結局1番根っこのrootウィジェットを指しているのか、それともrootウィジェットに追加されたウィジェットを指しているのかわからないからです。

pos_hintとsize_hint

アプリケーションを作るということになった場合、PCとスマホで画面の大きさが違うという問題にぶち当たりますよね。

通常は、以下のように、ピクセル単位で数字を指定していくのですが、これだと、PCの時は良いかもしれないが、スマホの時はサイズが合わないというふうになってしまいます。

pos: 20, 50
size: 500, 300

そこで、活躍してくれるのが、pos_hintsize_hintというものになります。

pos_hint: {'x':0, 'top':1}
size_hint: 0.3, 0.4

のような形で、size_hintは全画面に対してどれくらいの割合の大きさか、pos_hint画面の左下を(0,0)として、どれくらいの割合の場所に位置するかというのを指定します。

pos_hintの変数

pos_hintx, y, right, topを変数として指定することができます。

xはウィジェットの左の部分の位置を指定
yはウィジェットの下部分の位置を指定
rightはウィジェットの右部分の位置を指定
topはウィジェットの上部分の位置を指定

を行うことができます。

root.sizeとhint_sizeはどう使い分ければ良い?

個人的によくわかりにくかったのが、この2つを使い分けることですね。

  • root.sizeは1番大きい外枠のWidgetの大きさを基準にする
  • hint_sizeは今現在のWidgetの大きさを基準にする

少し文字だとわかりにくいので、具体例を使って説明していきたいと思います。

Application.py
from kivy.app import App
from kivy.uix.widget import Widget

class MyWidget(Widget):
    pass

class MyApp(App):
    def build(self):
        return MyWidget()

if __name__ == '__main__':
    MyApp().run() 
my.kv
<MyWidget>:
    BoxLayout:
        orientation: 'vertical'
        size: root.size

        Label:
            id: txt01
            text: root.text
            size_hint: 1, 0.1

BoxLayoutでまず、root.sizeを使用しているのですが、これは、ルートウィジェットの大きさまで大きくするという認識で考えております。

Labelの方で、size_hintを使用していますが、こちらは、Labelより1つ外側のWidget(今回は、BoxLayout)の大きさを基準にして、調整する形になります。

もしも、Labelroot.sizeを使った場合は、MyWidgetの大きさのLabelができあがります。(間違っていたらすみません。)

つまづいた点

こちらは、GUIアプリを作っている際に、つまづいて、Teratailにも質問してみた内容です。

Application.py
import japanize_kivy
import pandas_datareader.data as web
import numpy as np
import matplotlib.pyplot as plt
import datetime
import talib
import mplfinance as mpf
import time
import pandas as pd
import tensorflow as tf
import schedule
import traceback
import sys
from sklearn import preprocessing
from StockApp import *
from matplotlib import gridspec
from kivy.app import App
from kivy.config import Config
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import *
from kivy.resources import resource_add_path
from kivy.core.text import LabelBase, DEFAULT_FONT
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
from kivy.graphics import *
from kivy.factory import Factory

# マルチタッチを無効化する => 右クリックしても赤い点が残らない
Config.set('input', 'mouse', 'mouse, disable_multitouch')
Config.set('modules', 'inspector', '')

Name = ['A', 'B']

# csvファイルを読み込む
Hello01 = pd.read_csv('graph01.csv')
Hello02 = pd.read_csv('graph02.csv')
Hello03 = pd.read_csv('graph03.csv')

Hello01['Date'] = pd.to_datetime(Hello01['Date'])
Hello02['Date'] = pd.to_datetime(Hello02['Date'])
Hello03['Date'] = pd.to_datetime(Hello03['Date'])

# DataFrameの状態では、インデックス番号で指定できないからnp.arrayで配列に変換する
A_Hello = np.array(Hello01).T
B_Hello = np.array(Hello02).T
C_Hello = np.array(Hello03).T

class FirstPage(BoxLayout):
    text = StringProperty() # プロパティの追加
    graph = NumericProperty(0)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.text = ' '

        # リストに入れる
        self.d_list = []
        self.w_list = []
        self.m_list = []

        self.numbers = 0
        with open('File01.txt', 'r', encoding='utf-8') as day:
            self.day = day.readlines()
        with open('File02.txt', 'r', encoding='utf-8') as week:
            self.week = week.readlines()
        with open('File03.txt', 'r', encoding='utf-8') as month:
            self.month = month.readlines()

    # ActionBarの次へボタンを押したら次の会社に行くようにする処理
    def Next(self, index_num):
        if 0 <= index_num < len(Name) - 1:
            self.numbers = index_num 

        self.text = self.day[self.numbers] 

    # 月のグラフの表示を更新する処理
    def update01(self):
        self.text = self.month[self.numbers]

    # 週のグラフの表示を更新する処理
    def update02(self):
        self.text = self.week[self.numbers]

    # 日の表示を更新する処理
    def update03(self):
        self.text = self.day[self.numbers]


class GraphView(BoxLayout):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # データをrootウィジェットの方に呼び出すようにする => このクラスがインスタンス化された時に1番最初に呼び出される
        self.A_Hello = A_Hello
        self.B_Hello = B_Hello
        self.C_Hello = C_Hello

        self.d_data = Hello01
        self.w_data = Hello02
        self.m_data = Hello03

        self.num = 0


        # figとaxだけで表す必要あり
        self.fig, self.ax = plt.subplots(2, 1, 
                                        gridspec_kw={
                                            'height_ratios':[4,1]
                                        })

        self.Update(self.A_Hello[self.num+1], self.d_data[Name[self.num]], Hello['Date'])

        self.add_widget(FigureCanvasKivyAgg(self.fig))


    def Update(self, data, signal, times):
        # 前にプロットされたグラフを消去する
        self.ax[0].clear()
        self.ax[1].clear()

        period = 30
        upper, middle, lower = talib.BBANDS(signal, timeperiod=period, nbdevup=1, nbdevdn=1, matype=0)    # 引数としてHelloを取得するからselfをつける必要はない

        rsi = talib.RSI(signal, timeperiod=period)

        # 取得したデータを元にプロットする
        self.ax[0].plot(times, data)
        self.ax[0].set_ylabel('price', fontsize=15)
        self.ax[0].plot(times, upper)
        self.ax[0].plot(times, lower)
        self.ax[0].legend([Name[self.num]])
        self.ax[0].tick_params(labelsize=10)

        self.ax[1].plot(times, rsi)
        self.ax[1].set_xlabel('time', fontsize=20)
        self.ax[1].set_ylabel('persentage', fontsize=15)
        self.ax[1].legend(['RSI'])
        self.ax[1].tick_params(labelsize=10)

        # 再描画する
        self.fig.canvas.draw()
        self.fig.canvas.flush_events()
        time.sleep(0.1) # 0.1秒だけ開ける



    # ActionBarの次へボタンを押したら次に行くようにする処理
    def Next(self, index_num):
        if 0 <= index_num < len(Name) - 1:
            self.num = index_num

        A_Hello = self.A_Hello[self.num+1]
        data = self.d_data[Name[self.num]]

        self.Update(A_Hello, data, Hello['Date'])


    # 月のグラフの表示を更新する処理
    def update01(self):
        C_Hello = self.C_Hello[self.num+1]
        m_data =  self.m_data[Name[self.num]]

        self.Update(C_Hello, m_data, Hello03['Date']) # 上で変更された変数を元にしてメソッドが実行される


    # 週のグラフの表示を更新する処理
    def update02(self):
        B_Hello = self.B_Hello[self.num+1]
        w_data = self.w_data[Name[self.num]]

        self.Update(B_Hello, w_data, Hello02['Date'])

    # 日毎の表示を更新する処理
    def update03(self):
        A_Hello = self.A_Hello[self.num+1]
        d_data = self.d_data[Name[self.num]]

        self.Update(A_Hello, d_data, Hello['Date'])

class TextWidget(Widget):
    sm = ScreenManager()
    print(Name[0])
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def change_page(self):
        self.clear_widgets()
        self.add_widget(FirstPage())

    def change_page2(self):
        self.clear_widgets()
        page2 = Factory.SecondPage()
        self.add_widget(page2)

class CopyHelloApp(App):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.title = 'HelloWold' # ウィンドウの名前を変更する

    def build(self):
        return TextWidget()

if __name__ == '__main__':
    CopyHelloApp().run()
copyhello.kv
#:kivy 2.0.0

# TextWidget rootウィジェットに指定されている
TextWidget:

<TextWidget>:
    FirstPage:
        id: page1

<page1>:
    orientation: 'vertical'
    size: root.size  # BoxLayoutのサイズ rootウィジェット(TextWidget)の大きさに合わせる

    # メニューバー
    ActionBar:

        ActionView:
            ActionPrevious:
                title: 'ページタイトル1'
                with_previous: False # 戻るボタンを表示する

            ActionButton:
                text: '次のページ'
                on_release: app.root.change_page2()

            ActionGroup:
                text: 'グループ名'
                mode: 'spinner'
                ActionButton:
                    text: 'A'
                    on_release: app.root.ids['graph_view'].Next(0)
                    on_release: app.root.ids['page1'].Next(0)
                    on_release: name.text = 'A'
                ActionButton:
                    text: "B"
                    on_release: app.root.ids['graph_view'].Next(1)
                    on_release: app.root.ids['page1'].Next(1)
                    on_release: name.text = 'B'


        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: 0.1

            Label:
                id: name
                text: 'A'

            Label:
                id: show
                text: app.root.ids['page1'].text


        # グラフをここで表示する
        GraphView:
            size_hint_y: 0.9
            id: graph_view

        GridLayout:
            cols: 3
            rows: 1
            size_hint_y: 0.1
            Button:
                id: button11
                text: '月'
                font_size: 48
                on_release: app.root.ids['graph_view'].update01()
                on_release: app.root.ids['page1'].update01()

            Button:
                id: button12
                text: '週'
                font_size: 48
                on_release: app.root.ids['graph_view'].update02()
                on_release: app.root.ids['page1'].update02()

            Button:
                id: button13
                text: '日'
                font_size: 48
                on_release: app.root.ids['graph_view'].update03()
                on_release: app.root.ids['page1'].update03()

<GraphView>:


<SecondPage@BoxLayout>:
    id: page2
    size: root.size
    orientation: 'horizontal'

    # 画面の切り替えを行う
    ActionBar:

        ActionView:
            ActionPrevious:
                title: 'ページタイトル2'
                with_previous: False # 戻るボタンを表示する

            ActionButton:
                text: '前のページ'
                on_release: app.root.change_page()

    MyLabel:
        id: first_button
        text: 'Hello'

    MyLabel:
        id: second_button
        text: 'World'

<MyLabel@Label>:
    font_size: 60
    size_hint_y: 1
'''

このコードですと、なぜか画面が真っ暗になってしまったので、なんでかなと思いつつ、エラーになる前の状態から1つずつ機能を追加していきました。

Application.py
import japanize_kivy
import pandas_datareader.data as web
import numpy as np
import matplotlib.pyplot as plt
import datetime
import talib
import mplfinance as mpf
import time
import pandas as pd
import tensorflow as tf
import schedule
import traceback
import sys
from sklearn import preprocessing
from StockApp import *
from matplotlib import gridspec
from kivy.app import App
from kivy.config import Config
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import *
from kivy.resources import resource_add_path
from kivy.core.text import LabelBase, DEFAULT_FONT
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
from kivy.graphics import *
from kivy.factory import Factory

# マルチタッチを無効化する => 右クリックしても赤い点が残らない
Config.set('input', 'mouse', 'mouse, disable_multitouch')
Config.set('modules', 'inspector', '')

Name = ['A', 'B']

# csvファイルを読み込む
Hello01 = pd.read_csv('graph01.csv')
Hello02 = pd.read_csv('graph02.csv')
Hello03 = pd.read_csv('graph03.csv')

Hello01['Date'] = pd.to_datetime(Hello01['Date'])
Hello02['Date'] = pd.to_datetime(Hello02['Date'])
Hello03['Date'] = pd.to_datetime(Hello03['Date'])

# DataFrameの状態では、インデックス番号で指定できないからnp.arrayで配列に変換する
A_Hello = np.array(Hello01).T
B_Hello = np.array(Hello02).T
C_Hello = np.array(Hello03).T

class FirstPage(BoxLayout):
    text = StringProperty() # プロパティの追加
    graph = NumericProperty(0)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.text = ' '

        # リストに入れる
        self.d_list = []
        self.w_list = []
        self.m_list = []

        self.numbers = 0
        with open('File01.txt', 'r', encoding='utf-8') as day:
            self.day = day.readlines()
        with open('File02.txt', 'r', encoding='utf-8') as week:
            self.week = week.readlines()
        with open('File03.txt', 'r', encoding='utf-8') as month:
            self.month = month.readlines()

    # ActionBarの次へボタンを押したら次の会社に行くようにする処理
    def Next(self, index_num):
        if 0 <= index_num < len(Name) - 1:
            self.numbers = index_num 

        self.text = self.day[self.numbers] 

    # 月のグラフの表示を更新する処理
    def update01(self):
        self.text = self.month[self.numbers]

    # 週のグラフの表示を更新する処理
    def update02(self):
        self.text = self.week[self.numbers]

    # 日の表示を更新する処理
    def update03(self):
        self.text = self.day[self.numbers]


class GraphView(BoxLayout):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # データをrootウィジェットの方に呼び出すようにする => このクラスがインスタンス化された時に1番最初に呼び出される
        self.A_Hello = A_Hello
        self.B_Hello = B_Hello
        self.C_Hello = C_Hello

        self.d_data = Hello01
        self.w_data = Hello02
        self.m_data = Hello03

        self.num = 0


        # figとaxだけで表す必要あり
        self.fig, self.ax = plt.subplots(2, 1, 
                                        gridspec_kw={
                                            'height_ratios':[4,1]
                                        })

        self.Update(self.A_Hello[self.num+1], self.d_data[Name[self.num]], Hello['Date']) # グラフを初期化する => ここは日毎のデータにする
        # ax[0].set_title('HelloPredict')

        self.add_widget(FigureCanvasKivyAgg(self.fig))


    def Update(self, data, signal, times):
        # 前にプロットされたグラフを消去する
        self.ax[0].clear()
        self.ax[1].clear()

        period = 30
        upper, middle, lower = talib.BBANDS(signal, timeperiod=period, nbdevup=1, nbdevdn=1, matype=0)    # 引数としてHelloを取得するからselfをつける必要はない

        rsi = talib.RSI(signal, timeperiod=period)

        # 取得したデータを元にプロットする
        self.ax[0].plot(times, data)
        self.ax[0].set_ylabel('price', fontsize=15)
        self.ax[0].plot(times, upper)
        self.ax[0].plot(times, lower)
        self.ax[0].legend([Name[self.num]])
        self.ax[0].tick_params(labelsize=10)

        self.ax[1].plot(times, rsi)
        self.ax[1].set_xlabel('time', fontsize=20)
        self.ax[1].set_ylabel('persentage', fontsize=15)
        self.ax[1].legend(['RSI'])
        self.ax[1].tick_params(labelsize=10)

        # 再描画する
        self.fig.canvas.draw()
        self.fig.canvas.flush_events()
        time.sleep(0.1) # 0.1秒だけ開ける



    # ActionBarの次へボタンを押したら次に行くようにする処理
    def Next(self, index_num):
        if 0 <= index_num < len(Name) - 1:
            self.num = index_num

        A_Hello = self.A_Hello[self.num+1]
        data = self.d_data[Name[self.num]]

        self.Update(A_Hello, data, Hello['Date'])


    # 月のグラフの表示を更新する処理
    def update01(self):
        C_Hello = self.C_Hello[self.num+1]
        m_data =  self.m_data[Name[self.num]]

        self.Update(C_Hello, m_data, Hello03['Date']) # 上で変更された変数を元にしてメソッドが実行される


    # 週のグラフの表示を更新する処理
    def update02(self):
        B_Hello = self.B_Hello[self.num+1]
        w_data = self.w_data[Name[self.num]]

        self.Update(B_Hello, w_data, Hello02['Date'])

    # 日毎の表示を更新する処理
    def update03(self):
        A_Hello = self.A_Hello[self.num+1]
        d_data = self.d_data[Name[self.num]]

        self.Update(A_Hello, d_data, Hello['Date'])

class DisplayCompany(BoxLayout):
    pass


class TextWidget(BoxLayout):
    sm = ScreenManager()
    print(Name[0])
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.page1 = Factory.FirstPage()
        self.page2 = Factory.SecondPage()

    def change_page(self):
        self.clear_widgets()
        self.add_widget(self.page1)

    def change_page2(self):
        self.clear_widgets()
        self.add_widget(self.page2)

class CopyHelloApp(App):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.title = 'HelloWold' # ウィンドウの名前を変更する

    def build(self):
        return TextWidget()

if __name__ == '__main__':
    CopyHelloApp().run()
copyhello.kv
#:kivy 2.0.0

# TextWidget rootウィジェットに指定されている
TextWidget:

<TextWidget>:
    PredictShow:
        id: page1

<PredictShow>:
    orientation: 'vertical'
    size: root.size  # BoxLayoutのサイズ rootウィジェット(TextWidget)の大きさに合わせる

    # メニューバー
    ActionBar:

        ActionView:
            ActionPrevious:
                title: 'ページタイトル1'
                with_previous: False # 戻るボタンを表示する

            ActionButton:
                text: '次のページ'
                on_release: app.root.change_page2()

            ActionGroup:
                text: 'グループ名'
                mode: 'spinner'
                ActionButton:
                    text: 'A'
                    on_release: root.ids.graph_view.Next(0)
                    on_release: app.root.ids['page1'].Next(0)
                    on_release: name.text = 'A'
                ActionButton:
                    text: "B"
                    on_release: root.ids.graph_view.Next(1)
                    on_release: app.root.ids['page1'].Next(1)
                    on_release: name.text = 'B'


        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: 0.1

            Label:
                id: name
                text: 'A'

            Label:
                id: show
                text: root.text


        # グラフをここで表示する
        GraphView:
            size_hint_y: 0.9
            id: graph_view

        GridLayout:
            cols: 3
            rows: 1
            size_hint_y: 0.1
            Button:
                id: button11
                text: '月'
                font_size: 48
                on_release: root.ids.graph_view.update01()
                on_release: app.root.ids['page1'].update01()

            Button:
                id: button12
                text: '週'
                font_size: 48
                on_release: root.ids.graph_view.update02()
                on_release: app.root.ids['page1'].update02()

            Button:
                id: button13
                text: '日'
                font_size: 48
                on_release: root.ids.graph_view.update03()
                on_release: app.root.ids['page1'].update03()

<GraphView>:


<DisplayCompany>:
    id: page2
    size: root.size
    orientation: 'horizontal'

    # 画面の切り替えを行う
    ActionBar:

        ActionView:
            ActionPrevious:
                title: 'ページタイトル2'
                with_previous: False # 戻るボタンを表示する

            ActionButton:
                text: '前のページ'
                on_release: app.root.change_page()

    MyLabel:
        id: first_button
        text: 'Hello'

    MyLabel:
        id: second_button
        text: 'World'

<MyLabel@Label>:
    font_size: 60
    size_hint_y: 1

変更点

・rootウィジェットである、TextWidgetの親クラスをBoxLayoutに変更。
・カスタムウィジェットであるの部分をに変更。
・Labelのtextをapp.root.ids['page1'].textからroot.textに変更
・TextWidgetクラス内で、__init__メソッドを追加し、self.page1=Factory.FirstPage()self.page2=Factory.SecondPage()を記述。
・rootウィジェット内にidを置いたウィジェット(今回ではFirstPage)のメソッドにアクセスする際に、app.root.ids['ID名'].メソッド名を使用する
・rootウィジェットのメソッドにアクセスする際に、root.メソッド名を使用する。
・rootウィジェットの下のウィジェットのメソッドにアクセスする際に、root.ids.ID名.メソッド名を使用する。

わかった点(個人的に解釈しているだけで確証はない)

・画面を切り替えるときは、Widgetクラスじゃなくて、BoxLayoutクラスを継承する方が上手くいく。
・textやsizeなどイベントでは無い部分に対して、app.rootは使わない。root.sizeroot.textという記述をする。
・TextWidgetクラスの初期化部分で、self.ページ名を記述しておくと、別のページに移動しても元の画面の情報が失われない。
・rootウィジェットに追加したウィジェットのメソッドにアクセス→app.root.ids['ID名'].メソッド名
・rootウィジェットクラスのメソッドにアクセス→app.root.メソッド名
・rootウィジェットの下のウィジェットのメソッドにアクセス→root.ids.ID名.メソッド名

まとめ

Kivyの記事は少ないとのことで、自分も理解するのが大変でした。
わかりやすい記事があって助かりました。

自分の記事も皆さんのお助けになればと思います。

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

Android: Lottie Animation をオーバーレイ(ViewOverlay)で表示させる

TL;DR

  • Lottie Animation を ViewOverlay に表示することで、View Tree に影響を与えずにアニメーション表示させる
  • ViewOverlay は Android 4.3 以上で使えます

やりたいことと問題点

Button タップを起点として Lottie でリッチなアニメーションを表示させるとき、レイアウト上の Button のサイズよりも広い範囲にアニメーションを表示させたいことがあります。

たとえば、 Twitter Android アプリのお気に入り Button UI はタップするとハートマークよりも広い範囲にアニメーションが表示されています。

heart1.png

heart2.png

これを XML レイアウト上で表現すると以下のようなレイアウトとなりますが、この方法では Button の周囲の View と LottieAnimationView が被ってしまいレイアウトの配置に苦労します。レイアウトの重なり順にも問題があるため、LottieAnimationView を最前面に移動させる工夫も必要になります。

このレイアウトで A も C もアニメーションを付けることになったら XML が大変なことになりそうです...

image.png

アニメーションを XML で表現したときの構成:

<LinearLayout...>
    <Button.../>
    <FrameLayout...
        android:layout_marginTop="-14dp"
        android:layout_marginStart="-10dp"
        android:layout_marginEnd="-10dp">
        <Butto.../>
        <com.airbnb.lottie.LottieAnimationView.../>
    </FrameLayout>
    <Button..../>
</LinearLayout>

解決策: Lottie Animation を ViewOverlay へ表示する

Android 4.3 以上であれば、ViewOverlay を利用できます。

https://developer.android.com/reference/android/view/ViewGroupOverlay

ViewOverlay は View の最前面に存在しているレイヤーで、任意の View か Drawable を ViewOverlay に表示することができます。
ViewOverlay は View ごとに存在します。ViewOverlay は描画のみに使われるため、タップイベントなどには反応しません。

Activity や Fragment 画面遷移時の Shared Element Transition も ViewOverlay により実現されています。

ViewOverlay の使い方

  • 任意の View に対して view.overlay でアクセスできる
  • オーバーレイ表示するタイミングで view.overlay.add(Drawable) または、 view.overlay.add(View) により、描画したい要素を追加する
    • Drawable の描画位置は drawable.bounds で指定する
  • オーバーレイ表示が不要になったら、view.overlay.remove() で要素を削除する

ViewOverlay + LottieDrawable 実装

後述の PositionedLottieDrawable を用いて、以下のように実装します。

private val targetView: ViewGroup = ... // オーバーレイ表示したい View
private val button: ToggleButton = ... // アニメーションの起点となる Button
private val drawable = PositionedLottieDrawable()
private val compositionTask: LottieTask<LottieComposition>
init {
    drawable.addAnimatorListener(object: Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationEnd(animation: Animator?) {
            // アニメーション終了で Drawable を Overlay から削除
            targetView.overlay.remove(drawable)
            // アニメーション終了後の状態 (Toggle ON/OFF) で表示
            button.alpha = 1f
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    compositionTask = LottieCompositionFactory
        .fromRawRes(context, R.raw.lottie_animation)
        .addListener {
            drawable.composition = it
        }
}
fun clickButton(checked: Boolean) {
    if (checked) {
        // checked = true へ移行するアニメーション処理

        // アニメーション中は Button を非表示
        // 表示したままでよければ alpha を変更する必要はない
        // アニメーション中にも Button タップ判定を拾いたいため、visibility ではなく alpha 変更
        button.alpha = 0f
        // アニメーション Drawable の座標を計算する
        // このサンプルでは Button とアニメーションが中央合わせとなるように計算している
        drawable.x = (this.x - (drawable.composition.bounds.width() - this.width) / 2)
        drawable.y = (this.y - (drawable.composition.bounds.height() - this.height) / 2)
        targetView.overlay.add(drawable)
        drawable.playAnimation()
    } else {
        // checked = false へ移行する実装は省略
        // button.alpha やアニメーションの処理を実装する
        // こちらにもアニメーションが必要なら、checked = true のアニメーションを停止してから
        // あたらしくアニメーションを開始したりする
    }
}

PositionedLottieDrawable ワークアラウンド

Lottie 3.6.1 時点で、LottieDrawable は bounds で指定した座標に描画してくれない問題があります。

以下の PositionedLottieDrawable により、描画座標を修正します。

class PositionedLottieDrawable : LottieDrawable() {
    var x: Float = 0f
    var y: Float = 0f
    override fun draw(canvas: Canvas) {
        canvas.save()
        canvas.translate(x, y)
        super.draw(canvas)
        canvas.restore()
    }
}

ViewOverlay + LottieAnimationView ではうまく動かない

Lottie 3.6.1 時点で、LottieAnimationView は ViewOverlay に追加しても正しくアニメーションしません。

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

Android: Lottie Animation を任意の位置とサイズにオーバーレイ(ViewOverlay)表示させる

TL;DR

  • Lottie Animation を ViewOverlay に表示することで、View Tree に影響を与えずにアニメーション表示させる
  • ViewOverlay は Android 4.3 以上で使えます

やりたいことと問題点

Button タップを起点として Lottie でリッチなアニメーションを表示させるとき、レイアウト上の Button のサイズよりも広い範囲にアニメーションを表示させたいことがあります。

たとえば、 Twitter Android アプリのお気に入り Button UI はタップするとハートマークよりも広い範囲にアニメーションが表示されています。

heart1.png

heart2.png

これを XML レイアウト上で表現すると以下のようなレイアウトとなりますが、この方法では Button の周囲の View と LottieAnimationView が被ることがあったり、見た目上の margin と XML での margin 指定がズレており、レイアウトの配置に苦労します。レイアウトの重なり順にも問題があるため、LottieAnimationView を最前面に移動させる工夫も必要になります。

このレイアウトで A も C もアニメーションを付けることになったら XML が大変なことになりそうです...

image.png

アニメーションを XML で表現したときの構成:

<LinearLayout...>
    <Button.../>
    <FrameLayout...
        android:layout_marginTop="-14dp"
        android:layout_marginStart="-10dp"
        android:layout_marginEnd="-10dp">
        <Button.../>
        <com.airbnb.lottie.LottieAnimationView.../>
    </FrameLayout>
    <Button..../>
</LinearLayout>

解決策: Lottie Animation を ViewOverlay へ表示する

Android 4.3 以上であれば、ViewOverlay を利用できます。

https://developer.android.com/reference/android/view/ViewGroupOverlay

ViewOverlay は View の最前面に存在しているレイヤーで、任意の View か Drawable を ViewOverlay に表示することができます。
ViewOverlay は View ごとに存在します。ViewOverlay は描画のみに使われるため、タップイベントなどには反応しません。

Activity や Fragment 画面遷移時の Shared Element Transition も ViewOverlay により実現されています。

ViewOverlay の使い方

  • 任意の View に対して view.overlay でアクセスできる
  • オーバーレイ表示するタイミングで view.overlay.add(Drawable) または、 view.overlay.add(View) により、描画したい要素を追加する
    • Drawable の描画位置は drawable.bounds で指定する
  • オーバーレイ表示が不要になったら、view.overlay.remove() で要素を削除する

ViewOverlay + LottieDrawable 実装

後述の PositionedLottieDrawable を用いて、以下のように実装します。

private val targetView: ViewGroup = ... // オーバーレイ表示したい View
private val button: ToggleButton = ... // アニメーションの起点となる Button
private val drawable = PositionedLottieDrawable()
private val compositionTask: LottieTask<LottieComposition>
init {
    drawable.addAnimatorListener(object: Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationEnd(animation: Animator?) {
            // アニメーション終了で Drawable を Overlay から削除
            targetView.overlay.remove(drawable)
            // アニメーション終了後の状態 (Toggle ON/OFF) で表示
            button.alpha = 1f
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    compositionTask = LottieCompositionFactory
        .fromRawRes(context, R.raw.lottie_animation)
        .addListener {
            // 非同期で Lottie JSON を読み込み、drawable へ設定する
            drawable.composition = it
        }
}
fun clickButton(checked: Boolean) {
    if (checked) {
        // checked = true へ移行するアニメーション処理

        // より安全な実装とするには、ここで drawable.composition が読み込み済みであることを確認してください
        // 読み込みが完了していなければ compositionTask の終了を待ってからアニメーションを実行する必要があります

        // アニメーション中は Button を非表示
        // 表示したままでよければ alpha を変更する必要はない
        // アニメーション中にも Button タップ判定を拾いたいため、visibility ではなく alpha 変更
        button.alpha = 0f
        // アニメーション Drawable の座標を計算する
        // このサンプルでは Button とアニメーションが中央合わせとなるように計算している
        drawable.x = (button.x - (drawable.composition.bounds.width() - button.width) / 2)
        drawable.y = (button.y - (drawable.composition.bounds.height() - button.height) / 2)
        targetView.overlay.add(drawable)
        drawable.playAnimation()
    } else {
        // checked = false へ移行する実装は省略
        // button.alpha やアニメーションの処理を実装する
        // こちらにもアニメーションが必要なら、checked = true のアニメーションを停止してから
        // あたらしくアニメーションを開始したりする
    }
}

PositionedLottieDrawable ワークアラウンド

Lottie 3.6.1 時点で、LottieDrawable は bounds で指定した座標に描画してくれない問題があります。

以下の PositionedLottieDrawable により、描画座標を修正します。

class PositionedLottieDrawable : LottieDrawable() {
    var x: Float = 0f
    var y: Float = 0f
    override fun draw(canvas: Canvas) {
        canvas.save()
        canvas.translate(x, y)
        super.draw(canvas)
        canvas.restore()
    }
}

ViewOverlay + LottieAnimationView ではうまく動かない

Lottie 3.6.1 時点で、LottieAnimationView は ViewOverlay に追加しても正しくアニメーションしません。

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

[Android]ViewBindingを使いたい!

ViewBindingを使いたい理由

xmlファイルでViewに指定したidを用いてアクティビティやフラグメントを書く際、findViewByIdを逐一書かなくて良きなのと、型安全なので記述ミスがなくなる。数か月くらい前に、話題になりましたが、まだ使えてない!時代のビックウェーブに乗り遅れた!という人向けに書いてみました。実装は、簡単です!
https://developer.android.com/topic/libraries/view-binding

// モジュールの方ね!書けたらSync now!
android {
    ...
    buildFeatures {
        viewBinding true
    }
}

適当に、Viewを配置して…

<?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="match_parent"
    tools:context=".MainActivity">
        <TextView
            android:id="@+id/text1"
            android:layout_margin="10dp"
            android:textSize="20sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0"/>
        <TextView
            android:id="@+id/text2"
            android:layout_margin="10dp"
            android:textSize="20sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
    // ここで、召喚!
    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)

        //bindigを付ければ、操作できる!
        binding.text1.text = "小鳥ん"
        binding.text2.text = "小鳥ん"
おまけ

スコープ関数を使うと毎回bindinを書かなくて良くなり、Viewが増えた時に楽。

class MainActivity : AppCompatActivity() {


    // ここで、召喚!
    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        binding.run {
             text1.text = "小鳥ん"
             text2.text = "小鳥ん"
        }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-native-webview のコンテンツを Android の戻るボタンで操作できるようにする(SPA対応)

概要

単純に「戻る」だけならシンプルに実装できますが、「これ以上戻れるかどうか」により処理を分けたい場合への対応について、 SPA 対応まで含めて説明している記事がなさそうだったのでまとめました。

「Android の戻るボタン」と書いていますが、 UI で「戻る」ボタンを表示しつつ、それ以上戻れないときは非アクティブにするといった対応にも応用できると思います。

1. 単純に戻す

WebView の ref を使って以下のようにすれば戻す操作はできます。

CustomWebView1.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback} from 'react';
import {BackHandler} from 'react-native';
import WebView from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    ref.current?.goBack();

    // true を返すと先に登録されていたイベントハンドラー実行をブロック
    return true;
  }, []);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return <WebView ref={ref} source={{uri}} />;
};

こんな感じでしょうか。

2. それ以上戻れない場合の処理

しかし、 WebView 内でそれ以上戻れない場合は React Navigation などのルーター側の「戻る」機能を使いたい場合があると思います。
そのような場合は onNavigationStateChange を使って画面遷移が発生するごとに canGoBack を取得しておいて、この値が真のときだけ戻れるようにすると良いかと思います。

CustomWebView2.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {WebViewNavigation} from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);
  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

3. ページが SPA の場合にも対応する

2 の実装はあくまで Multi Page Application を想定した実装で、 WebView のコンテンツが Single Page Application だった場合、 onNavigationStateChange イベントが発生しないため、うまく動きません。

SPA でウェブで「戻る」操作が実行されたときは popstate イベントが発生するのですが、「戻る」以外の SPA で画面遷移が発生したときにそのイベントを簡単に検知する web 標準仕様は現状ありません(WHATWGに Issue があります)。

いくつか実装パターンがあると思うのですが、ひとつの方法として、 MutationObserver を使って
window.location.href の変化を検知し、 postMessage で画面遷移を React Native 側で受け取るようにしてみます。

CustomWebView3.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {
  WebViewMessageEvent,
  WebViewNavigation,
} from 'react-native-webview';

type Props = {
  uri: string;
};

// Android 6 の System WebView のデフォルトバージョンが 44 で
// アロー関数に対応していないので
// Android 6 をサポートする場合、一応、アロー関数の使用は避ける
// const, let を使うには "use strict" も必要
// 実際に System WebView のバージョン更新していないユーザーは稀だと思われるが
// 検証用のシミュレーターで動かない場合があるため
const injectedScript = `
  window.addEventListener('DOMContentLoaded', function () {
    let oldHref = window.location.href;
    const bodyList = document.querySelector("body")
    const observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (oldHref != window.location.href) {
          oldHref = window.location.href;
          const message = JSON.stringify({
            type: 'pagemove',
            url: window.location.href
          });

          window.ReactNativeWebView.postMessage(message);
        }
      });
    });

    const config = {
      childList: true,
      subtree: true
    };

    observer.observe(bodyList, config);
  });
`;

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);

  const onMessage = useCallback(
    (event: WebViewMessageEvent) => {
      const message = event.nativeEvent.data;

      try {
        const data = JSON.parse(message);

        // アクションログ
        if (data.type === 'action') {
          const {actionId, query, lat, long} = data as {
            actionId: string;
            query: string;
            lat: string;
            long: string;
          };
          sendLog(actionId, query, lat, long);
        }
        // ブラウザーバック
        else if (data.type === 'pagemove') {
          const {url} = data as {url: string};
          // console.log('pagemove / canGoBack: ', url !== uri);
          setCanGoBack(url !== uri);
        }
      } catch (e) {
        console.error(e);
      }
    },
    [uri],
  );

  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      onMessage={onMessage}
      injectedJavaScriptBeforeContentLoaded={injectedScript}
      javaScriptEnabled
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

MutationObserver を使った実装の参考先: https://stackoverflow.com/a/46428962

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

react-native-webview で表示するページを Android の戻るボタンで戻れるようにする(SPA対応)

概要

単純に「戻る」だけならシンプルに実装できますが、「これ以上戻れるかどうか」により処理を分けたい場合への対応について、 SPA 対応まで含めて説明している記事がなさそうだったのでまとめました。

「Android の戻るボタン」と書いていますが、 UI で「戻る」ボタンを表示しつつ、それ以上戻れないときは非アクティブにするといった対応にも応用できると思います。

1. 単純に戻す

WebView の ref を使って以下のようにすれば戻す操作はできます。

CustomWebView1.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback} from 'react';
import {BackHandler} from 'react-native';
import WebView from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    ref.current?.goBack();

    // true を返すと先に登録されていたイベントハンドラー実行をブロック
    return true;
  }, []);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return <WebView ref={ref} source={{uri}} />;
};

こんな感じでしょうか。

2. それ以上戻れない場合の処理

しかし、 WebView 内でそれ以上戻れない場合は React Navigation などのルーター側の「戻る」機能を使いたい場合があると思います。
そのような場合は onNavigationStateChange を使って画面遷移が発生するごとに canGoBack を取得しておいて、この値が真のときだけ戻れるようにすると良いかと思います。

CustomWebView2.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {WebViewNavigation} from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);
  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

3. ページが SPA の場合にも対応する

2 の実装はあくまで Multi Page Application を想定した実装で、 WebView のコンテンツが Single Page Application だった場合、 onNavigationStateChange イベントが発生しないため、うまく動きません。

SPA でウェブで「戻る」操作が実行されたときは popstate イベントが発生するのですが、「戻る」以外の SPA で画面遷移が発生したときにそのイベントを簡単に検知する web 標準仕様は現状ありません(WHATWGに Issue があります)。

いくつか実装パターンがあると思うのですが、ひとつの方法として、 MutationObserver を使って
window.location.href の変化を検知し、 postMessage で画面遷移を React Native 側で受け取るようにしてみます。

CustomWebView3.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {
  WebViewMessageEvent,
  WebViewNavigation,
} from 'react-native-webview';

type Props = {
  uri: string;
};

// Android 6 の System WebView のデフォルトバージョンが 44 で
// アロー関数に対応していないので
// Android 6 をサポートする場合、一応、アロー関数の使用は避ける
// const, let を使うには "use strict" も必要
// 実際に System WebView のバージョン更新していないユーザーは稀だと思われるが
// 検証用のシミュレーターで動かない場合があるため
const injectedScript = `
  window.addEventListener('DOMContentLoaded', function () {
    let oldHref = window.location.href;
    const bodyList = document.querySelector("body")
    const observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (oldHref != window.location.href) {
          oldHref = window.location.href;
          const message = JSON.stringify({
            type: 'pagemove',
            url: window.location.href
          });

          window.ReactNativeWebView.postMessage(message);
        }
      });
    });

    const config = {
      childList: true,
      subtree: true
    };

    observer.observe(bodyList, config);
  });
`;

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);

  const onMessage = useCallback(
    (event: WebViewMessageEvent) => {
      const message = event.nativeEvent.data;

      try {
        const data = JSON.parse(message);

        // アクションログ
        if (data.type === 'action') {
          const {actionId, query, lat, long} = data as {
            actionId: string;
            query: string;
            lat: string;
            long: string;
          };
          sendLog(actionId, query, lat, long);
        }
        // ブラウザーバック
        else if (data.type === 'pagemove') {
          const {url} = data as {url: string};
          // console.log('pagemove / canGoBack: ', url !== uri);
          setCanGoBack(url !== uri);
        }
      } catch (e) {
        console.error(e);
      }
    },
    [uri],
  );

  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      onMessage={onMessage}
      injectedJavaScriptBeforeContentLoaded={injectedScript}
      javaScriptEnabled
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

MutationObserver を使った実装の参考先: https://stackoverflow.com/a/46428962

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

react-native-webview で表示するページを Android の戻るボタンで戻れるようにする…のはわりと簡単だけど「戻れるかどうか」をちゃんと判定するのは結構難しいという話

概要

単純に「戻る」だけならシンプルに実装できますが、「これ以上戻れるかどうか」により処理を分けたい場合への対応について、 SPA 対応まで含めて説明している記事がなさそうだったのでまとめました。

「Android の戻るボタン」と書いていますが、 UI で「戻る」ボタンを表示しつつ、それ以上戻れないときは非アクティブにするといった対応にも応用できると思います。

1. 単純に戻す

WebView の ref を使って以下のようにすれば戻す操作はできます。

CustomWebView1.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback} from 'react';
import {BackHandler} from 'react-native';
import WebView from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    ref.current?.goBack();

    // true を返すと先に登録されていたイベントハンドラー実行をブロック
    return true;
  }, []);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return <WebView ref={ref} source={{uri}} />;
};

こんな感じでしょうか。

2. それ以上戻れない場合の処理

しかし、 WebView 内でそれ以上戻れない場合は React Navigation などのルーター側の「戻る」機能を使いたい場合があると思います。
そのような場合は onNavigationStateChange を使って画面遷移が発生するごとに canGoBack を取得しておいて、この値が真のときだけ戻れるようにすると良いかと思います。

CustomWebView2.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {WebViewNavigation} from 'react-native-webview';

type Props = {
  uri: string;
};

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);
  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

3. ページが SPA の場合にも対応する

2 の実装はあくまで Multi Page Application を想定した実装で、 WebView のコンテンツが Single Page Application だった場合、 onNavigationStateChange イベントが発生しないため、うまく動きません。

SPA でウェブで「戻る」操作が実行されたときは popstate イベントが発生するのですが、「戻る」以外の SPA で画面遷移が発生したときにそのイベントを簡単に検知する web 標準仕様は現状ありません(WHATWGに Issue があります)。

いくつか実装パターンがあると思うのですが、ひとつの方法として、 MutationObserver を使って
window.location.href の変化を検知し、 postMessage で画面遷移を React Native 側で受け取るようにしてみます。

CustomWebView3.tsx
import {useFocusEffect} from '@react-navigation/native';
import React, {useRef, useCallback, useState} from 'react';
import {BackHandler} from 'react-native';
import WebView, {
  WebViewMessageEvent,
  WebViewNavigation,
} from 'react-native-webview';

type Props = {
  uri: string;
};

// Android 6 の System WebView のデフォルトバージョンが 44 で
// アロー関数に対応していないので
// Android 6 をサポートする場合、一応、アロー関数の使用は避ける
// const, let を使うには "use strict" も必要
// 実際に System WebView のバージョン更新していないユーザーは稀だと思われるが
// 検証用のシミュレーターで動かない場合があるため
const injectedScript = `
  window.addEventListener('DOMContentLoaded', function () {
    "use strict"
    let oldHref = window.location.href;
    const bodyList = document.querySelector("body")
    const observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (oldHref != window.location.href) {
          oldHref = window.location.href;
          const message = JSON.stringify({
            type: 'pagemove',
            url: window.location.href
          });

          window.ReactNativeWebView.postMessage(message);
        }
      });
    });

    const config = {
      childList: true,
      subtree: true
    };

    observer.observe(bodyList, config);
  });
`;

export const CustomWebView = ({uri}: Props) => {
  const [canGoBack, setCanGoBack] = useState(false);

  const onMessage = useCallback(
    (event: WebViewMessageEvent) => {
      const message = event.nativeEvent.data;

      try {
        const data = JSON.parse(message);

        if (data.type === 'pagemove') {
          const {url} = data as {url: string};
          setCanGoBack(url !== uri);
        }
      } catch (e) {
        console.error(e);
      }
    },
    [uri],
  );

  const onNavigationStateChange = useCallback((state: WebViewNavigation) => {
    setCanGoBack(state.canGoBack);
  }, []);

  const ref = useRef<WebView>(null);

  const onBack = useCallback(() => {
    if (canGoBack) {
      ref.current?.goBack();
      return true;
    }

    return false;
  }, [canGoBack]);

  useFocusEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', onBack);

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', onBack);
    };
  });

  return (
    <WebView
      onNavigationStateChange={onNavigationStateChange}
      onMessage={onMessage}
      injectedJavaScriptBeforeContentLoaded={injectedScript}
      javaScriptEnabled
      ref={ref}
      source={{uri}}
    />
  );
};

こんな感じでしょうか。

MutationObserver を使った実装の参考先: https://stackoverflow.com/a/46428962

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

[Android]Action BarとUINavigationの連携がうまくいかない

連携?

UpボタンがDestinationの遷移時に自動的に表示されたり、labelが自動的にタイトルに設定されたりすることです。

原因

初期化処理を
onCreate(savedInstanceState: Bundle?)
ではなく、
onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?)
で行っていたことが原因でした。

ブレークポイントを設定してデバッグすればもっと早く見つかったかもしれません。

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