2016年Android界隈で話題になったと個人的に思うこと その2


qnote Advent Calendar 2016 8日目を担当いたします。
チャン・ジュン(日本人)です。
よろしくどうぞ

さて、2016年はズンドコキヨシの年でしたね。
しかしながらGetWildはAdventCalendarがあるのにズンドコキヨシにはAdventCalendarが無いようです。

そんなわけで、今回はズンドコキヨシアプリをKotlinで書くお話です

ちなみに、ズンドコキヨシについてはこちら → ズンドコキヨシまとめ

まずは準備から

ということで、まずはKotlinを使うために新規プロジェクト作成 → Kotlin化ですが、
プロジェクトのKotlin化は2日目の記事を御覧ください。

さて、今回はbuild.gradleに色々追加します

build.gradle
apply plugin: 'kotlin-android-extensions'

↑kotlinExtensionsが利用可能になります。

build.gradle
android {
    ...

    dataBinding {
        enabled = true
    }
}

dependencies {
    ...

    kapt 'com.android.databinding:compiler:1.0-rc5'
}

kapt {
    generateStubs = true
}

↑kotlinでdataBindingが利用可能になります。

build.gradle
dependencies {
    ...

    compile 'com.android.support:appcompat-v7:25.0.0'
    compile "com.android.support:cardview-v7:25.0.0"
    compile 'com.android.support:recyclerview-v7:25.0.0'
}

↑サポートライブラリを使う場合には必要です
今回はカードビューを使う機会がなさすぎたので使ってみようという感じです。

Let's ズンドコ

ズンドコDataBinding

zundoko_card_layout.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="zundokoObj" type="com.chanjun.zundoko.Zundoko"/>
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:cardUseCompatPadding="true"
        app:cardCornerRadius="10dp">

        ...

    </android.support.v7.widget.CardView>
</layout>

ソースコードばかり長くなってしまうのでCardViewの中身は省略しましたがズンドコ表示を行うTextViewが入ります。
式が使えるのでこんな感じに記述すればOKです。

android:text="@{zundokoObj.zundokoText}"

RecyclerViewについては特に特別な実装はないので割愛して次はAdapterです。
onCreateViewHolderでバインディングクラスを作るのは説明不要かと思いますが、ズンドコオブジェクトのセットはRecyclerView内でViewの使い回しが発生した時にデータが更新されなくなってしまうのでonBindViewHolderで行うようにしてください。

ZundokoAdapter.kt
class ZundokoAdapter(val context: Context) : RecyclerView.Adapter<ZundokoViewHolder>() {
    override fun onBindViewHolder(holder: ZundokoViewHolder?, position: Int) {
        holder?.layoutBinding?.zundokoObj = zundokoList.get(position)
    }

    ...

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ZundokoViewHolder {
        return ZundokoViewHolder(ZundokoCardLayoutBinding.inflate(LayoutInflater.from(context), parent, false))
    }

}

ZundokoViewHolderでは、上述した使い回しのためにバインディングクラスを保持するようにします。
コンストラクタから変数を代入したりしなくていいので1行で済みます。
{}も不要です。
別ファイルにする必要も無いのでAdapterと一緒にしておけば良いかと思います。

ZundokoViewHolder.kt
class ZundokoViewHolder(val layoutBinding: ZundokoCardLayoutBinding) : RecyclerView.ViewHolder(layoutBinding.root)

ZundokoオブジェクトはさらにKotlin独特の書き方になりますね。
特にfield = valueという書き方はわかりやすくて好みです。

Zundoko.kt
class Zundoko() :BaseObservable() {
    @get:Bindable
    var zundokoText: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.zundokoText)
        }

    @get:Bindable
    var zundokoResult: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.zundokoResult)
        }
}

ズンドコロジック

ズンドコ文字列の生成は下記のメソッドで行います。
addZundokoObj()で生成されたズンドコオブジェクトは即座にリストに追加され、画面に表示されます。
その後zundokoLoop()で文字列のチェックを行いながら文字列が追加されていきます。

最重要ポイントは500msの遅延で表示されるズンドコを眺めながら、
あのメロディを脳内で補完することです!

/**
 * ズンドコオブジェクト追加メソッド
 */
fun addZundokoObj() {
    val zundokoObj = Zundoko()
    zundokoObj.zundokoText = getZundokoText()
    zundokoList.add(zundokoObj)
    zundokoView.scrollToPosition(zundokoList.maxPosition())

    //リズム良く表示するためのディレイ
    Handler().postDelayed({
        zundokoObj.zundokoText += "!"
        zundokoLoop()
    }, 500)
}

fun zundokoLoop() {
    //リズム良く表示するためのディレイ
    Handler().postDelayed({
        var zundokoObj = zundokoList.get(zundokoList.maxPosition())

        if (checkZundokoFinal(zundokoObj.zundokoText)) {
            zundokoObj.zundokoResult = successText
            zundokoList.add(zundokoList.maxPosition(), zundokoObj)
            return@postDelayed
        }

        if (checkZundoko(zundokoObj.zundokoText)) {
            zundokoObj.zundokoText += getZundokoText()
        } else {
            zundokoObj.zundokoResult = failureText
            addZundokoObj()
            return@postDelayed
        }
        zundokoList.set(zundokoList.maxPosition(), zundokoObj)
        zundokoLoop()
    }, 500)
}

ズンドコチェック

さて、肝心のズンドコチェックメソッドですが、今回はTextUtils.equals(a, b)とか書くのはなんかやだなぁというだけの理由でパターンマッチで行いました。

最終的にsuccessTextを出すか確認するためにパターン文字列は()でグループ化し、5グループあるパターンでマッチしたかをチェックしています。
そして、var regex = "($zun!)"と記述することで変数zunを""内で扱うことができ、
var regex = "(" + zun + "!)"とか書かなくていいので快適です。

また、ズンドコパターン生成のループではレンジを利用しています。
2..zundoko.length / 2は演算子オーバーロードなので2.rangeTo(zundoko.length / 2)と表すことができます。

val zun = "ズン"
val doko = "ドコ"

fun checkZundoko(zundoko: String): Boolean {
    return zundokoMatcher(zundoko).find()
}

fun checkZundokoFinal(zundoko: String): Boolean {
    val m = zundokoMatcher(zundoko)
    return if (m.find())
        m.groupCount() == 5
    else
        false
}

fun zundokoMatcher(zundoko: String): Matcher {
    var regex = "($zun!)"
    for(i in 2..zundoko.length / 2) {
        if (i == 5)
            regex += "($doko)"
        else
            regex += "($zun)"
    }
    val pattern = Pattern.compile(regex)
    return pattern.matcher(zundoko)
}

まとめ

コード書くよりまとめるのに時間がかかりました