Friday, April 1, 2016 / Android, SVG, kotlin

戻るボタンのアイコンを描画する Drawable の自作

以前は、R.drawable.ic_menu_back といった戻るボタンのリソースが標準であったような気がしたが、見つからない。標準の android のアイコン一覧を探したところ、こちら にそれがあり、リソースは material-design-icons から入手できるようだ。

ic_arrow_back_black_○○dp.png が大量に必要

material-design-icons から入手したリソースを調べるとどうやら 戻るボタンは ic_arrow_back*.png がそれらしい。

find . -name ic_arrow_back_black*png | grep drawable | wc

してみると 40個存在している。(一種類のアイコンのために40個のpngファイルは多すぎでしょ・・・) 軽く調べてみると Lollipop からは VectorDrawable というものが導入されて、それならば、ic_arrow_back_black_24dp.xml 1つで済むらしい。(詳しくは調べていないのであしからず。)

しかし今つくっているアプリは Kitkat 以上を前提としている。 何か回避策もあるようだが、とりあえず戻るボタン1つ必要なだけなので、自前のDrawableをつくって問題を回避することにした。

ic_arrow_back_black_24dp.xml からDrawable をつくる

内容は以下のようになっていて、pathData の部分にSVGの描画コマンドが入っている。 この部分を解析してDrawableクラス内でアイコン描画をし直せばよい。

ic_arrow_back_black_24dp.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

内容を把握するために、このXMLからHTML(SVG)を作成して、ブラウザでアイコン描画を確認する。

ic_arrow_back_black.html

<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 24 24">
    <path stroke="black" stroke-width="1" fill="true" d="
        M 20,11
        H 7.83
        l 5.59,-5.59
        L 12,4
        l -8,8 8,8 1.41,-1.41
        L 7.83,13
        H 20
        v -2
        z
        "/>
</svg>
</body>
</html>

※)わかりやすいように、pathDataの値に適当にスペースと改行を追加しています。

ic_arrow_back_black.html をブラウザで表示した結果。

ic_arrow_back.jpg

ArrowBackDrawable

あとはこの描画コマンドを android のコードに翻訳すればよい。

※)ちなみに、https://github.com/telly/MrVector 等にあるような PathParser クラスを使えば、今回のように、自分でSVGコマンドをいちいち翻訳する必要はない。

ArrowBackDrawable.java

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.Path;

class ArrowBackDrawable extends ColorDrawable {
    ArrowBackDrawable() {
        super(Color.BLACK);
    }

    // 最適化の余地はありますが、説明上わかりやすくなるように実装.
    public void draw(Canvas canvas) {
        //
        // SVGコマンドを翻訳
        // M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z
        //

        // M 20,11
        PointF a = new PointF(20f, 11f);
        // H 7.83
        PointF b = new PointF(7.83f, a.y);
        // l 5.59,-5.59
        PointF c = new PointF((b.x + 5.59f), (b.y - 5.59f));
        // L 12,4
        PointF d = new PointF(12f, 4f);

        // l -8,8 8,8 1.41,-1.41
        PointF e = new PointF((d.x - 8f), (d.y + 8f));
        PointF f = new PointF((e.x + 8f), (e.y + 8f));
        PointF g = new PointF((f.x + 1.41f), (f.y - 1.41f));

        // L 7.83,13
        PointF h = new PointF(7.83f, 13f);

        // H 20
        PointF i = new PointF(20f, h.y);
        // v -2
        PointF j = new PointF(i.x, (i.y - 2f));

        //
        // 点を現在のDrawableの大きさに合わせて調整する.
        //
        PointF[] pointArray = new PointF[]{a, b, c, d, e, f, g, h, i, j};
        for (PointF pt : pointArray) {
            pt.x = pt.x * (canvas.getWidth() / 24f);
            pt.y = pt.y * (canvas.getHeight() / 24f);
        }


        //
        // パスの構築
        //
        Path path = new Path();
        path.moveTo(pointArray[0].x, pointArray[0].y);
        for (int index = 1; index < pointArray.length; index++) {
            path.lineTo(pointArray[index].x, pointArray[index].y);
        }

        // z
        path.close();


        //
        // 描画
        //
        Paint paint = new Paint();
        paint.setColor(getColor());
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
    }
}

アイコンのまわりのマージン設定が足りないような気がするけど、とりあえずこんな感じで。

おまけ

kotlin に移植した ArrowBackDrawable.kt , 多少の最適化とマージの追加。

ArrowBackDrawable.kt

import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.graphics.Path

internal class ArrowBackDrawable(color:Int) : ColorDrawable(color) {

    val pointArray: Array<PointF>
    val paint: Paint
    val path: Path
    val matrix: Matrix

    init {
        // M 20,11
        val a = PointF(20f, 11f)
        // H 7.83
        val b = PointF(7.83f, a.y)
        // l 5.59,-5.59
        val c = PointF(b.x + 5.59f, b.y - 5.59f)
        // L 12,4
        val d = PointF(12f, 4f)

        // l -8,8 8,8 1.41,-1.41
        val e = PointF(d.x - 8f, d.y + 8f)
        val f = PointF(e.x + 8f, e.y + 8f)
        val g = PointF(f.x + 1.41f, f.y - 1.41f)

        // L 7.83,13
        val h = PointF(7.83f, 13f)

        // H 20
        val i = PointF(20f, h.y)
        // v -2
        val j = PointF(i.x, i.y - 2f)

        pointArray = arrayOf(a, b, c, d, e, f, g, h, i, j)

        paint = Paint()
        paint.color = color
        paint.style = Paint.Style.FILL

        path = Path()

        matrix = Matrix()
    }

    override fun draw(canvas: Canvas) {
        //
        // パスの構築
        //
        path.reset()

        val firstPtArray = pointArray.take(1)
        path.moveTo(firstPtArray[0].x, firstPtArray[0].y)

        pointArray.drop(1).forEach { pt->
            path.lineTo(pt.x, pt.y)
        }

        // z
        path.close()


        //
        // パスのトランスフォーム
        //
        matrix.reset()

        val scaleX = (canvas.width*20f/24f) / 24f
        val scaleY = (canvas.height*20f/24f) / 24f
        val tx = canvas.width*2f/24f
        val ty = canvas.height*2f/24f

        val values = arrayOf(
                scaleX, 0f, tx,
                0f, scaleY, ty,
                0f, 0f,      1f
        ).toFloatArray()

        matrix.setValues(values)

        path.transform(matrix)

        //
        // 描画
        //
        canvas.drawPath(path, paint)
    }
}