10
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

AnkoでXMLでのアレをやりたい

Ankoは、XMLに慣れた人にも違和感なく使用できます。
今までXMLで書いていたものをAnkoではどう書くのかを見ていきましょう。
途中でAnkoの実装に触れた部分が少しありますが、今回は軽い説明で勘弁してください。

Viewの追加

Anko
verticalLayout {
    textView {
        text = "Hello, Anko!"
    }
}

{ } の中に入れ子に書いていくことでViewを追加していくことができます。
XMLとほぼ変わらない感覚で書けることが分かると思います。
addViewを呼ぶ必要はありません。
ちなみに、verticalLayoutはorientationにverticalを指定したLinearLayoutです。

このverticalLayoutの正体はActivityの拡張関数です。

CustomViews.kt
inline fun Activity.verticalLayout(): LinearLayout = verticalLayout({})
inline fun Activity.verticalLayout(init: _LinearLayout.() -> Unit): LinearLayout {
    return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, init)
}
Custom.kt
inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, init: T.() -> Unit): T {
    val view = factory(this)
    view.init()
    AnkoInternals.addView(this, view)
    return view
}

少々分かりづらいかと思いますが、
1. verticalなLinearLayoutのインスタンスを作成して
2. 初期化を行い
3. addView()する
という流れになっています。

UIの初期化に使われるのがカッコの中身の部分です。

Anko
verticalLayout {
    // ここの中身が初期化時に呼ばれる
    textView {
        text = "Hello, Anko!"
    }
}

初期化関数をカッコの中身にして渡す部分は、拡張関数を引数に取れるというKotlinの言語機能を利用して作られています。
詳しくは以下の記事が参考になりました。

jetBrains/anko触ってたらよくわからない実装を見たので調べたらKotlinで引数に拡張関数を取る関数を作れる事がわかった

属性の定義

XML
<TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="This is XML layout."
        android:background="@color/black"
        android:textColor="@color/white"
        />
Anko
textView {
    text = "Hello, Anko!"
    backgroundColor = Color.BLACK
    textColor = Color.WHITE
}

Anko DSLでは色の指定時にColorの定数を使用できます。
また、XMLのような名前空間の指定は不要です。

text = "Hello, Anko"の部分はsetterのシンタックスシュガーとなっており、setText()しています。

textColor = Color.WHITEの部分は拡張プロパティとなっています。
実際にはsetTextColor()を呼んでいますが、プロパティへの代入のように記述できます。

Properties.kt
var android.widget.TextView.textColor: Int
    get() = throw AnkoException("'android.widget.TextView.textColor' property does not have a getter")
    set(v) = setTextColor(v)

イベントリスナーの追加

Anko
button("Show Toast") {
    onClick { toast("Click!") }
}

イベントリスナーの設定もViewの追加と同じような表記で書くことができます。
上記のように書くとonClick時にToastを表示することができます。
こちらも拡張関数で実装されており、実際にはsetOnClickListener()を呼んでいます。

Listeners.kt
fun android.view.View.onClick(l: (v: android.view.View?) -> Unit) {
    setOnClickListener(l)
}

LayoutParams

XML
<TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="This is XML layout."
        android:background="@color/black"
        android:textColor="@color/white"
        android:layout_marginTop="16dip"
        />
Anko
textView {
    text = "Hello, Anko!"
    backgroundColor = Color.BLACK
    textColor = Color.WHITE
    lparams {
        topMargin = dip(16)
    }
}

marginなどのLayoutParamsに属するものは lparams {} の中で指定します。
dipの指定はdip(16)のように書くことができます。
以下のようなContextの拡張関数が定義されており、これを使用することで簡潔な表現が可能になっています。

Dimensions.kt
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()

先ほどのレイアウトと同じものを以下のように表記することもできます。

Anko
textView {
    text = "Hello, Anko!"
    backgroundColor = Color.BLACK
    textColor = Color.WHITE
}.lparams {
    topMargin = dip(16)
}

公式のドキュメントではこちらの表記の方が普通のようです。

XMLのinclude

