初めてのkotlin

kotlinによるAndroidアプリ開発での気付き

正規表現

ここでは正規表現の文法については扱いません。

Regexのメソッドの使い方にいつも悩むので、 その覚書としてのメモになります。

perl正規表現は、$str=~ /abc/と書きさえすれば、 正規表現を活用するあらゆる処理が可能となりますが、 kotlinだとなかなかそうは行きません。

判定だけを行いたい時、 一致箇所が欲しい時、 複数の一致箇所を全て抽出したい時で、 求める戻り値が違うので、メソッドを使い分けなければなりません。

また、部分一致なのか($str=~ /abc/)、 全体を一致させたいのか($str=~ /^abc$/s)でもメソッドが分かれていて、 しかも、メソッド名に法則性を感じないので、 これらの使い分けでいつも悩みます。

なので、整理のために一覧にしました。

perlなど、正規表現を多用するスクリプト系から来た人は、 ^ $ \A \zなどのメタ文字が有効なようですので、 部分一致だけ覚えればいいと思いますが、 一応全て書きます。

メソッド 戻り値
部分一致系
containsMatchIn Boolean
find MatchResult
findAll Sequence<MatchResult>
全体一致系
matches Boolean
matchEntire MatchResult
分割
split List<String>
置換
replaceFirst String
replace String

拡張子の取り除き方

androidではあまり使う機会はないかもしれませんが、 応用できるので、使用経験があまりない方も知っておいて損はないかと思います。

拡張子を取り除く方法について、 ネットを調べると、splitやlastIndexOfを用いた方法がたくさんヒットするのですが、 これって実は、ディレクトリに拡張子が付いていると正しく動作しないことがあります。

なので、別のアプローチで拡張子を除外してみました。

    val regex = Regex("""\.[^./\\]+$""")
    println("""aaa\bbb.txt""".replaceFirst(regex,""))
    println("""aaa\bbb.ccc.txt""".replaceFirst(regex,""))
    println("""aaa.dir\bbb.txt""".replaceFirst(regex,""))
    println("""aaa.dir\bbb""".replaceFirst(regex,""))
    println("""aaa\bbb""".replaceFirst(regex,""))

実行結果です。

aaa\bbb
aaa\bbb.ccc
aaa.dir\bbb
aaa.dir\bbb
aaa\bbb

ファイルに拡張子がなくても、ディレクトリに拡張子があっても、正しく動作しました。

正規表現って、perlのプログラムではとてもたくさん登場する便利な機能なのですが、 JavaC++のサンプルでは、あまり登場しませんね。

私もRegexはどのメソッド使えばいいか覚えられていないので、使い辛いです。

正規表現の説明

\. ドット文字(ドットのみだと一文字と言う意味になるので)

\\ バックスラッシュ(または円サイン)

[^a-z]+ 指定された文字を含まない文字列と言う意味

$ 行末(または文末)

\.[^./\\]+$ ディレクトリを含まない最後のドット以降と言う意味

java.langなどjava系のクラスが利用できない

今回、バッチプログラムを書くにあたり、 せっかくなのでバッチでもkotlinを使ってみようと、 IntelliJ IDEAでkotlinのプロジェクトを作ってみたら、 タイトルのとおり、何故かjava.~に由来するクラスが一切使えない状態でした。

build.gradleファイルを触ってみたり、 Mavenでモジュールを漁ってみたりしたけどダメ。

ネットで調べてそれらしいものを色々チェックしてみましたが、 どれも効果がありませんでした。

で、ふとIntelliJ IDEAはどこにインストールされたJavaを見てるんだろうと思い、 いっそ新しいSDKを入れてみようと言うことで、

ファイル→プロジェクト構造 →SDK→+Download JDK...

にてOracle OpenJDKをダウンロードし、

プロジェクト構造→プロジェクト→プロジェクトSDK:

の欄をダウンロードしたJDKに変えたら、 無事解決しました^^

単にIntelliJ IDEAと一緒にインストールしたJDKに不足があっただけのようですね。 たぶん、インストール時の設定に問題があったんだと思うのですが、 それを確認する画面って無いのかな?

バナー広告を設置する

Androidアプリを作れば、やはり収益に結びつけたくなるのが人情。

と言うわけで、バナー広告を設置します。

但し、ここではサンプルIDを用いて広告が表示されるのを確認するまでとします。

実際にアプリを公開する際には、 自身でIDを発行し、それに差し替える必要がありますのでご注意ください。

build.gradle

allprojects {
    repositories {
        ・・・
        google()
    }
}

google()allprojectsセクションに書かれていることを確認します。 (無ければ追加)

app/build.gradle

dependencies {
    ・・・
    implementation 'com.google.android.gms:play-services-ads:19.3.0'
}

Mobile Ads SDKを組み込みます。

main\AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest ・・・
    >

    <application ・・・
    >
        ・・・
        <!-- Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>

    </application>

    <!-- ネット接続の許可 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

</manifest>

AdMob用アプリIDの設定と、ネットワーク接続の許可を追加します。

