Takuji->find;

株式会社はてなでアプリケーションエンジニアやってます、技術的な記事を書いているつもり

RecyclerView.ItemDecorationについて #関モバ

こんばんは、最近Androidエンジニアを名乗っていいのか悩ましいid:takuji31です。

今日はRecyclerViewのItemDecorationについて簡単な使い方をまとめてみました。

なお、この記事の一部は本日開催の関西モバイルアプリ研究会#20で発表された内容です。

RecyclerView.ItemDecoration とは

RecyclerViewの各アイテムに対して何らかの装飾を施す仕組みです。

代表的なのは罫線を引いてくれる(Support Library25.0.0でようやく追加された)DividerItemDecorationやアイテムのDnDやスワイプが行えるようになるめっちゃ便利なItemTouchHelperがあります(というより公式で提供されている子クラスはこの2つだけ)。

色んなRecyclerViewのアイテムに対して簡単な装飾を施したいのなら、レイアウトで頑張るよりこちらでやる方がよいかと思います。

メソッド

getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)

指定したViewが本来レイアウトした位置からどれくらいズレるのかを指定するメソッドです。

最後の state がないメソッドがありますが、これは非推奨です。

view が対象のView、 outRect がアイテムのオフセットを返すための Rect です、 outRectleft|top|right|bottom に直接値をセットします。

onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)

装飾の描画時に呼ばれるメソッドです。呼ばれるタイミングは各アイテムの内容が描画される前です。

最後の state がないメソッドがありますが、これは非推奨です。

c はRecyclerView全体のCanvasです、各要素に対して呼ばれるのではなく、描画ごとに1回だけ呼ばれます。

このメソッドで装飾を描画することになります。

onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)

基本的には onDraw と同じですが、こちらは呼び出されるタイミングがアイテムの内容が描画された後です。

最後の state がないメソッドがありますが、これは非推奨です。

アイテムに重ねて描画したい場合に使うとよさそうです。

使い方

基本的には ItemDecoration を継承したクラスを作って、getItemOffsetonDraw もしくは onDrawOver (あるいは両方)を実装してやるとよいです。

使う時はRecyclerView.addItemDecoration(RecyclerView.ItemDecoration decor) を呼んで装飾を追加します。

今回はiOSUITableViewCell にある accessoryType のようなものを作ってみました。

AccessoryTypeを定義する

雑にenumを定義しました、とりあえず2タイプだけサポート。

enum class AccessoryType(@DrawableRes val resId: Int) {
    NONE(0),
    DISCLOSURE_INDICATOR(R.drawable.ic_chevron_right_black_24dp),
    CHECK_MARK(R.drawable.ic_done_black_24dp),
}

Adapterを作る

文字列と AccessoryType を持つ Pair のリストを持つだけの Adapter を作りました。

ポイントは ViewHolder.itemViewsetTagAccessoryType を渡していること。

ViewRecyclerView から ViewHolder を取り出すことはできますが、これだと ViewHolder の型に ItemDecoration が依存してしまうので、Tagで渡しています。

class Adapter(var items: List<Pair<String, AccessoryType>>) : RecyclerView.Adapter<ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflator = LayoutInflater.from(parent.context)
        return ViewHolder(view = inflator.inflate(R.layout.recyler_row_simple_textview, parent, false))
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val (text, accessoryType) = items[position]
        holder.textView.text = text
        holder.textView.setOnClickListener {
            Log.d(javaClass.simpleName, "clicked")
        }
        holder.itemView.setTag(R.id.tagAccessoryType, accessoryType)
    }
}

ItemDecorationを作る

今回は AccessoryType.NONE 以外の時に右側にいい感じに装飾を表示することにしました。

ItemDecoration 全体のコードは↓の通り

class AccessoryItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
    val horizontalMargin by lazy {
        context.resources.getDimensionPixelOffset(R.dimen.accessory_horizontal_margin)
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val accessoryType = view.getTag(R.id.tagAccessoryType) as? AccessoryType
        if (accessoryType != null) {
            if (accessoryType != AccessoryType.NONE) {
                val drawable = ContextCompat.getDrawable(view.context, accessoryType.resId)
                outRect.right = drawable.intrinsicWidth + horizontalMargin  * 2
            }
        }
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

        val childCount = parent.childCount
        for (i in 0..childCount - 1) {
            val child = parent.getChildAt(i)
            val accessoryType = child.getTag(R.id.tagAccessoryType) as? AccessoryType
            if (accessoryType != null && accessoryType != AccessoryType.NONE) {
                val drawable = ContextCompat.getDrawable(child.context, accessoryType.resId)
                val bounds = Rect()
                parent.getDecoratedBoundsWithMargins(child, bounds)
                val left = bounds.right - horizontalMargin - drawable.intrinsicWidth
                val right = left + drawable.intrinsicWidth
                val top = bounds.top + ((bounds.bottom - bounds.top - drawable.intrinsicHeight) / 2) + Math.round(ViewCompat.getTranslationY(child))
                val bottom = top + drawable.intrinsicHeight
                drawable.setBounds(left, top, right, bottom)
                drawable.draw(c)
            }
        }
    }
}

getItemOffsets

ViewHolder でセットした AccessoryType のTagを取得して、修飾に使うDrawableとmargin分だけ右側にoffsetを設定しています。

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    val accessoryType = view.getTag(R.id.tagAccessoryType) as? AccessoryType
    if (accessoryType != null) {
        if (accessoryType != AccessoryType.NONE) {
            val drawable = ContextCompat.getDrawable(view.context, accessoryType.resId)
            outRect.right = drawable.intrinsicWidth + horizontalMargin  * 2
        }
    }
}

onDrawOver

子の View を1つずつ取得して、それぞれ必要に応じて Drawable を描画しています。

parent.getDecoratedBoundsWithMargins(child, bounds) でそのViewの装飾込みの境界を表すRectが取得できます。

ViewCompat.getTranslationY(child) でViewのtranslationYが取得できるので、この分描画位置をずらす必要があります。

あとは DrawableCanvas に描画するだけです。

override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

    val childCount = parent.childCount
    for (i in 0..childCount - 1) {
        val child = parent.getChildAt(i)
        val accessoryType = child.getTag(R.id.tagAccessoryType) as? AccessoryType
        if (accessoryType != null && accessoryType != AccessoryType.NONE) {
            val drawable = ContextCompat.getDrawable(child.context, accessoryType.resId)
            val bounds = Rect()
            parent.getDecoratedBoundsWithMargins(child, bounds)
            val left = bounds.right - horizontalMargin - drawable.intrinsicWidth
            val right = left + drawable.intrinsicWidth
            val top = bounds.top + ((bounds.bottom - bounds.top - drawable.intrinsicHeight) / 2) + Math.round(ViewCompat.getTranslationY(child))
            val bottom = top + drawable.intrinsicHeight
            drawable.setBounds(left, top, right, bottom)
            drawable.draw(c)
        }
    }
}

スクリーンショット

f:id:takuji31:20161128184829p:plain

このコードの問題点

  • Offsetを指定してる分選択した時の背景描画が小さくなる
  • もちろんdrawしているだけなので、その装飾をタップできたりはしない

今回は分かりやすい例を出すためにこんな感じにしましたが、こういうのはViewに直接レイアウトした方がいいかもしれません。

まとめ

  • getItemOffsets でoffsetを決める
  • onDrawonDrawOver で描画する
  • RecyclerView.addItemDecoration() して使う

今回のサンプルコードはこちら

github.com

Kotlin1.1の新機能についてざっと眺めてみた(M1-M3対応) #kansaikt

こんにちは、Kotlinエバンジェリストになりたいid:takuji31です。

先日EAPでM3が公開されたKotlin1.1の新機能について、あまりちゃんと把握していなかったのでM1から見直してみました。

なお、この内容の一部は Kansai.kt #2で発表されました。

各タイトルは公式ブログから

新機能

Coroutines

他の言語にある async/await とか generator/yield ですね。

非同期や反復処理をモダンな感じに書けるようになります。

Kotlin 1.1最大の新機能とも言えます。

使い方

coroutineは今はKotlin本体と分離されていて、別の依存を追加することで使えます。

compile 'org.jetbrains.kotlinx:kotlinx-coroutines-generate:0.1-alpha-2'
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-async:0.1-alpha-2'
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx:0.1-alpha-2'

それぞれ coroutines-asyncasync/awaitcoroutines-generategenerator/yieldcoroutines-rx が RxJavaのObservableをawaitできるライブラリーです。

async/await は誤解を恐れない書き方をすると、非同期な処理を実行したいメソッド内で async 関数を呼び、その中の非同期にやりたい処理をawait()で囲ってやります。 Observable の場合は async/await ではなく asyncRx/await(First|Last|Single) を使います。 await の引数は CompletableFuture なのでAndroidだと7.0以降(API24)、Javaだと8以降でしか使えません、残念。

