対象者
- FlutterでのUIデザインを向上させたいエンジニア
- Squircleの概念と実装方法を知りたいデザイナー・開発者
- アプリのデザインをよりモダンで柔らかい印象にしたい方
はじめに
皆さんは「Squircle(スクワークル)」という形をご存知でしょうか?これは四角形と円の中間に位置する形で、最近のモダンなUIデザインでよく見かけるようになりました。特にFlutterでアプリ開発を行う際、デフォルトの角丸では表現できない柔らかさや滑らかさを実現できます。本記事では、Squircleの概念とFlutterでの実装方法について詳しく解説します。
Squircleとはなにか
Squircleは「Square(四角)」と「Circle(円)」を組み合わせた造語で、四角でも円でもない中間的な形状を指します。この形は直線と曲線が滑らかに融合しており、柔らかく自然な印象を与えます。AppleのiOSアプリアイコンやInstagramのロゴなど、さまざまなデザインで採用されています。
左:Squircle (四角の隅の、角と円が一致していない)
右:角丸 (四角の隅の、角と円が一致している)
なぜSquircleを使うのか
- 視覚的な柔らかさ:尖った角や単純な角丸よりも柔らかい印象を与える
- モダンなデザイン:最新のデザイントレンドに合致
- ユーザーエクスペリエンスの向上:視覚的な心地よさがユーザーの満足度を高める
SquircleのFlutterでの実装方法
Flutterでは、デフォルトの角丸だけではSquircle特有の滑らかさを再現できません。しかし、いくつかの方法を組み合わせることで、Squircleを実装することが可能です。
ContinuousRectangleBorderを使用する
ContinuousRectangleBorder
は、FlutterでSquircleを実現するための標準的なクラスです。しかし、そのまま使用すると期待する形にならない場合があります。
Container(
height: 64,
decoration: ShapeDecoration(
color: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Center(
child: Text('ContinuousRectangleBorder circular:16')),
),
半径に係数を掛ける方法
ContinuousRectangleBorder
の半径に約2.3529を掛けると、より適切なSquircle形状になります。
Container(
height: 64,
decoration: ShapeDecoration(
color: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16 * 2.3529),
),
),
child: const Center(
child:
Text('ContinuousRectangleBorder circular:16 * 2.3529')),
),
ClipPathとCustomClipperを使用する
ClipPath
とShapeBorderClipper
を使っても作成することができます。
ClipPath(
clipper: const ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16 * 2.3529)),
),
),
child: Container(
height: 64,
decoration: const BoxDecoration(color: Colors.blue),
child: const Center(child: Text('ClipPath circular:16 * 2.3529')),
),
),
カスタムパッケージを使用する
figma_squircleパッケージ
Figmaのコーナースムージングと同様のSquircleを実現できるそうです。ただ3年前の更新が最後で、使用するのには一抹の不安があります。
ウィジェットごとの実装パターン
なんとなく使用方法は分かりましたが、実装がWidgetごとに微妙に異なるので、ひとまずまとめてみました。
- ボタン系ウィジェット(FilledButtonなど):
- 該当の
Style
のShape
プロパティにContinuousRectangleBorder
を使用
- 該当の
- Container:
decoration
にShapeDecoration(shape:ContinuousRectangleBorder)
を組み合わせる
- Card, ListTile:
shape
プロパティにContinuousRectangleBorder
を使用し、必要に応じて係数を調整
Q&A
Q1: なぜ角丸ではなくSquircleを使うべきなのでしょうか?
Squircleは角丸よりも滑らかな曲線を持ち、より自然でモダンな印象を与えます。ユーザーエクスペリエンスを向上させ、デザインに一貫性と洗練さをもたらします。
Q2:ContinuousRectangleBorder
の係数2.3529はどこから来たのですか?
この係数は、ContinuousRectangleBorder
とRoundedRectangleBorder
の見た目を一致させるための経験的な値です。正確な数学的根拠はありませんが、実際のデザインで視覚的に一致させるために使用されています。
Q3:パフォーマンスへの影響はありますか?
カスタムパッケージやClipPath
を多用すると、描画にコストがかかる場合があります。パフォーマンスが重要な場面では、使用するウィジェットや実装方法を慎重に選択してください。
Q4:ContinuousRectangleBorder とは何ですか?
ContinuousRectangleBorder は、Flutterにおけるカスタムシェイプ(形状)を定義するためのクラスの一つです。このクラスは、ウィジェットの境界線に対して連続的な曲線を適用し、滑らかで自然な形状を実現するために使用されます。特に、角が滑らかに曲がる「スクワークル(Squircle)」のような形状を作成する際に有用です。
主な特徴
- 滑らかな曲線: ContinuousRectangleBorder は、角が単純な円弧ではなく、より滑らかで連続的な曲線を描くため、視覚的に柔らかく自然な印象を与えます。
- カスタマイズ可能なボーダー半径: borderRadius プロパティを使用して、角の丸み具合を細かく調整できます。
- シェイプの統一性: ボタンやカードなど、複数のウィジェットで一貫した形状を保つことができます。
参考
-
Creating Squircles in Flutter: A Sleek UI Design Alternative
Squircleなるものが存在することを知る -
四角でも丸でもない形!Squircleを取り入れたUI考察
Squircleの一般的な記事 -
より良いユーザー体験を求めて "角丸" を深掘りする
FlutterでのSquircleの解説。「ContinuousRectangleBorder x 2.3529」を知る
–Flutter Squricle Study (v1.3.0)
FlutterでのSquircleについて、なんか詳しそう。
- figma_squircleパッケージ
まとめ
SquircleをFlutterで実装することで、アプリのデザインに柔らかさとモダンさを加えることができます。デフォルトの角丸では表現できない滑らかな曲線を持つSquircleは、ユーザーエクスペリエンスの向上にも寄与します。さまざまな実装方法がありますので、プロジェクトの要件やパフォーマンスを考慮して最適な方法を選びましょう。
皆さんもぜひFlutterでSquircleを取り入れて、デザインの幅を広げてみてください!
ソース(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: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 64,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
child: const Center(child: Text('ただの角丸 circular:16')),
),
const SizedBox(height: 8),
Container(
height: 64,
decoration: ShapeDecoration(
color: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Center(
child: Text('ContinuousRectangleBorder circular:16')),
),
const SizedBox(height: 8),
Container(
height: 64,
decoration: ShapeDecoration(
color: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16 * 2.3529),
),
),
child: const Center(
child:
Text('ContinuousRectangleBorder circular:16 * 2.3529')),
),
const SizedBox(height: 8),
ClipPath(
clipper: const ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16 * 2.3529)),
),
),
child: Container(
height: 64,
decoration: const BoxDecoration(color: Colors.blue),
child:
const Center(child: Text('ClipPath circular:16 * 2.3529')),
),
),
const SizedBox(height: 32),
FilledButton(
style: FilledButton.styleFrom(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(10 * 2.3529),
),
),
onPressed: () {},
child: const Text('Squircle: ボタン'),
),
const SizedBox(height: 32),
ListTile(
tileColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
title: const Center(child: Text('角丸 ListTile')),
),
const SizedBox(height: 8),
ListTile(
tileColor: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(10 * 2.3529),
),
title: const Center(child: Text('Squircle: ListTile')),
),
Card(
color: Colors.blue,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(10 * 2.3529),
),
child: const SizedBox(
height: 64,
child: Center(child: Text('Squircle: Card')),
),
),
],
),
),
);
}
}