CSSのbackground: linear-gradient(45deg, #FF0000, #0000FF)のようなグラデーションをFlutterのテキストにつけたい
↓ これをFlutterで実装したい。
↓ 上からコピーしてきたstyle。
background: linear-gradient(104deg, #F00 17.74%, #F0F 49.9%, #00F 80.65%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
上記のCSSの104degのように角度をしていするのではなく、左上のスミから右下のスミにグラデーションをつけるのであれば、次のようにできる。これを角度指定でやりたい。
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16),
child: ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: (bounds) => const LinearGradient(
begin: Alignment(-1, -1),
end: Alignment(1, 1),
colors: [
Color(0xFFFF0000),
Color(0xFFFF00FF),
Color(0xFF0000FF),
],
stops: [
0.0,
0.5,
1.0,
],
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
child: Text(
"ABC",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w700,
),
),
),
),
),
);
}
}
ここで質問されていた: How to create a linear gradient with 45 degrees in Flutter?
- LinearGradientのtransformを使う方法が使えそう: https://stackoverflow.com/a/64732687/8834586
LinearGradient( begin: Alignment(-1.0, 0.0), end: Alignment(1.0, 0.0), colors: [], stops: [], transform: GradientRotation(math.pi / 4), ),
- 気になる点
-
transform: GradientRotation(math.pi / 4),
とあるが、どこを中心にrotationされるのか? - begin, endの設定を見ると、左上スミから右上スミへのグラデーションとなっている。pi / 4ラジアンrotationしたら、長さの調節も必要ではないか。
-
- 上の「気になる点」を確認してみたところ、領域の中心でrotationし、
長さの調節は必要なさそう必要そう(下のコメントへ)。そのためこの方法を使えそう。- スクリーンショット(上がtransformなし。下があり。)
- コード (transform無しの方)
ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => const LinearGradient( begin: Alignment(-1, 0), end: Alignment(1, 0), colors: [ Color(0xFFFF0000), Color(0xFFFF0000), Color(0xFFFFFF00), Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FF00), Color(0xFF0000FF), Color(0xFF0000FF), ], stops: [ 0.0, 0.249, 0.251, 0.499, 0.501, 0.749, 0.751, 1.0, ], ).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)), child: Text( "ABC", style: TextStyle( fontSize: 48, fontWeight: FontWeight.w700, ), ), ),
- コード (transform有りの方)
-
transform: GradientRotation(pi / 4),
をLinearGradientに追加。
-
- スクリーンショット(上がtransformなし。下があり。)
- 気になる点
上のtransform: GradientRotation(pi / 4)
を使って実装してみた。
しかし、以下の通り、見た目がすこしデザイン(Figma)やhtmlと異なる。
Flutter | Figma | html |
---|---|---|
![]() |
![]() |
![]() |
↓Flutterのコード
ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: (bounds) => const LinearGradient(
// transformを適用する前の状態。これは下から上とする(CSSでいう0degreeとする)。
begin: Alignment(0, 1),
end: Alignment(0, -1),
colors: [
Color(0xFFFF0000),
Color(0xFFFF00FF),
Color(0xFF0000FF),
],
stops: [
0.1774,
0.499,
0.8065,
],
transform: GradientRotation((104 / 360) * (pi * 2)),
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
child: Text(
"ABC",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w700,
fontFamily: 'NotoSans'
),
),
),
↓html(styleはFigmaからコピーしたもの)
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap" rel="stylesheet">
<style>
body {
background: #424242;
}
.label {
margin: 24px;
text-align: left;
font-family: Noto Sans;
font-size: 48px;
font-style: normal;
font-weight: 700;
line-height: normal;
background: linear-gradient(104deg, #F00 17.74%, #F0F 49.9%, #00F 80.65%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<span class="label">ABC</span>
</body>
</html>
↑のFlutterとhtmlの違いがわかりやすくなるよう、くっきりしたグラデーションにしてみる。
以下の通りサイズはそれほど変わりない。なので同じように表示されるはず。なのに表示されない。
↓ やはり、長さの調整が必要かもしれない
begin, endの設定を見ると、左上スミから右上スミへのグラデーションとなっている。pi / 4ラジアンrotationしたら、長さの調節も必要ではないか。
(上のコメント)
Flutter | html |
---|---|
![]() |
![]() |
![]() |
![]() |
↓Flutter
colors: [
Color(0xFFFF0000),
Color(0xFFFF0000),
Color(0xFFFFFF00),
Color(0xFFFFFF00),
Color(0xFF00FF00),
Color(0xFF00FF00),
Color(0xFF0000FF),
Color(0xFF0000FF),
],
stops: [
0,
0.25,
0.25,
0.5,
0.5,
0.75,
0.75,
1.0,
],
↓html(css)
background: linear-gradient(135deg, #F00 0%, #F00 25%, #FF0 25%, #FF0 50%, #0F0 50%, #0F0 75%, #0FF 75%, #0FF 100%);
上記の「長さの調整」は、GradientTransformation(abstract class)のカスタムクラスを作ればいけそう。現状implementorはGradientRotationのみ。
- GradientRotationは、SweepGradient向けであって、LinearGradation向きではないみたい。
For example, a SweepGradient normally starts its gradation at 3 o'clock and draws clockwise. To have the sweep appear to start at 6 o'clock, supply a GradientRotation of pi/4 radians (i.e. 45 degrees).
https://api.flutter.dev/flutter/painting/GradientTransform-class.html - 参考になりそう: GradientRotation.transform()
@override Matrix4 transform(Rect bounds, {TextDirection? textDirection}) { final double sinRadians = math.sin(radians); final double oneMinusCosRadians = 1 - math.cos(radians); final Offset center = bounds.center; final double originX = sinRadians * center.dy + oneMinusCosRadians * center.dx; // (1) final double originY = -sinRadians * center.dx + oneMinusCosRadians * center.dy; // (2) return Matrix4.identity() ..translate(originX, originY) ..rotateZ(radians); }
- (1), (2)の部分で何をしているかわからない。x, yの1x2の行列に、1-cosθ, sinθ, -sinθ, 1-cosθの2x2の行列を適用しているけど、回転しているというわけではなさそう。
- なぜ、回転する角度に応じて、回転の中心(origin)を変える必要があるのか?
- 180degreeなら、originXは、2 x centerX。originYは、2 x centerY。
- 360degreeなら、originXは、0。originYも、0。(まあ、そりゃそうか)
- 90degreeなら、originXは、centerY + centerX。originYは、-centerX + centerY。
- 270degreeなら、originXは、-centerY + centerX。originYは、centerX + centerY。
- translateしなかった場合、どこを中心に回転するのか→Textの領域の左上っぽい。
- 呼び出し元を見ればわかるかな。→ 呼び出し元を見て、matrixをどう使っているか確認しようとしたところ、nativeに渡しており、追えなかった。
- Textではなく、いったん、Containerを使って長方形で確認した方がわかりやすそう!
- やはり、translateしない場合、widgetの左上を中心に回転していた。
- なぜ、回転する角度に応じて、回転の中心(origin)を変える必要があるのか?
- (1), (2)の部分で何をしているかわからない。x, yの1x2の行列に、1-cosθ, sinθ, -sinθ, 1-cosθの2x2の行列を適用しているけど、回転しているというわけではなさそう。
以下の通りできた。GradientTransformのカスタムクラスを作った。
Flutter | html |
---|---|
![]() |
![]() |
↓htmlのコード(styleは、いつものようにFigmaからコピーしたもの)
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap" rel="stylesheet">
<style>
body {
background: #424242;
}
.label {
margin: 24px;
text-align: left;
font-family: Noto Sans;
font-size: 48px;
font-style: normal;
font-weight: 700;
line-height: normal;
background: linear-gradient(135deg, #F00 0%, #F00 25%, #FF0 25%, #FF0 50%, #0F0 50%, #0F0 75%, #00F 75%, #00F 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<span class="label">ABC</span>
</body>
</html>
↓Flutterのコード
// ...
ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: (bounds) => const LinearGradient(
begin: Alignment(0, 1),
end: Alignment(0, -1),
colors: [
Color(0xFFFF0000),
Color(0xFFFF0000),
Color(0xFFFFFF00),
Color(0xFFFFFF00),
Color(0xFF00FF00),
Color(0xFF00FF00),
Color(0xFF0000FF),
Color(0xFF0000FF),
],
stops: [
0,
0.25,
0.25,
0.5,
0.5,
0.75,
0.75,
1.0,
],
transform: LinearGradientRotation(degree: 135),
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
// ...
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
class LinearGradientRotation extends GradientTransform {
final double degree;
const LinearGradientRotation(required this.degree);
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
final radians = ((degree % 360) / 360) * (math.pi * 2);
final radians2 = radians % math.pi;
// 長方形の頂点を左上から反時計周りにABCD、対角線の交点をOとする。
final diagonalLength = math.sqrt(math.pow(bounds.height, 2) + math.pow(bounds.width, 2));
final sinBDC = bounds.width / diagonalLength;
final cornerBDC = math.asin(sinBDC);
final cornerDBC = math.pi / 2 - cornerBDC;
const scaleX = 1.0;
double scaleY = 1.0;
if (radians2 == 0.0) {
scaleY = 1.0;
} else if (radians2 > 0.0 && radians2 < cornerBDC) {
final cosOfCornerBDCMinusRadians2 = math.cos(cornerBDC - radians2);
scaleY = ((diagonalLength / 2) * cosOfCornerBDCMinusRadians2 * 2) / bounds.height;
} else if (radians2 == cornerBDC) {
scaleY = diagonalLength / bounds.height;
} else if (radians2 > cornerBDC && radians2 < (math.pi / 2)) {
final cosOfRadians2MinusCornerBDC = math.cos(radians2 - cornerBDC);
scaleY = ((diagonalLength / 2) * cosOfRadians2MinusCornerBDC * 2) / bounds.height;
} else if (radians2 == (math.pi / 2)) {
scaleY = bounds.width / bounds.height;
} else if (radians2 > (math.pi / 2) && radians2 < (math.pi - cornerBDC)) {
final cosOfCornerDBCPlusHalfPiMinusRadians2 = math.cos(cornerDBC + math.pi / 2 - radians2);
scaleY = ((diagonalLength / 2) * cosOfCornerDBCPlusHalfPiMinusRadians2 * 2) / bounds.height;
} else if (radians2 == (math.pi - cornerBDC)) {
scaleY = diagonalLength / bounds.height;
} else if (radians2 > (math.pi - cornerBDC)) {
final cosOfRadians2MinusHalfPiMinusCornerDBC = math.cos(radians2 - math.pi / 2 - cornerDBC);
scaleY = ((diagonalLength / 2) * cosOfRadians2MinusHalfPiMinusCornerDBC * 2) / bounds.height;
}
final Offset center = bounds.center;
final scale = Matrix4.fromList([
scaleX, 0.0, 0.0, 0.0,
0.0, scaleY, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
final translate = Matrix4.fromList([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-center.dx * scaleX, -center.dy * scaleY, 0.0, 1.0,
]);
final rotate = Matrix4.fromList([
math.cos(radians), math.sin(radians), 0.0, 0.0,
-math.sin(radians), math.cos(radians), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
final translate2 = Matrix4.fromList([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
center.dx, center.dy, 0.0, 1.0,
]);
return translate2
.multiplied(rotate)
.multiplied(translate)
.multiplied(scale)
;
}
}
これ、ちゃんと、Containerのdecorationでも使えるか?
→ 以下のように使えた。
Container(
height: 50,
width: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0, 1),
end: Alignment(0, -1),
colors: [
Color(0xFFFF0000),
Color(0xFFFF0000),
Color(0xFFFFFF00),
Color(0xFFFFFF00),
Color(0xFF00FF00),
Color(0xFF00FF00),
Color(0xFF0000FF),
Color(0xFF0000FF),
],
stops: [
0,
0.25,
0.25,
0.5,
0.5,
0.75,
0.75,
1.0,
],
transform: LinearGradientRotation(degree: 135),
),
),
),