- 投稿日:2019-05-21T22:01:57+09:00
Android開発者のためのOCR入門(tess-two編)
はじめに
AndroidアプリでOCRを実装する際、TessaractのAndroid用ラッパーライブラリtess-twoを利用するか、またはML Kit for Firebaseを利用する方法があります。
今回は、前者のtess-twoを使ってギャラリーから画像を選択してその画像にある文字を読み取るアプリを作ってみたいと思います。サンプルコードはここに置いてあります。
インストール
まずはtess-twoを入れます。また、画像をギャラリーから読み込む際にExifを読み取って画像の向きを正しい方向に直したいのでexifinterfaceも入れておきます。
app/build.gradle.kts// 略 dependencies { implementation("com.rmtheis:tess-two:9.0.0") implementation("androidx.exifinterface:exifinterface:1.0.0") }続いてappディレクトリ以下に/assets/tessdata/ディレクトリを作成して言語データを配置します。
今回はこんな感じにしました。app - assets - tessdata |-- jpn.traineddata `-- eng.traineddata言語データは下記からダウンロードしてください。tess-twoはtesseract3.0.5以下で作成された言語データが必要です。
https://github.com/tesseract-ocr/tessdata/tree/3.04.00実装
tess-twoの使い方自体は非常に簡単です。わずか5行程度です。
OCRUtil.ktval baseApi = TessBaseAPI() // initで言語データを読み込む baseApi.init(context.getFilesDir().toString(), "jpn") // ギャラリーから読み込んだ画像をFile or Bitmap or byte[] or Pix形式に変換して渡してあげる baseApi.setImage(bitmap) // これだけで読み取ったテキストを取得できる val recognizedText = baseApi.utF8Text baseApi.end()ですが注意点が2つあります。
1つ目はbaseApi.initはの中では、言語データをFileでopenしているためFileで読み込める場所にコピーしてあげる必要があります。
OCRUtil.ktprivate val TESS_DATA_DIR = "tessdata" + File.separator private val TESS_TRAINED_DATA = arrayListOf("eng.traineddata", "jpn.traineddata") private fun copyFiles(context: Context) { try { TESS_TRAINED_DATA.forEach { val filePath = context.filesDir.toString() + File.separator + TESS_DATA_DIR + it // assets以下をinputStreamでopenしてbaseApi.initで読み込める領域にコピー context.assets.open(TESS_DATA_DIR + it).use {inputStream -> FileOutputStream(filePath).use {outStream -> val buffer = ByteArray(1024) var read = inputStream.read(buffer) while (read != -1) { outStream.write(buffer, 0, read) read = inputStream.read(buffer) } outStream.flush() } } val file = File(filePath) if (!file.exists()) throw FileNotFoundException() } } catch (e: FileNotFoundException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } }2つ目はtess-twoで画像を読み込む場合、画像の向きが正しくないと当然読み込めません。ギャラリーから読み込む場合、画像が回転してしまっている場合があるのでExifを見て正しい方向に直す必要があります。
MainActivity.kt// 画像のuri uri?.let { contentResolver.openFileDescriptor(it, "r").use {parcelFileDescriptorNullable -> parcelFileDescriptorNullable?.let {parcelFileDescriptor -> val fileDescriptor = parcelFileDescriptor.fileDescriptor bitmapOrigin = BitmapFactory.decodeFileDescriptor(fileDescriptor) contentResolver.openInputStream(it).use { it?.let { // ExifInterfaceを初期化 val exifInterface = ExifInterface(it) val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) val degrees = when (orientation) { // 正しい方向の場合は回転させない 1 -> { 0f } // 逆向きなので180度回転させる 3-> { 180f } // 左向きの画像になってるので90度回転させる 6 -> { 90f } // 右向きの画像になってるので270度回転させる 8 -> { 270f } else -> { 0f } } val matrix = Matrix() val imageWidth = bitmapOrigin?.getWidth() ?: 0 val imageHeight = bitmapOrigin?.getHeight() ?: 0 matrix.setRotate(degrees, imageWidth.toFloat() / 2, imageHeight.toFloat() / 2) bitmap = Bitmap.createBitmap(bitmapOrigin!!, 0, 0, imageWidth, imageHeight, matrix, true) } } } } }ここまで実装したら早速動かしてみましょう。今回はこちらの画像から文字を読み取ってみます。
早速アプリを起動して画像を読み込んでみると…
無事、文字を読み取ることが出来るようになりました!が、、精度がいまいちです…
日本語言語データをカスタマイズ
先程の画像を見てみると数字が全然読み取れていないことに気がつくと思います。今回はこの数字の精度を改善してみたいと思います。
数字の誤変換の原因ですが、こちらのサイトを見てみると認識結果の変換マッピングが原因のようです。なのでjpn.unicharambigsを修正してみます。
今回、tesseract-ocrの環境をDockerで用意しました。こちらの環境を使ってカスタマイズします。
https://github.com/tarumzu/OCRSampleAndroid/tree/master/containersまずは下記の手順でDockerコンテナにログイン
cd containers docker-compose build docker-compose up docker-compose exec tesseract bashログインしたら、jpn.traineddataをダウンロード、次にcombine_tessdataコマンドのeオプションでjpn.traineddataからjpn.unicharambigsを取り出します。
wget https://github.com/tesseract-ocr/tessdata/raw/3.04.00/jpn.traineddata combine_tessdata -e jpn.traineddata jpn.unicharambigs
取り出したらjpn.unicharambigsをvimで開いて下記の行を削除します。これは左側のキャラクタを右側のものに変換するというルールなのですが正直必要ない変換だと思います。
1 l 1 ー 1 1 | 1 ー 1 1 I 1 ー 1 1 1 1 ー 1 1 | 1 ー 1 1 O 1 。 1 1 ° 1 。 1ここまでできたらjpn.traineddataを再作成します。
combine_tessdata -o jpn.traineddata jpn.unicharambigs
では、先程作成した言語データはcontainersに保存されます。この言語データを使って再度画像を読み込んでみましょう。
たったこれだけで数字の精度が劇的に改善できました!
終わりに
簡単にOCRが実装できましたが言語データのカスタマイズは必須ですね…
次はML Kit for FirebaseでOCRを実装してみて違いなどを記事にしてみたいと思います。参考
- 投稿日:2019-05-21T18:05:53+09:00
Jetpack Composeについて調査 文法編
はじめに
Google I/O 2019で発表された
Jetpack Compose
について調査しました。本記事ではその中でも特徴的な宣言的UI
の構築を行うための文法についてまとめてみます。注意点
本記事は5/21時点でpre-alpha段階の
Jetpack Compose
について調査を行なっており、今後の変更で大きくインターフェースが変わる可能性があります。Jetpack Composeの文法
Hello World
を書く定番の
Hello World
は以下のように書けます。class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { MaterialTheme { val textColor = Color(0xFFFF0000.toInt()) Text( text = "Hello World!", style = +themeTextStyle { h2.copy(color = textColor) } ) } } } } }これは以下のように表示されます。
文字列を縦方向に二つ表示してみましょう。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { MaterialTheme { val textColor = Color(0xFFFF0000.toInt()) Column { Text( text = "Hello", style = +themeTextStyle { h2.copy(color = textColor) } ) Text( text = "World!", style = +themeTextStyle { h2.copy(color = textColor) } ) } } } } } }これは以下のように表示されます。
宣言的にUIが構築されているので、結構読みやすいです。
Hello World
の解説上記
Hello World
に登場したJetpack Compose
固有のワードについて解説します。
setContent
以下のように
Activity
の拡張関数として実装されています。fun Activity.setContent(composable: @Composable() () -> Unit) = setContentView(FrameLayout(this).apply { compose(composable) })受け取った
composable
な関数をFrameLayout
に描画して自身にsetしています。
基本的にはこの関数を使うことでJetpack Compose
で書かれたUIを描画することになります。
CraneWrapper
Context
,FocusManager
,TextInputService
を作り、Jetpack Compose
の世界でこれらを使えるようにするためのwrapperです。上記Android上でのUI構築に必須なものを作る部分なので、setContent
直下にこれを記述し、これに関数を渡すようにUIを設計しないと例外が発生してしまいます。
MaterialTheme
Text
やButton
など、各種コンポーネントに適用できるマテリアルデザインなスタイルを提供します。
+themeTextStyle
上記
MaterialTheme
で提供されている文字に関するスタイルです。これに文字のスタイルを定義する関数を渡すことができます。
この辺りはKotlinDSL
の利点が活かされており、適切なスタイル定義をIDEやコンパイラの支援を受けつつ簡単に書くことができるようになっています。
上記コードでは、h2
スタイルの色だけを0xFFFF0000
(赤色)に変更するようにスタイルを定義しています。
Text
文字列を表現する関数です。Androidで言う
TextView
に相当します。
text
この名前で渡した引数が
Text
で表示される文字列になります。
style
この名前で渡した引数が
Text
で表示される文字列のスタイルになります。
Column
複数のコンポーネントを縦に並べて表示することができます。Androidのorientation=”vertical”な
LinearLayout
に相当します。コンポーネントを自作する
Jetpack Compose
ではコンポーネントの自作も簡単に行うことができます。試しに上記Hello Worldを以下のような仕様でコンポーネントにしてみます。
- Hello World!と表示する
- styleをパラメータとして適用できる
これは以下のように実装することができます。
@Composable fun HelloWorld(style: TextStyle? = null) { Text( text = "Hello World!", style = style ) }このようにして作られたコンポーネントは、以下のようにして使うことができます。
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { MaterialTheme { val textColor = Color(0xFFFF0000.toInt()) HelloWorld( style = +themeTextStyle { h2.copy(color = textColor) } ) } } } } }コンポーネントの自作は非常に簡単で、
@Composable
を付与した関数を作るだけです。ただし、いくつか気をつけなければならない点もあり、
- 副作用を起こさないようにする
- 関数の引数として渡されたパラメータ以外は使わないようにする(例えばグローバル変数にアクセスしたりしない)
と行った注意点も存在します。
ステートを持つUI
を作る上記
Hello World
はステートを持たないシンプルなUIでした。
今度は「ボタンをタップする度にカウントアップする」と言う機能を持ったUIを作ってみます。
タップされた回数を保持するのでステートを必要とします。class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { MaterialTheme { CountableButton() } } } } } @Composable fun CountableButton() { val count = +state { 0 } Column { Button( onClick = { count.value++ }, text = "Tap Me!!" ) Text( text = "Tapped count ${count.value}", style = +themeTextStyle { h3 } ) } }これは以下のように動作します
タップされた回数をカウントしていきます。上の例ではInt
を使いましたが、@Model
を使うことで独自に定義した型を使うこともできます。独自の型を利用する形でCountableButton
コンポーネントを書き換えると以下のようになります。これは上記GIF画像と同じ動きをします。@Composable fun CountableButton() { val count = +state { CountModel() } Column { Button( onClick = { count.value.increment() }, text = "Tap Me!!" ) Text( text = "Tapped count ${count.value.num}", style = +themeTextStyle { h3 } ) } } @Model class CountModel { var num = 0 fun increment() { num++ } }
ステートを持つUI
の解説上記
ステートを持つUI
に登場したJetpack Compose
固有のワードについて解説します。
+state
この関数は
State
インスタンスを作るトップレベル関数です。
この関数にはState
インスタンスが保持するパラメータを初期化する関数を与えることができます。
上記初期化式で初期化されたパラメータを保持したState
インスタンスを返します。
State
stateを管理するクラスです。上記
+state
関数で作ることができます。
value
プロパティを経由することで、内部で保持しているパラメータを参照、更新することができます。
このようにしてパラメータを更新すると、UIが再描画され、更新後のパラメータが適用されます。
@Model
stateを表現するクラスに付与しなければならないアノテーションです。
+state
関数で作成したState
インスタンスで独自のクラスを保持させたい場合には、これを対象のクラスに付与しなければなりません。
これを付与し忘れた場合、コンパイルは通りますが、パラメータに変更があった際にUIが再描画されません。KTX
以下のようなコードは
Text(text = "Hello World!")このようにも書けます。
<Text text="Hello World!" />この
JSX
のタグにも似た書き方はKTX
と呼ばれているそうです。この書き方はKotlinのコンパイラを拡張する方法で実現されているそうです。しかし、KTX
は関数呼び出しで記述する方法を開発する以前の古い記法らしく、現在はKTX
からそちらに移行している最中だそうです。今後のことを考えるとこの書き方はしない方が良さそうです。おわりに
Jetpack Compose
について調べてみました。
Androidの既存の書き方(いわゆる宣言的ではない方法)だと色々と辛いところもありましたが、Jetpack Compose
が実用化されると、非常に簡単かつ苦しみを少なくしてUIを構築できそうです。
stable
になるまでまだまだ時間はかかりそうですが、非常に期待できそうです。参考
https://developer.android.com/jetpack/compose
https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/compose/README.md
https://medium.com/q42-engineering/android-jetpack-compose-895b7fd04bf4
- 投稿日:2019-05-21T17:23:34+09:00
[Kotlin] AlertDialogをカスタマイズする
AlertBuilderでAlertDialogをカスタマイズします。(カスタムレイアウト作成した方がおすすめですが)
ソースコード
AlertDialogFragment.kt// 省略 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // ダイアログのタイトルのカスタマイズ val text = TextView(context) text.text = "Error" text.setPadding(0, 25, 0, 25) text.textSize = 20F text.textAlignment = View.TEXT_ALIGNMENT_CENTER text.setTextColor(resources.getColor(R.color.white, null)) text.background = resources.getDrawable(R.color.green, null) val builder = activity?.let { AlertDialog.Builder(it) } // setTitleではなくsetCustomTitleを使う builder!!.setCustomTitle(text) .setMessage(message) .setPositiveButton(okButtonName, onOkClickListener) val dialog = builder.show() // OKボタンを中央揃えに val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE) button.gravity = Gravity.CENTER button.setTextColor(resources.getColor(R.color.colorWhite, null)) button.setPadding(0, -30, 0, -30) button.background = resources.getDrawable(R.drawable.green_button, null) button.textSize = 15F val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 55) layoutParams.gravity = Gravity.CENTER layoutParams.marginStart = 250 layoutParams.marginEnd = 250 layoutParams.topMargin = 20 layoutParams.bottomMargin = 15 button.layoutParams = layoutParams this.isCancelable = false return dialog }二つ以上のボタンがある時
AlertDialogFragment.kt// 省略 builder!!.setCustomTitle(text) .setMessage(message) .setPositiveButton("OK",onOkClickListener) .setNegativeButton("CANCEL",onCancelClickListener) .setOnDismissListener(onDismissListener) val dialog = builder.show() val okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE) okButton.gravity = Gravity.CENTER okButton.setTextColor(resources.getColor(R.color.white, null)) okButton.setPadding(70, -30, 70, -30) okButton.background = resources.getDrawable(R.drawable.green_button, null) okButton.textSize = 15F val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, 55) layoutParams.gravity = Gravity.CENTER layoutParams.marginStart = 30 layoutParams.marginEnd = 160 layoutParams.topMargin = 20 layoutParams.bottomMargin = 10 okButton.layoutParams = layoutParams val cancelButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE) cancelButton.gravity = Gravity.CENTER cancelButton.setTextColor(resources.getColor(R.color.green, null)) cancelButton.setPadding(50, -30, 50, -30) cancelButton.textSize = 15F val layoutParams2 = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, 55) layoutParams2.gravity = Gravity.CENTER layoutParams2.marginStart = 60 layoutParams2.marginEnd = 30 layoutParams2.topMargin = 20 layoutParams2.bottomMargin = 10 cancelButton.layoutParams = layoutParams2 // 省略成果物
- 投稿日:2019-05-21T10:14:58+09:00
Android GoogleMapの使い方メモ
はじめに
最近プロジェクトでGoogleMapAPIを利用したので、細かな使い方を記事として残しておきます。
プロジェクト作成〜地図の表示
AndroidStudioのプロジェクトウィザードで簡単に作れます。
生成された、google_maps_api.xml を開き、
YOUR_KEY_HERE
を更新します。キーは https://console.developers.google.com から取得します。
UIの設定
移動
スワイプで地図を平行移動できます。
googleMap.uiSettings.isScrollGesturesEnabled = trueズーム
ピンチイン・アウトで縮尺を変更できます。
googleMap.uiSettings.isZoomGesturesEnabled = true回転
ピンチからの回転で地図が回転できます。
googleMap.uiSettings.isRotateGesturesEnabled = trueズームボタン
画面右下にズーム変更ボタンが表示されます。
googleMap.uiSettings.isZoomGesturesEnabled = trueツールバー
マーカーをタップした時に、
画面の右下に表示されるGoogleMapのユーザインターフェイスです。googleMap.uiSettings.isMapToolbarEnabled = trueズームボタンと同時に表示するとこのように表示されます。
ティルト
2本指でスワイプで視点を傾けることができます。
googleMap.uiSettings.isTiltGesturesEnabled = trueコンパス
画面左上の表示されます。
googleMap.uiSettings.isCompassEnabled = true現在地表示
ACCESS_COARSE_LOCATION
またはACCESS_FINE_LOCATION
の許可を取得しておく必要があります。googleMap.isMyLocationEnabled = true現在地ボタン
画面の右上に表示されます。
タップすると現在地にカメラと縮尺が調整されます。googleMap.isMyLocationButtonEnabled = trueカメラの移動
// アニメーションなし googleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng)) // アニメーションあり googleMap.animateCamera(CameraUpdateFactory.newLatLng(sydney))カメラの縮尺変更
val zoomValue = 14.0f // 1.0f 〜 21.0f を指定 googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoomValue))おおよその詳細レベル
1: World 5: Landmass/continent 10: City 15: Streets 20: Buildingsズームレベルの詳細はこちらを参考に
https://developers.google.com/maps/documentation/android-sdk/views
https://qiita.com/SnowMonkey/items/795779913be692c12a0bマーカー表示
val sydney = LatLng(-34.0, 151.0) googleMap.addMarker( MarkerOptions() .position(sydney) // 地図上のマーカーの位置 .title("Marker in Sydney") // マーカーをタップ時に表示するテキスト文字列 .snippet("Australian cities") // タイトルの下に表示される追加のテキスト .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) // アイコン )また、マーカーをタップしなくても吹き出しを表示することも出来る。
val marker = googleMap.addMarker( MarkerOptions() .position(latLng) .title("test") .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)) ) marker.showInfoWindow() // タップした時と同じ挙動円の描画
val latLng = LatLng(35.681236, 139.767125) // 東京駅 val radius = 1000 * 10.0 // 10km googleMap.addCircle( CircleOptions() .center(latLng) // 円の中心位置 .radius(radius) // 半径 (メートル単位) .strokeColor(Color.BLUE) // 線の色 .strokeWidth(2f) // 線の太さ .fillColor(0x400080ff) // 円の塗りつぶし色 )線の描画
googleMap.addPolyline( PolylineOptions() .add(LatLng(35.681236, 139.767125)) // 東京駅 .add(LatLng(34.7331, 135.5002)) // 新大阪駅 .color(Color.RED) // 線の色 .width(8f) // 線の太さ )2点間に距離を計算(SphericalUtil)
距離を簡単に計算するのに必要なユーティリティライブラリを追加します。
https://developers.google.com/maps/documentation/android-sdk/utility//app/build.gradledependencies { ... // add implementation 'com.google.maps.android:android-maps-utils:0.5' }検証として、
東京駅
〜新大阪駅
の緯度経度を使います。val latLngA = LatLng(35.681236, 139.767125) val latLngB = LatLng(34.7331, 135.5002) // 距離をメートル単位で返す val distance = SphericalUtil.computeDistanceBetween(latLngA, latLngB) println("AB: $distance m") println("AB: ${distance / 1000} km")出力
AB: 401709.92548952583 m AB: 401.70992548952586 km
以下は、PCのブラウザでGoogleMapで距離を測った結果です。
2点間に距離を計算(Location)
ユーティリティライブラリを入れなくても、
Location
クラスで2点間の距離を計算できます。
簡単に呼び出せるように以下のExtension
を定義しておきます。fun LatLng.distanceBetween(toLatLng: LatLng): Float { val results = FloatArray(1) try { Location.distanceBetween( this.latitude, this.longitude, toLatLng.latitude, toLatLng.longitude, results ) } catch (e: IllegalArgumentException) { return -1.0f } return results[0] }呼び出し元
val latLngA = LatLng(35.681236, 139.767125) val latLngB = LatLng(34.7331, 135.5002) // 距離をメートル単位で返す val distance = latLngA.distanceBetween(latLngB) println("AB: ${latLngA.distanceBetween(latLngB)} m") println("AB: ${latLngA.distanceBetween(latLngB) / 1000} km")
SphericalUtil
と比較した結果、こちらは少し誤差が出るようです。AB: 402483.6 m AB: 402.48358 km
まとめ
本記事で記述しているのはGoogleMapAPIの中でもごく一部です。
忘れた頃にいちいちググりにいくのが面倒なので、ひとまずまとめておきました。公式資料
https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap
http://googlemaps.github.io/android-maps-utils/javadoc/com/google/maps/android/SphericalUtil.html
- 投稿日:2019-05-21T09:54:03+09:00
いまさらWorkManager〜基本編③(繰り返し処理を実行する)〜
はじめに
この記事で説明した通り、様々な機能を活用していきましょう。
今回は、繰り返し処理を設定する方法を学習していきます。動作環境
この記事の動作環境は以下のとおりです。
- Android Studio:3.3
- Kotln:1.3.11
- Open JDK:1.8
- compileSdkVersion:28
- targetSdkVersion:28
- minSdkVersion:19
目標
繰り返して処理を実行出来るようになる!
概要
繰り返し処理を実行したい場合は、PeriodicWorkRequestクラスを利用します。
連続して実行は出来ませんが、ある一定時間(インターバル)を置いて実行が可能です。
イメージとしてはこんな感じでしょうか。実際に一定のインターバルでノーティフィケーションを表示する処理をすると下図のような実行結果になります。
実装方法
基本的な実装方法はOnetimeWorkRequestと同じですが、最後のRequestオブジェクトを作成する時に、インターバルなどの設定を行います。
PeriodicWorkRequestオブジェクトを生成するには、Builderクラスを利用します。
そのコンストラクタで、インターバルを設定します。共通の条件
インターバルの最小時間は決まっています。 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS 以上でなければなりません。
定義を確認してみると。。。public final class PeriodicWorkRequest extends WorkRequest { /** * The minimum interval duration for {@link PeriodicWorkRequest} (in milliseconds). */ public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes. 〜〜〜省略〜〜〜〜 }となっていました。
なので、最低でも 15分 以上は開ける必要があります。PeriodicWorkRequest.Builder(Class<? extends ListenableWorker> workerClass, long repeatInterval, TimeUnit repeatIntervalTimeUnit)
コンストラクタの引数
引数 値と説明 第1引数 Workerクラスのクラス情報 第2引数 インターバルの時間 第3引数 時間の単位 説明
APIの説明を読むと以下のように記述されています。
引数で指定したインターバル期間に実行するPeriodicWorkRequeクラスのオブジェクトを生成します。
PeriodicWorkRequestはインターバルの期間中に1度だけ実行されることが保証されています。
バッテリーや端末の状況によっては、その影響を受ける可能性はあります。実行する処理は15以上間隔を開ける必要があります。それ以外の条件が満たされていれば、その期間中に実行可能状態になります。また、実行する処理の時間はフレックス期間を設定し、制限することも可能です。
ということは、インターバル期間中に実行が可能なれば実行できるみたいです。
きっちり15分間とかではなく、その15分間の中で実行されるイメージみたいです。
分かりづらいですが、絵にすると下図のような感じになります。また、フレックスについては、後述します。
実装例
val periodicWork = PeriodicWorkRequest.Builder( MyWorker::class.java, 15, TimeUnit.MINUTES ).build()public PeriodicWorkRequest.Builder (Class<? extends ListenableWorker> workerClass, Duration repeatInterval)
コンストラクタの引数
引数 値と説明 第1引数 Workerクラスのクラス情報 第2引数 インターバルの時間と単位 説明
基本的には、上記に記載した「PeriodicWorkRequest.Builder(Class<? extends ListenableWorker> workerClass, long repeatInterval, TimeUnit repeatIntervalTimeUnit)」と同じです。
違いとしては、インターバルの時間をDurationクラスのメソッドを使用して、時間と単位を一度に設定できることです。実装例
val periodicWork = PeriodicWorkRequest.Builder( MyWorker::class.java, Duration.ofMinutes(15) ).build()PeriodicWorkRequest.Builder(Class<? extends ListenableWorker> workerClass, long repeatInterval, TimeUnit repeatIntervalTimeUnit, long flexInterval, TimeUnit flexIntervalTimeUnit)
コンストラクタの引数
引数 値と説明 第1引数 Workerクラスのクラス情報 第2引数 インターバルの時間 第3引数 インターバルの時間の単位 第4引数 フレックスの時間 第5引数 フレックスの時間の単位 説明
インターバルの時間内にフレックス時間を設け、そのフレックス時間内に処理が実行されます。
フレックス時間は下記で計算された時間からインターバル終了までとします。フレックス開始時間 = インターバル時間 ー フレックス時間
また、インターバルの時間は最低でもPeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS(15分以上) 以上、フレックスの時間は、最低でもPeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS(15以上)に設定しなければなりません。
これだけだといまいちピンとこなかったので、図にしてみました。
要するに、インターバルの中にフレックスがありそのフレックスの時間の間で処理を実行します。
フレックス時間の終了はインターバルの終了時間と同じであるため、インターバルの最初の方で処理が実行されることはありません。実装例
val periodicWork = PeriodicWorkRequest.Builder( MyWorker::class.java, 20, TimeUnit.MINUTES, // インターバルの時間 15, TimeUnit.MINUTES // フレックスの時間 ).build()PeriodicWorkRequest.Builder(Class<? extends ListenableWorker> workerClass, Duration repeatInterval, Duration flexInterval)
コンストラクタの引数
引数 値と説明 第1引数 Workerクラスのクラス情報 第2引数 インターバルの時間と単位 第3引数 フレックスの時間と単位 説明
基本的な内容はPeriodicWorkRequest.Builder(Class<? extends ListenableWorker> workerClass, long repeatInterval, TimeUnit repeatIntervalTimeUnit, long flexInterval, TimeUnit flexIntervalTimeUnit)と同じです。
違いとしては、Durationクラスを利用して、時間と単位を一気に設定します。実装例
val periodicWork = PeriodicWorkRequest.Builder( MyWorker::class.java, Duration.ofMinutes(20), // インターバルの時間 Duration.ofMinutes(15) // フレックスの時間 ).build()動作確認をした際のコード
最後に、動作確認した際のコードをすべて掲載します。
AndroidManifest.xmlはプロジェクトのテンプレートから変更していないため、掲載はしません。build.gradleapply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 defaultConfig { applicationId "jp.co.casareal.workmanagerperiodicwork" minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' def work_version = "1.0.0" implementation "android.arch.work:work-runtime-ktx:$work_version" }MyWork.ktpackage jp.co.casareal.workmanagerperiodicwork import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.os.Build import android.support.v4.app.NotificationCompat import androidx.work.Worker import androidx.work.WorkerParameters import java.text.SimpleDateFormat class MyWorker(cxt: Context, params: WorkerParameters) : Worker(cxt, params) { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // カテゴリー名(通知設定画面に表示される情報) val name = "通知のタイトル的情報を設定" // システムに登録するChannelのID val id = "casareal_chanel" // 通知の詳細情報(通知設定画面に表示される情報) val notifyDescription = "この通知の詳細情報を設定します" private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") companion object { var nid = 1; } init { // Channelの取得と生成 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationManager.getNotificationChannel(id) == null val mChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) mChannel.apply { description = notifyDescription } notificationManager.createNotificationChannel(mChannel) } } override fun doWork(): Result { val notification = NotificationCompat.Builder(applicationContext, id).apply { setContentText("${nid}回目のメッセージ:${simpleDateFormat.format(System.currentTimeMillis())}") setSmallIcon(R.drawable.ic_launcher_background) } notificationManager.notify(MyWorker.nid, notification.build()) MyWorker.nid++ return Result.success() } }MainActivity.ktpackage jp.co.casareal.workmanagerperiodicwork import android.os.Build import android.os.Bundle import android.support.v7.app.AppCompatActivity import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import kotlinx.android.synthetic.main.activity_main.* import java.time.Duration class MainActivity : AppCompatActivity() { private val manager = WorkManager.getInstance() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) buttonStart.setOnClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val periodicWork = PeriodicWorkRequest.Builder( MyWorker::class.java, Duration.ofMinutes(15) ).build() val operation = manager.enqueue(periodicWork) } } } }activity_main.xml<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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" tools:context=".MainActivity"> <Button android:id="@+id/buttonStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout>まとめ
繰り返し実行する場合は、インターバルの時間を設定することが大事なようです。
そこで、インターバルの時間を開始してから実行を遅らせるためにフレックスの時間を利用することによって適切なタイミングでの実行が可能になっていることがわかりました。