ピクシブ株式会社で主にアプリ開発を担当している @m4kvn です。普段は、パルシィ(Palcy)のAndroidアプリ版の開発をしています。
以前には次のような記事も書いています。
今回は、パルシィで利用するために作成している縁取り文字用のTextViewについて、どのように実装しているのかを紹介します。
縁取り文字ってなに?
次の画像の文字列のように、まわりに色がついていて強調表現された文字のことです。
パルシィでは、この縁取られた文字を使って表現したい箇所が複数あります。そのため専用のTextViewを作りました。
どのように実現するか?
TextViewの onDraw(Canvas?)
をオーバーライドし、strokeを適用した文字列を描画したあと、その上にstroke未適用時の文字列を描画します。このとき super.onDraw(canvas)
を使うことで、TextViewにもともとあった機能をそのまま使えるようにしています。
override fun onDraw(canvas: Canvas?) { ... val restoreColor = currentTextColor val restorePaint = Paint(paint) // はじめにstrokeを適用したテキストを描画する paint.style = Paint.Style.STROKE paint.strokeJoin = strokeJoin paint.strokeMiter = strokeMiter paint.strokeWidth = strokeWidth setTextColor(strokeColor) super.onDraw(canvas) // 上記で描画したテキストの上にもともとのテキストを描画する paint.style = restorePaint.style paint.strokeJoin = restorePaint.strokeJoin paint.strokeMiter = restorePaint.strokeMiter paint.strokeWidth = restorePaint.strokeWidth setTextColor(restoreColor) super.onDraw(canvas) ... }
これを実装したTextViewで次のように HTML.fromHtml
で変換した値を設定してみます。
val html = "<big><big>BIG</big></big> and normal" val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) outlineTextView.text = spanned
すると次のような結果になり縁取り文字を表現できました。
HTMLで文字色を指定した場合にうまく描画できない
次のような <font color=””>
を含む文字列を使った場合に、そこで指定した色で塗りつぶされた文字が描画されてしまいます。
val html = "<font color=\"red\">colored</font> <big><big>NOT</big></big> colored" val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) outlineTextView.text = spanned
どうやらForegroundColorSpanが設定されていた場合に setTextColor(color)
をしても上書きされてしまうらしいです。そのため onDraw(canvas)
をする前にForegroundColorSpanを取り除いたテキストを作成して縁取りの描画をしてから元々の文字列に戻して中の文字を描画するようにしてみます。
override fun onDraw(canvas: Canvas?) { ... val restoreString = text if (text is Spanned) { val spannable = text.toSpannable() val fgSpans = spannable.getSpans<ForegroundColorSpan>() fgSpans.forEach { spannable.removeSpan(it) } text = spannable // ForegroundColorSpanを除いたものを設定 } setTextColor(strokeColor) super.onDraw(canvas) // 縁取り用の描画 ... text = restoreString // ForegroundColorSpanを含めたものを再設定 setTextColor(restoreColor) super.onDraw(canvas) // 中の文字用の描画 ... }
すると思っていたような結果になりました。
最終的なコード
こちらが最終的に完成したTextViewのコードになります。ComposeでのAndroidViewで利用することが前提なのでコンストラクタが1つしかありませんが、XMLから利用する場合はコンストラクタの追加や、AttributeSetの処理が必要になります。
class OutlineTextView(context: Context) : AppCompatTextView(context) { private var strokeColor: Int? = null private var strokeWidth: Float = 1f private var strokeJoin: Paint.Join = Paint.Join.MITER private var strokeMiter: Float = 10f fun setStroke( width: Float = strokeWidth, color: Int? = strokeColor, join: Paint.Join = strokeJoin, miter: Float = strokeMiter, ) { strokeWidth = width strokeColor = color strokeJoin = join strokeMiter = miter } @SuppressLint("DrawAllocation") override fun onDraw(canvas: Canvas?) { val strokeColor = strokeColor ?: return super.onDraw(canvas) val restoreString = text val restoreColor = currentTextColor // ForegroundColorSpanがある状態ではstrokeColorが上書きされてしまう // なのでForegroundColorSpanを削除したtextを別途つくりtextを上書きする if (text is Spanned) { val spannable = text.toSpannable() val fgSpans = spannable.getSpans<ForegroundColorSpan>() fgSpans.forEach { spannable.removeSpan(it) } text = spannable } val restorePaint = Paint(paint) paint.style = Paint.Style.STROKE paint.strokeJoin = strokeJoin paint.strokeMiter = strokeMiter paint.strokeWidth = strokeWidth setTextColor(strokeColor) super.onDraw(canvas) text = restoreString // textを上書きしている可能性があるため元に戻す paint.style = restorePaint.style paint.strokeJoin = restorePaint.strokeJoin paint.strokeMiter = restorePaint.strokeMiter paint.strokeWidth = restorePaint.strokeWidth setTextColor(restoreColor) super.onDraw(canvas) } }