めっちゃ雑なサンプル

fun asyncOperation(i: Int): CompletableFuture<Int> = CompletableFuture.supplyAsync {
    Thread.sleep(1000)
    i
}
val future = async<Int> {
    val i = await(asyncOperation(1))
    i
}
Log.d("MainActivity", "i:" + future.get())

雑に使ってみた感想だと、CompletableFutureをいい感じに使う機能でFutureあまり使わない勢からするとさほど利点はなさそうというイメージ、他言語のものほどはまだ洗練されてなさそうでした、まぁalpha2だしこんなもんでしょう。

Type aliases

型に別名を付ける機能です。 今まで(String, Int) -> Unitみたいな感じで書いていて面倒なのでわざわざinterface作っていたりしたコールバックとかをいい感じに名前つけられて便利。

typealias VeryUsefulAction = (String, Int) -> Unit

個人的に1.1の今のところ出ている新機能で一番ありがたいです。

Local delegated properties & Inline properties

関数とかメソッド内で使えるプロパティーらしい、関数内での初期化を簡単に書けたりするようになりそう?

いまいち使いどころがよく分からなかった。

Scripting

Kotlinでスクリプト書けるようになったらしいです、GradleがKotlinサポートを表明したのは記憶に新しいですが、これもその機能の1つのようです。

Generic Enum values access

Genericsを使っていい感じにEnumの値にアクセスできたりするらしいです、型引数と組み合わせると便利な処理が書けるようになったりしそう。 inline なのでコンパイル時にインライン展開してくれます。

inline fun <reified T : Enum<T>> enumValues(): Array<T>
inline fun <reified T : Enum<T>> enumValueOf(name: String): T

新たな文法

Bound callable references

いわゆるmethod referenceみたいなやつですね、今までは Class.method をサポートしていたけど、新たに variable.method もサポートするとのことです。

Destructuring in Lambdas (M2)

合成値(data classとかPairみたいにvariable.component1()...componentN()で分解できるやつ)の分解を、ラムダ式でもいい感じにやってくれるようになりました。

listOf(Pair(1, 2), Pair(2,3)).map{
    (a, b) -> a + b // 3, 5
};

今までは面倒だけど変数定義し直していたりしたので、簡単に書けるようになって便利ですね。

Underscore for unused symbols (M3)

使わない戻り値を_で無視することのできる機能

ラムダ式の引数を無視したり、合成値の分解をした時に途中の値だけ捨てるのに便利ですね。

data class User(val id: Long, val name: String)
// ...
val (_, name) = user;

Underscore in numeric literals (M3)

数値リテラルの途中に_を挟めるようになりました。10000000 -> 10_000_000 みたいな感じ。

Perlで見たことあるやつって感じで桁の多い数値を表現する時に便利ですね。

その他変更

Relaxed rules for sealed classes and data classes

sealed classやdata classの定義が柔軟になったとのこと。

data class は非data classを継承できるようになっています。 もちろんopenである必要があるし、プロパティーもopenじゃないと継承できません。

open class User(open val id: Long, open val name: String)

data class AdminUser(override val id:Long, override val name: String, val role: String) : User(id = id, name = name)

これは結構ありがたい。

sealed classは今までは必ず子クラスは親の中で定義する必要があった(sealed A を継承する場合は A.B)のですが、これが不要になり、同じファイル内ならどこに書いても良いことになりました。

sealed class Score(val number: Int)

class Strike() : Score(10)
class Gutter() : Score(0)
class Miss() : Score(0)

importする時に若干面倒だったのでありがたいですね。

Java 7/8 support

Kotlin1.1からJava7/8がサポートされました。

ちゃんとJavaの新しいクラスなんかもKotlinで使えます。

JavaScript

JSサポートが強化されているらしいですが、使ってるのを聞いたことがないのでよく分かりませんでした。

眺めてみて

結構色々便利にはなっていますが、どれもまだEAPなので変わる可能性はありますし、今後もっと増えるかもしれません。

余談ですが、IDEAの2016.3で1.1M3のプラグインがまともに動かなかったので、Android Studioを使ってAndroidアプリで試しました。

LTの資料を作るときにまずブログ記事から書いている

メンターのシニアエンジニアと話していたら、それいいじゃんって感じになったのでブログにまとめてみる。

