-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
flutter/engine
#55194Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work listc: regressionIt was better in the past than it is nowIt was better in the past than it is nowc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.24Found to occur in 3.24Found to occur in 3.24found in release: 3.26Found to occur in 3.26Found to occur in 3.26has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamOwned by Engine team
Description
Steps to reproduce
- Run the sample code below.
- Tap those TextField one by one.
I have modified InputDecorationTheme with a custom Border.
The custom AbovelineInputBorder is copy and modified from UnderlineInputBorder,
Which draw both underline and aboveline border for TextField.
It looks good in Skia and Flutter 3.22.3
However the effect become broken in 3.24.2
Expected results
The custom border draws both aboveline and underline.
Actual results
There are some extra underlines on the screen.
Some time they even glitch when you tap TextForms one by one.
Code sample
Code sample
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() {
final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue);
runApp(MyApp(
colorScheme: colorScheme,
));
}
class MyApp extends StatelessWidget {
final ColorScheme colorScheme;
const MyApp({super.key, required this.colorScheme});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light().copyWith(
colorScheme: colorScheme,
inputDecorationTheme: InputDecorationTheme(
filled: true,
// fillColor: Color.lerp(
// colorScheme.primaryFixed, colorScheme.surfaceContainerHighest, 0.7),
border: AbovelineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: AbovelineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primaryContainer),
),
focusedBorder: AbovelineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primary),
),
focusColor: colorScheme.primaryContainer,
),
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(),
SizedBox(height: 20),
TextField(),
SizedBox(height: 20),
TextField(),
SizedBox(height: 20),
],
),
),
),
);
}
}
/// Copy and Modified from [UnderlineInputBorder].
/// Draws a horizontal line at the top of an [InputDecorator]'s container and
/// defines the container's shape.
///
/// The input decorator's "container" is the optionally filled area above the
/// decorator's helper, error, and counter.
///
/// See also:
///
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container.
/// * [InputDecoration], which is used to configure an [InputDecorator].
class AbovelineInputBorder extends InputBorder {
/// Creates an underline border for an [InputDecorator].
///
/// The [borderSide] parameter defaults to [BorderSide.none] (it must not be
/// null). Applications typically do not specify a [borderSide] parameter
/// because the input decorator substitutes its own, using [copyWith], based
/// on the current theme and [InputDecorator.isFocused].
///
/// The [borderRadius] parameter defaults to a value where the top left
/// and right corners have a circular radius of 4.0.
const AbovelineInputBorder({
super.borderSide = const BorderSide(),
this.borderRadius = const BorderRadius.only(
topLeft: Radius.circular(4.0),
topRight: Radius.circular(4.0),
),
});
/// The radii of the border's rounded rectangle corners.
///
/// When this border is used with a filled input decorator, see
/// [InputDecoration.filled], the border radius defines the shape
/// of the background fill as well as the top left and right
/// edges of the underline itself.
///
/// By default the top right and top left corners have a circular radius
/// of 4.0.
final BorderRadius borderRadius;
@override
bool get isOutline => false;
@override
AbovelineInputBorder copyWith(
{BorderSide? borderSide, BorderRadius? borderRadius}) {
return AbovelineInputBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
);
}
@override
EdgeInsetsGeometry get dimensions {
return EdgeInsets.symmetric(vertical: borderSide.width);
}
@override
AbovelineInputBorder scale(double t) {
return AbovelineInputBorder(borderSide: borderSide.scale(t));
}
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..addRect(Rect.fromLTWH(rect.left, math.max(0.0, borderSide.width),
rect.width, rect.top - 2 * math.max(0.0, borderSide.width)));
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
@override
void paintInterior(Canvas canvas, Rect rect, Paint paint,
{TextDirection? textDirection}) {
canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
}
@override
bool get preferPaintInterior => true;
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
if (a is AbovelineInputBorder) {
return AbovelineInputBorder(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t)!,
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
if (b is AbovelineInputBorder) {
return AbovelineInputBorder(
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t)!,
);
}
return super.lerpTo(b, t);
}
/// Draw a horizontal line at the top of [rect].
///
/// The [borderSide] defines the line's color and weight. The `textDirection`
/// `gap` and `textDirection` parameters are ignored.
@override
void paint(
Canvas canvas,
Rect rect, {
double? gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection? textDirection,
}) {
if (borderSide.style == BorderStyle.none) {
return;
}
if (borderRadius.topLeft != Radius.zero ||
borderRadius.topRight != Radius.zero ||
borderRadius.bottomLeft != Radius.zero ||
borderRadius.bottomRight != Radius.zero) {
// This prevents the border from leaking the color due to anti-aliasing rounding errors.
final BorderRadius updatedBorderRadius = BorderRadius.only(
topLeft: borderRadius.topLeft
.clamp(maximum: Radius.circular(rect.height / 2)),
topRight: borderRadius.topRight
.clamp(maximum: Radius.circular(rect.height / 2)),
bottomLeft: borderRadius.bottomLeft
.clamp(maximum: Radius.circular(rect.height / 2)),
bottomRight: borderRadius.bottomRight
.clamp(maximum: Radius.circular(rect.height / 2)),
);
// We set the strokeAlign to center, so the behavior is consistent with
// drawLine and with the historical behavior of this border.
paintNonUniformBorder(
canvas,
rect,
textDirection: textDirection,
borderRadius: updatedBorderRadius,
top: borderSide.copyWith(strokeAlign: BorderSide.strokeAlignCenter),
color: borderSide.color,
maskFilter: const MaskFilter.blur(BlurStyle.inner, 1),
);
paintNonUniformBorder(
canvas,
rect,
textDirection: textDirection,
borderRadius: updatedBorderRadius,
bottom: borderSide.copyWith(strokeAlign: BorderSide.strokeAlignCenter),
color: borderSide.color,
maskFilter: const MaskFilter.blur(BlurStyle.solid, 0.5),
);
} else {
canvas.drawLine(
rect.topLeft,
rect.topRight,
borderSide.toPaint()
..maskFilter = const MaskFilter.blur(BlurStyle.inner, 1));
canvas.drawLine(
rect.bottomLeft,
rect.bottomRight,
borderSide.toPaint()
..maskFilter = const MaskFilter.blur(BlurStyle.inner, 1));
}
}
static void paintNonUniformBorder(
Canvas canvas,
Rect rect, {
required BorderRadius? borderRadius,
required TextDirection? textDirection,
BoxShape shape = BoxShape.rectangle,
BorderSide top = BorderSide.none,
BorderSide right = BorderSide.none,
BorderSide bottom = BorderSide.none,
BorderSide left = BorderSide.none,
required Color color,
MaskFilter? maskFilter,
}) {
final RRect borderRect;
switch (shape) {
case BoxShape.rectangle:
borderRect = (borderRadius ?? BorderRadius.zero)
.resolve(textDirection)
.toRRect(rect);
case BoxShape.circle:
assert(borderRadius == null,
'A borderRadius cannot be given when shape is a BoxShape.circle.');
borderRect = RRect.fromRectAndRadius(
Rect.fromCircle(center: rect.center, radius: rect.shortestSide / 2.0),
Radius.circular(rect.width),
);
}
final Paint paint = Paint()
..color = color
..maskFilter = maskFilter;
final RRect inner = _deflateRRect(
borderRect,
EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset,
right.strokeInset, bottom.strokeInset));
final RRect outer = _inflateRRect(
borderRect,
EdgeInsets.fromLTRB(left.strokeOutset, top.strokeOutset,
right.strokeOutset, bottom.strokeOutset));
canvas.drawDRRect(outer, inner, paint);
}
static RRect _inflateRRect(RRect rect, EdgeInsets insets) {
return RRect.fromLTRBAndCorners(
rect.left - insets.left,
rect.top - insets.top,
rect.right + insets.right,
rect.bottom + insets.bottom,
topLeft: (rect.tlRadius + Radius.elliptical(insets.left, insets.top))
.clamp(minimum: Radius.zero),
topRight: (rect.trRadius + Radius.elliptical(insets.right, insets.top))
.clamp(minimum: Radius.zero),
bottomRight:
(rect.brRadius + Radius.elliptical(insets.right, insets.bottom))
.clamp(minimum: Radius.zero),
bottomLeft:
(rect.blRadius + Radius.elliptical(insets.left, insets.bottom))
.clamp(minimum: Radius.zero),
);
}
static RRect _deflateRRect(RRect rect, EdgeInsets insets) {
return RRect.fromLTRBAndCorners(
rect.left + insets.left,
rect.top + insets.top,
rect.right - insets.right,
rect.bottom - insets.bottom,
topLeft: (rect.tlRadius - Radius.elliptical(insets.left, insets.top))
.clamp(minimum: Radius.zero),
topRight: (rect.trRadius - Radius.elliptical(insets.right, insets.top))
.clamp(minimum: Radius.zero),
bottomRight:
(rect.brRadius - Radius.elliptical(insets.right, insets.bottom))
.clamp(minimum: Radius.zero),
bottomLeft:
(rect.blRadius - Radius.elliptical(insets.left, insets.bottom))
.clamp(minimum: Radius.zero),
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is AbovelineInputBorder &&
other.borderSide == borderSide &&
other.borderRadius == borderRadius;
}
@override
int get hashCode => Object.hash(borderSide, borderRadius);
}
Screenshots or Video
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.24.2, on macOS 13.6 22G120 darwin-arm64, locale zh-Hans-CN)
• Flutter version 3.24.2 on channel stable at /Users/zhx/fvm/versions/stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 4cf269e36d (9 days ago), 2024-09-03 14:30:00 -0700
• Engine revision a6bd3f1de1
• Dart version 3.5.2
• DevTools version 2.37.2
• Pub download mirror https://pub.flutter-io.cn
• Flutter download mirror https://storage.flutter-io.cn
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/zhx/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 15C500b
• CocoaPods version 1.15.2
[✓] Android Studio (version 2024.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)
[✓] VS Code (version 1.93.0)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.97.20240902
[!] Proxy Configuration
• HTTP_PROXY is set
! NO_PROXY is not set
[✓] Connected device (3 available)
• iPhone (mobile) • ************** • ios • iOS 17.5.1 21F90
• macOS (desktop) • macos • darwin-arm64 • macOS 13.6 22G120 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 13.6 22G120 darwin-arm64
[✓] Network resources
• All expected network resources are available.Metadata
Metadata
Assignees
Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work listc: regressionIt was better in the past than it is nowIt was better in the past than it is nowc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.24Found to occur in 3.24Found to occur in 3.24found in release: 3.26Found to occur in 3.26Found to occur in 3.26has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamOwned by Engine team