既存のXMLをAnkoの中にincludeするには下のように書きます。

Anko
include<RelativeLayout>(R.layout.xml_layout) {

}

これもやっぱり正体は拡張関数です。

CutomViews.kt
inline fun <T: View> ViewManager.include(layoutId: Int): T = include(layoutId, {})
inline fun <T: View> ViewManager.include(layoutId: Int, init: T.() -> Unit): T {
    @Suppress("UNCHECKED_CAST")
    return ankoView({ ctx -> ctx.layoutInflater.inflate(layoutId, null) as T }) { init() }
}

ctxはContextです。普通にXMLをinflateしているだけですね。

相対レイアウトを指定する

RelativeLayoutの中で、他のViewとの相対位置を指定するときにはrightOf()などの ~~Of()関数を使用します。

Anko
class MainActivityUi() : AnkoComponent<MainActivity> {

    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {

        relativeLayout {

            val xmlLayout = include<RelativeLayout>(R.layout.xml_layout) {

            }

            textView {
                text = "Hello, Anko!"
                backgroundColor = Color.BLACK
                textColor = Color.WHITE
            }.lparams {
                topMargin = dip(16)
                rightOf(xmlLayout)
                alignParentRight()
            }
        }
    }
}

idを指定することもできますが、この場合にはView.generateViewId()などを使用して動的に一意のidを作ります。

Anko
class MainActivityUi() : AnkoComponent<MainActivity> {

    val xmlLayoutId = View.generateViewId()
    var xmlLayout : RelativeLayout? = null

    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {

        relativeLayout {

            xmlLayout = include<RelativeLayout>(R.layout.xml_layout) {
                id = xmlLayoutId
            }

            textView {
                text = "Hello, Anko!"
                backgroundColor = Color.BLACK
                textColor = Color.WHITE
            }.lparams {
                topMargin = dip(16)
                rightOf(xmlLayoutId)
                alignParentRight()
            }
        }
    }
}

idを作成する方法は ids.xmlにidを指定する方法でも構いません。

カスタムビューを使用する

自作したViewも下の2行を追加すれば、Anko DLSの中に自然に混ぜることができます。

inline fun ViewManager.customView() = customView() {}
inline fun ViewManager.customView(init: CustomView.() -> Unit) = ankoView({ CustomView(it) }, init)
Anko
verticalLayout {
    customView {
        textColor = Color.RED
    }
}

上記は0.8.3までの方法で、0.9からは以下のようになります。

inline fun ViewManager.customView(theme: Int = 0) = customView(theme) {}
inline fun ViewManager.customView(theme: Int = 0, init: CustomView.() -> Unit) = ankoView({ CustomView(it) }, theme, init)

Fragmentを追加する

XMLでの<fragment>タグのようにAnkoの中でFragmentを追加したいときは以下のように書けます。

参考: <fragment> tag equivalent in anko DSL

Anko
supportFragmentManager.beginTransaction().add(this.id,com.myapp.MyFragment()).commit()

しかし、この方法でやったところPreviewが表示されなくなりました。

Ankoの中に直接書かずに、idを指定したViewに対して外からaddしたときにはPreviewを表示することができました。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val ui = MainActivityUi()
        ui.setContentView(this)

        supportFragmentManager.beginTransaction().add(ui.framgentId, com.myapp.MyFragment()).commit()
    }
}

XMLではできないこと

拡張関数を使用する

これまで見てきましたようにAnkoではKotlinの拡張関数の機能を生かして簡潔な表記を実現していました。
自分たちで追加した拡張関数も同じように利用することができます。
以下はPicassoで画像を読み込むようにする例です。

fun ImageView.loadImage(context: Context, imageUrl: String, error: Int) {
    Picasso.with(context).load(imageUrl).error(error).into(this)
}

imageView = imageView {
    loadImage(ctx, "画像のURL", R.drawable.error)
}

このように気軽に機能を追加できることが Anko DSL = Kotlinのコードで書くことのメリットの一つだと思います。


次の記事はListViewでの使用例について書こうと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
10
Help us understand the problem. What are the problem?