※ca-app-pub-3940256099942544~3347511713は、テスト用のアプリIDです。

res\layout\activity_main.xml

    <com.google.android.gms.ads.AdView
        xmlns:ads="http://schemas.android.com/apk/res-auto"
        android:id="@+id/adView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        ads:adSize="BANNER"
        ads:adUnitId="ca-app-pub-3940256099942544/6300978111" />

ActivityにAdView(広告ユニット)を設置する。 ca-app-pub-3940256099942544/6300978111はテスト用の広告ユニットIDです。

複数のActivityでアプリが動作している場合は、Activity毎に広告ユニットIDを取得すること。

ActivityMain.kt

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ・・・

        //AdMob初期化処理
        MobileAds.initialize(this) {}    //起動時に最初のActivityで1回だけ実行
        val mAdView = findViewById(R.id.adView) as AdView
        val adRequest = AdRequest.Builder().build()
        mAdView.loadAd(adRequest)

広告ユニットを起動する。 MobileAds.initialize(this) {}については、起動時に最初に表示されるActivityでのみ実行する。

詳細

developers.google.com

developers.google.com

キーボードを隠す

EditTextにフォーカスを当てるとソフトウェアキーボードが表示されます。

でも、そのキーボードは、入力が完了しても表示されたままなのですよね。

で、ネットで調べるとonFocusChangedイベントで非表示にすればいいと書かれたものが多いのですが、 フォーカスが当たるものがEditText以外にないと、上手く行きません。

試しにボタンにフォーカスが当たるようにしてみたのですが、 そうすると今度はボタンがダブルタップしないと反応しなくなると言う副作用が出ました(/_;)

なので、別の方法で非表示にすることにしました。

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        if (ev?.action == MotionEvent.ACTION_DOWN) {
            var v = currentFocus
            if (v is EditText) {
                var outRect = Rect()
                v.getGlobalVisibleRect(outRect)
                if (!outRect.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
                    v.clearFocus()
                    hideSoftKeyboard()
                }
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    //キーボードを消す
    private fun hideSoftKeyboard() {
        val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(oLaneNo.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
    }

キーボードを消す処理の本体はhideSoftKeyboardメソッドです。

キーボードを消すための判定は、dispatchTouchEventイベントで行います。

このイベントは、タッチイベントが発生した際に最初に発火します。

中でやってるのは、直前にフォーカスが当たっているViewがEditTextの時は、 タッチイベントがEditTextの外(!outRect.contains)で発火した場合に限り、 キーボードを消すと言う処理です。

この処理により、入力後にEditText以外を触ることで、キーボードが消えます。

リソースが見えなくなったら

Javaのコードをkotlinに変換した時によく遭遇するのですが、 リソースデータが見えなくなることがあります。

そんな時は、おかしなファイルがimportされていないかどうかをチェックしてください

import android.R

この行を消せば直ります。

ディスプレイ周りの各種サイズの取得

ステータスバーやナビゲーションバーなど、 画面関係のサイズや座標を取得します。 activity引数には、Activityから呼び出す時はthisを指定すればOKです。

ディスプレイサイズの取得
fun getRealDisplaySize(activity: Activity): Point {
    val outSize = Point()
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
        activity.display?.getRealSize(outSize)
    else
        activity.windowManager.defaultDisplay.getRealSize(outSize)
    return outSize
}

物理的な画面サイズです。

アプリケーションエリアのサイズ
fun getApplicationDisplaySize(activity: Activity): Point {
    return Point(
        activity.resources.displayMetrics.widthPixels,
        activity.resources.displayMetrics.heightPixels
    )
}

ディスプレイサイズからナビゲーションバーを除いた大きさです。

アプリケーション表示エリア
fun getApplicationVisibleArea(activity: Activity): Rect {
    val outRect = Rect()
    activity.window.decorView.getWindowVisibleDisplayFrame(outRect)    //アプリケーション表示エリア(バーの状態によって変化する)
    return outRect
}

ステータスバーとナビゲーションバーを除いた範囲の座標です。

この値は、フルスクリーンモードでは画面全体の座標に変化します。

ステータスバーの高さ
fun getStatusBarHeight(activity: Activity): Int {
    val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
    return if (resourceId > 0)
        activity.resources.getDimensionPixelSize(resourceId)
    else {
        val rect = getApplicationVisibleArea(activity)
        rect.top
    }
}

テストした範囲では、フルスクリーンモードでも正しい値が取得できています。

タイトルバー(ActionBar)の高さ
fun getActionBarHeight(activity: Activity): Int {
    val styledAttr: TypedArray = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.actionBarSize))
    val px = styledAttr.getDimension(0, 0f).toInt()
    styledAttr.recycle()
    return px
}

ToolBarで機能するかどうかは確認していません。

ナビゲーションバーの高さ
fun getNavigationBarHeight(activity: Activity): Int {
    val real = getRealDisplaySize(activity)     //スクリーンサイズ
    val app = getApplicationDisplaySize(activity)     //アプリケーション表示エリア
    return real.y - app.y
}