関西モバイルアプリ研究会でほぼ毎月(といいつつ最近発表回数減ってるけど)Androidネタで発表しているが、最近はそれと同時にブログ記事も公開している。

blog.takuji31.jp

blog.takuji31.jp

これらの記事は資料作成と同時に書いていて、LTの開始のタイミングで公開する。

なぜブログ記事と同時に書くのか

毎月発表していると、資料作成に使える時間は大きなカンファレンスに参加する時より圧倒的に少ない。関西モバイルアプリ研究会はLT形式なので、もちろん資料のボリュームは小さいんだけど、実際に話すネタとして考える内容は20分や50分のトークとさほど変わらないと思う。

人によってはアウトラインをまず書いてから細かく作っていく、という風に作っていくんだろうけど、自分は割とそういうことが得意でないことが多いので、スライドツールを目の前に頭を抱えることが多い。

そこで、先にブログ記事を書いて、それをLTの時間で話せる内容に落とし込んで資料にしている。

どうやって書いているか

はてなブログにはMarkdown記法で書く便利な設定がある。

そして、DecksetというMarkdown記法でスライドツールが書ける便利なアプリがMac App Storeにある、有料。

www.decksetapp.com

記事の内容をコピペしつつ、スライドとしての体裁を整える。

ブログ記事の順番に話が並ぶので、発表として破綻しているような順番になったりすることはまずない。

資料に書かない部分は発表ノートにしたりもしている。

問題点

  • ブログ記事と発表してる内容が大して変わらない
    • ここは補足を入れるなどすると解決しそう
  • ブログ記事ミスってたら資料もミスってマサカリとか椅子が飛んできそう
  • 本当はもっと良いやり方があるのではないか

よいこと

  • 素早く資料作成できる
  • 発表してきましたブログ書く手間が省ける
  • 参加していない人にもだいたい何を話しているか伝えることができる

やってみて

書いているといっても実際やってみたのはまだ2回だけなので、もっと改善の余地はありそう。

ただ、最近やっている「思いついたらとにかくブログに書く」と組み合わせることでアウトプット量は圧倒的に増えたという気がしているので、続けていきたい。

うまくいったらそのうち通常のトーク資料作る時にも使っていきたい。

一気に書いた新規ファイルを一部git addしたい時にはgit add -Nが便利

新機能作ったりする時、コード書くのに集中しすぎるとコミットする前にめっちゃ色々混ざったファイルができてしまったりして、最悪な感じになることが多い。

自分は割と細かい単位でコミットするようにしている(あとで戻せるように)ので、こうなってしまった時今まではチマチマと手動で消してaddして、commitして、undoしてみたいなことをやっていたが、ミスったりすると数時間分の作業が消える。

そういう時にはgit add -N (file path) すると、ファイルの存在だけindexに乗せることができるので、あとはgit add -pでパッチモードにしてがんばる。

そもそもちゃんと細かくコミットしろよって話でもある。

エンジニア立ち居振舞い : 常に問題意識を持つ

お題「エンジニア立ち居振舞い」

エンジニアに限ったことではなさそう。 サービスとかアプリが運用フェーズに入るとつい思考停止して日々のタスクをこなして生きるだけの機械になりそうになるが、現状が例え良さそうに見えても常に問題はあると考えるようにしている。 そうすることでサービスやアプリの改善に繋がるかもしれないし、業務やチーム、組織の改善につながるきっかけが生まれるのではと思っている。

常に思考し続けている結果、夜にも頭が冴えていることが多くてなかなか寝付けないのが最近の悩み。

MySQLの文字列型

雑なメモ

8月からWebのお仕事やってて、5千年ぶりくらいにまともにMySQL触ることが最近増えてきた。

MySQLのテーブルへのカラム追加のレビューをしていたときにわかったことを書いておく。

.*TEXTな型は TINYTEXT TEXT MEDIUMTEXT LONGTEXT があるが、 最大長は TINYTEXT < TEXT < MEDIUMTEXT < LONGTEXTの順らしい。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.1.3 文字列型の概要

MEDIUMなんて言うからてっきり無印と同じかと思ってたら全然そんなことなかった。

他にもつい VARCHAR(255) とか雑に書いてしまうけど、 VARCHAR は最大65535までいけるのね、たぶんCHARの最大長が255だからそれのノリで(学生時代の頃から)書いていたんだと思う。

以外と色々知らずに思考停止気味に使っていたことに気付いた昼下がりでした。