【Flutter】縁取り文字でテキストを装飾!自前でStrokedText

  • 2024年3月6日
  • 2024年3月6日
  • 小物

はじめに

アプリ開発において、鮮明で読みやすいテキストはユーザー体験の重要な要素です。特に背景画像が文字色と似ている場合、テキストの視認性が低下し、ユーザーが内容を理解するのが難しくなります。そんな時、デザインの世界では「縁取り文字」や英語でいうと「Outlined Text」や「Stroked Text」といった技法がよく用いられます。これは、文字の外側に別の色の線を加えることで、テキストを際立たせる方法です。

デザインツールではこの技術を簡単に実現できますが、Flutterで実装しようとすると、意外と手間がかかることがあります。外部パッケージを利用する方法もありますが、バージョンの違いや将来のメンテナンスの不安から、シンプルなテキストであれば自前で実装する方が安心できる場合もあります。

このブログでは、Flutterで縁取り文字を実装する方法について、実践的なアプローチを解説します。背景がうるさい画像や、色が近い背景でも読みやすいテキストを実現するためのテクニックを、わかりやすく説明していきます。

Flutterで縁取り文字を実装する方法

Flutterで縁取り文字(アウトラインテキスト)を実装するには、Textウィジェットを使って文字の輪郭を描く方法が一般的です。この記事では、上記のソースコードを例にして、縁取り文字の実装方法を解説します。

基本的な構造

まず、StrokedTextというカスタムウィジェットを作成します。このウィジェットは、テキストの色、輪郭の色、フォントサイズ、輪郭の太さをパラメータとして受け取ります。

class StrokedText extends StatelessWidget {
  const StrokedText(
    this.text, {
    this.color,
    this.strokeColor,
    this.fontSize,
    this.strokeSize,
    super.key,
  });

  final String text;
  final Color? color;
  final Color? strokeColor;
  final double? fontSize;
  final double? strokeSize;
}

テキストの輪郭を描く

StrokedTextウィジェットのbuildメソッドでは、Stackウィジェットを使用して、輪郭を持つテキストと実際のテキストを重ね合わせます。まず、輪郭を持つテキストをTextウィジェットで作成し、TextStyleforegroundプロパティを使って、PaintingStyle.strokeスタイルのPaintオブジェクトを設定します。これにより、テキストの輪郭が描かれます。

Text(
  text,
  style: TextStyle(
    fontSize: fontSize,
    foreground: Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeSize ?? 2
      ..color = strokeColor ?? Theme.of(context).colorScheme.onPrimary,
  ),
)

実際のテキストを描く

次に、実際のテキストを描くために、もう一つTextウィジェットを作成します。このウィジェットでは、TextStyleで通常のテキストの色とフォントサイズを指定します。

Text(
  text,
  style: TextStyle(
    fontSize: fontSize,
    color: color ?? Theme.of(context).colorScheme.primary,
  ),
)

使用例

StrokedTextウィジェットを使うことで、縁取り文字を簡単に実装できます。以下の例では、32サイズのテキストにデフォルトの色設定で輪郭を付けています。また、24サイズのテキストには、黒いテキストに赤い輪郭を付けています。

StrokedText('StrokedTextのテスト', fontSize: 32),
StrokedText(
  'StrokedTextのテスト',
  fontSize: 24,
  color: Colors.black,
  strokeColor: Colors.red,
)

このように、Flutterで縁取り文字を実装する方法は比較的簡単で、カスタムウィジェットを作成することで、アプリのさまざまな場所で再利用できる柔軟なテキストスタイルを実現できます。

まとめ

縁取り文字を作る必要があり、色々調べて作成しました。最初の一回は少し面倒ですが、後は使い回せる。
他にも影を使って実装する方法もあったのですが、こちらの方がまとまっている感じがして良いかな、と思ってます。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: const Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            StrokedText('StrokedTextのテスト', fontSize: 32),
            StrokedText(
              'StrokedTextのテスト',
              fontSize: 24,
              color: Colors.black,
              strokeColor: Colors.red,
            ),
          ],
        ),
      ),
    );
  }
}

class StrokedText extends StatelessWidget {
  const StrokedText(
    this.text, {
    this.color,
    this.strokeColor,
    this.fontSize,
    this.strokeSize,
    super.key,
  });

  final String text;
  final Color? color;
  final Color? strokeColor;
  final double? fontSize;
  final double? strokeSize;

  @override
  Widget build(BuildContext context) {
    final fontSize = this.fontSize ??
        Theme.of(context).textTheme.bodyMedium?.fontSize ??
        16.0;

    return Stack(
      children: <Widget>[
        // Stroked text as border.
        Text(
          text,
          style: TextStyle(
            fontSize: fontSize,
            foreground: Paint()
              ..style = PaintingStyle.stroke
              ..strokeWidth = strokeSize ?? 2
              ..color = color ?? Theme.of(context).colorScheme.primary,
          ),
        ),
        // Solid text as fill.
        Text(
          text,
          style: TextStyle(
            fontSize: fontSize,
            color: strokeColor ?? Theme.of(context).colorScheme.onPrimary,
          ),
        ),
      ],
    );
  }
}