Related
I want to make this design with an underline for a holiday in my calendar widget.
This is what I have so far. I don't know how to add spacing and round off the sides of the underline.
I am using SfDateRangePicker, here is the code so far:
monthCellStyle: DateRangePickerMonthCellStyle(
specialDatesTextStyle: TextStyle(
fontStyle: FontStyle.normal,
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.black,
decoration: TextDecoration.underline,
decorationThickness: 4,
decorationColor: Colors.red[800]),
Unfortunately syncfusion is not open source so can't really test this but they seem to have an example here
I don't know if it will be enough for what you want to accomplish, but you can get around this problem with this style. If you decrease the value "-2" the spacing will increase.
OBS: [credits]: Is it possible to underline text with some height between text and underline line?
Text(
"Your Text here",
style: TextStyle(
shadows: [
Shadow(color: Colors.black,
offset: Offset(0, -2))
],
color: Colors.transparent,
decoration: TextDecoration.underline,
decorationColor: Colors.blue,
decorationThickness: 4,
decorationStyle: TextDecorationStyle.solid,
),
),
I ended up following Mathiew's answer to create the design i needed. This is how i got it to look:
For anyone interested here is the code i ended up using:
class _MonthCellDecoration extends Decoration {
const _MonthCellDecoration(this.showIndicator,
{this.borderColor, this.backgroundColor, this.indicatorColor});
final Color borderColor;
final Color backgroundColor;
final bool showIndicator;
final Color indicatorColor;
#override
BoxPainter createBoxPainter([VoidCallback onChanged]) {
return _MonthCellDecorationPainter(showIndicator,
borderColor: borderColor,
backgroundColor: backgroundColor,
indicatorColor: indicatorColor);
}
}
class _MonthCellDecorationPainter extends BoxPainter {
_MonthCellDecorationPainter(this.showIndicator,
{this.borderColor, this.backgroundColor, this.indicatorColor});
final Color borderColor;
final Color backgroundColor;
final bool showIndicator;
final Color indicatorColor;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final Rect bounds = offset & configuration.size;
_drawDecoration(canvas, bounds);
}
void _drawDecoration(Canvas canvas, Rect bounds) {
final Paint paint = Paint()..color = backgroundColor;
canvas.drawRRect(
RRect.fromRectAndRadius(bounds, const Radius.circular(5)), paint);
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 3;
paint.strokeCap = StrokeCap.round;
if (borderColor != null) {
paint.color = borderColor;
canvas.drawRRect(
RRect.fromRectAndRadius(bounds, const Radius.circular(5)), paint);
}
if (showIndicator) {
paint.color = indicatorColor;
paint.style = PaintingStyle.fill;
canvas.drawLine(Offset(bounds.left + 18, bounds.bottom - 7),
Offset(bounds.right - 18, bounds.bottom - 7), paint);
}
}
}
And to use it just add this code to the monthCellStyle property
specialDatesDecoration: _MonthCellDecoration(true,
backgroundColor: Colors.transparent,
indicatorColor: Colors.red),
any way to add indicator to BottomNavigatorBarItem like this image?
This package should be able to help you achieve it.
You can use a TabBar instead of a BottomNavigationBar using a custom decoration:
class TopIndicator extends Decoration {
#override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TopIndicatorBox();
}
}
class _TopIndicatorBox extends BoxPainter {
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Paint _paint = Paint()
..color = Colors.lightblue
..strokeWidth = 5
..isAntiAlias = true;
canvas.drawLine(offset, Offset(cfg.size!.width + offset.dx, 0), _paint);
}
}
Then pass the decoration to the TapBar using TapBar(indicator: TopIndicator ...).
To use the TabBar as the Scaffold.bottomNavigationBar, you will most likely want to wrap it in a Material to apply a background color:
Scaffold(
bottomNavigationBar: Material(
color: Colors.white,
child: TabBar(
indicator: TopIndicator(),
tabs: const <Widget>[
Tab(icon: Icon(Icons.home_outlined), text: 'Reward'),
...
],
),
),
...
)
Thanks Ara Kurghinyan for the original idea.
I've had the same problem and all the packages I found seem to require raw IconData, which makes it impossible to use widget functionality like number badges (e.g. the number of unread chat messages).
I came up with my own little solution; first, I made a widget to display the actual indicators:
class TabIndicators extends StatelessWidget {
final int _numTabs;
final int _activeIdx;
final Color _activeColor;
final Color _inactiveColor;
final double _padding;
final double _height;
const TabIndicators({
required int numTabs,
required int activeIdx,
required Color activeColor,
required double padding,
required double height,
Color inactiveColor = const Color(0x00FFFFFF),
Key? key }) :
_numTabs = numTabs,
_activeIdx = activeIdx,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_padding = padding,
_height = height,
super(key: key);
#override
Widget build(BuildContext context) {
final elements = <Widget>[];
for(var i = 0; i < _numTabs; ++i) {
elements.add(
Expanded(child:
Padding(
padding: EdgeInsets.symmetric(horizontal: _padding),
child: Container(color: i == _activeIdx ? _activeColor : _inactiveColor),
)
)
);
}
return
SizedBox(
height: _height,
child: Row(
mainAxisSize: MainAxisSize.max,
children: elements,
),
);
}
}
This can be prepended to the actual BottomNavigationBar like this:
bottomNavigationBuilder: (context, tabsRouter) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TabIndicators(
activeIdx: tabsRouter.activeIndex,
activeColor: Theme.of(context).primaryColor,
numTabs: 4,
padding: 25,
height: 4,
),
BottomNavigationBar(...
This works perfectly well for me, but to make it look decent, you'd have to set the BottomNavigationBar's elevation to zero, otherwise there's still a faint horizontal line between the indicators and the icons.
I want to design a widget of shape of a chat bubble where one corner is pinned and its height should adjust to the lines of the text? For now I'm using ClipRRect widget with some borderRadius. But I want one corner pinned. Any suggestions ?
UPDATE
I know this can be done using a stack but I'm looking for a better solution since I have to use it many times in a single view and using many stacks might affect the performs. ( correct me here if I'm wrong )
For someone who want this get done with library. You can add bubble: ^1.1.9+1 (Take latest) package from pub.dev and wrap your message with Bubble.
Bubble(
style: right ? styleMe : styleSomebody,
//Your message content child here...
)
Here right is boolean which tells the bubble is at right or left, Write your logic for that and add the style properties styleMe and styleSomebody inside your widget as shown below. Change style according to your theme.
double pixelRatio = MediaQuery.of(context).devicePixelRatio;
double px = 1 / pixelRatio;
BubbleStyle styleSomebody = BubbleStyle(
nip: BubbleNip.leftTop,
color: Colors.white,
elevation: 1 * px,
margin: BubbleEdges.only(top: 8.0, right: 50.0),
alignment: Alignment.topLeft,
);
BubbleStyle styleMe = BubbleStyle(
nip: BubbleNip.rightTop,
color: Colors.grey,
elevation: 1 * px,
margin: BubbleEdges.only(top: 8.0, left: 50.0),
alignment: Alignment.topRight,
);
Hi im also searching for a chat bubble shaped widget finally i made one.
I have made this in custom Painter and i'm not good at it.
import 'package:flutter/material.dart';
class ChatBubble extends CustomPainter {
final Color color;
final Alignment alignment;
ChatBubble({
#required this.color,
this.alignment,
});
var _radius = 10.0;
var _x = 10.0;
#override
void paint(Canvas canvas, Size size) {
if (alignment == Alignment.topRight) {
canvas.drawRRect(
RRect.fromLTRBAndCorners(
0,
0,
size.width - 8,
size.height,
bottomLeft: Radius.circular(_radius),
topRight: Radius.circular(_radius),
topLeft: Radius.circular(_radius),
),
Paint()
..color = this.color
..style = PaintingStyle.fill);
var path = new Path();
path.moveTo(size.width - _x, size.height - 20);
path.lineTo(size.width - _x, size.height);
path.lineTo(size.width, size.height);
canvas.clipPath(path);
canvas.drawRRect(
RRect.fromLTRBAndCorners(
size.width - _x,
0.0,
size.width,
size.height,
topRight: Radius.circular(_radius),
),
Paint()
..color = this.color
..style = PaintingStyle.fill);
} else {
canvas.drawRRect(
RRect.fromLTRBAndCorners(
_x,
0,
size.width,
size.height,
bottomRight: Radius.circular(_radius),
topRight: Radius.circular(_radius),
topLeft: Radius.circular(_radius),
),
Paint()
..color = this.color
..style = PaintingStyle.fill);
var path = new Path();
path.moveTo(0, size.height);
path.lineTo(_x, size.height);
path.lineTo(_x, size.height-20);
canvas.clipPath(path);
canvas.drawRRect(
RRect.fromLTRBAndCorners(
0,
0.0,
_x,
size.height,
topRight: Radius.circular(_radius),
),
Paint()
..color = this.color
..style = PaintingStyle.fill);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
copy and paste the above code and paste in your project
Align(
alignment: alignment, //Change this to Alignment.topRight or Alignment.topLeft
child: CustomPaint(
painter: ChatBubble(color: Colors.blue, alignment: alignment),
child: Container(
margin: EdgeInsets.all(10),
child: Stack(
children: <Widget>[
TextView("Hello World"),
],
),
),
),
)
paste this code where you have to show chatBubble Widget.
And i have also upload this code in bitbucket ChatBubble Widget Please be free if you have anything to contribute.
Sorry I am not able to show you the code for it but I can present an idea that might work if you implement it correctly. Suppose the Widget you made with ClipRect is called MyChatBubbleRect. Now, make another widget that draws a triangle using CustomPainter, let's call it MyChatBubbleTriangle, of course fill it with same color as the chat bubble but you can use a different color initially for debugging. Now that we have two widgets we can stack 'em together on top of each other and using Positioned widget over the MyChatBubbleTriangle. Something like this:
Stack(
children : [
MyChatBubbleRect(), // Maybe decrease the width a bit
Positioned(
top: 0,
right: 0,
child: MyChatBubbleTriangle()
)
]
)
This is just an idea I think you can pursue. Sorry couldn't provide the proper source code.
Chat Body
DecoratedBox(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8.0),
),
child: Text("your message goes here"),
);
Make a custom Triangle
class ChatBubbleTriangle extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()..color = Colors.blue;
var path = Path();
path.lineTo(-10, 0);
path.lineTo(0, 10);
path.lineTo(10, 0);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Use both of them within a stack wrapping ChatBubbleTriangle with Positioned() widget
I have a text form field in flutter I want to add a drop shadow to it. how should I do that?
final password = TextFormField(
obscureText: true,
autofocus: false,
decoration: InputDecoration(
icon: new Icon(Icons.lock, color: Color(0xff224597)),
hintText: 'Password',
fillColor: Colors.white,
filled: true,
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius:BorderRadius.circular(5.0),
borderSide: BorderSide(color: Colors.white, width: 3.0))
),
);
You can Wrap your TextFormField with a Material widget and edit its properties such as elevation and shadowColor.
Example:
Material(
elevation: 20.0,
shadowColor: Colors.blue,
child: TextFormField(
obscureText: true,
autofocus: false,
decoration: InputDecoration(
icon: new Icon(Icons.lock, color: Color(0xff224597)),
hintText: 'Password',
fillColor: Colors.white,
filled: true,
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius:BorderRadius.circular(5.0),
borderSide: BorderSide(color: Colors.white, width: 3.0))
),
),
)
You will Get something similar to the image below.
You can Use PhysicalModel to add shadow on each widget like this:
PhysicalModel(
borderRadius: BorderRadius.circular(25),
color: Colors.white,
elevation: 5.0,
shadowColor: Color(0xff44BD32),
child: CustomTextField(...
You can use this class as a wrapper for an element's border. It takes the border of a control and draws a shadow to that border above the control. To create an illusion that the shadow is behind the control, the shadow's area above the control gets cut off.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class DecoratedInputBorder extends InputBorder {
DecoratedInputBorder({
required this.child,
required this.shadow,
}) : super(borderSide: child.borderSide);
final InputBorder child;
final BoxShadow shadow;
#override
bool get isOutline => child.isOutline;
#override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => child.getInnerPath(rect, textDirection: textDirection);
#override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) => child.getOuterPath(rect, textDirection: textDirection);
#override
EdgeInsetsGeometry get dimensions => child.dimensions;
#override
InputBorder copyWith({BorderSide? borderSide, InputBorder? child, BoxShadow? shadow, bool? isOutline}) {
return DecoratedInputBorder(
child: (child ?? this.child).copyWith(borderSide: borderSide),
shadow: shadow ?? this.shadow,
);
}
#override
ShapeBorder scale(double t) {
final scalledChild = child.scale(t);
return DecoratedInputBorder(
child: scalledChild is InputBorder ? scalledChild : child,
shadow: BoxShadow.lerp(null, shadow, t)!,
);
}
#override
void paint(Canvas canvas, Rect rect, {double? gapStart, double gapExtent = 0.0, double gapPercentage = 0.0, TextDirection? textDirection}) {
final clipPath = Path()
..addRect(const Rect.fromLTWH(-5000, -5000, 10000, 10000))
..addPath(getInnerPath(rect), Offset.zero)
..fillType = PathFillType.evenOdd;
canvas.clipPath(clipPath);
final Paint paint = shadow.toPaint();
final Rect bounds = rect.shift(shadow.offset).inflate(shadow.spreadRadius);
canvas.drawPath(getOuterPath(bounds), paint);
child.paint(canvas, rect, gapStart: gapStart, gapExtent: gapExtent, gapPercentage: gapPercentage, textDirection: textDirection);
}
#override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is DecoratedInputBorder && other.borderSide == borderSide && other.child == child && other.shadow == shadow;
}
#override
int get hashCode => hashValues(borderSide, child, shadow);
#override
String toString() {
return '${objectRuntimeType(this, 'DecoratedInputBorder')}($borderSide, $shadow, $child)';
}
}
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
inputDecorationTheme: InputDecorationTheme(
border: DecoratedInputBorder(
child: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16.0)),
),
shadow: const BoxShadow(
color: Colors.blue,
blurRadius: 15,
),
),
),
),
It should looks this way:
Interactive example: https://dartpad.dev/?id=35f1249b52d177d47bc91c87d0a8c08c
Alternatively, you can use my package control_style. It implements a deeper realisation of this approach.
Here is a possible solution where the BoxShadow is only displayed behind the TextField, but is not expanding vertically if an error text is displayed.
My solution was to use the Stack widget to create an additional Container behind the actual TextField that is responsible for displaying the shadow.
A TextPainter is used to determine the height of the error text depending on its style:
import 'package:flutter/material.dart';
class TextFieldWithBoxShadow extends StatelessWidget {
final String? errorText;
final String? labelText;
final TextEditingController? controller;
final double height;
const TextFieldWithBoxShadow({
Key? key,
this.errorText,
this.labelText,
this.controller,
this.height = 40,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final errorStyle = const TextStyle(
fontSize: 14,
);
// Wrap everything in LayoutBuilder so that the available maxWidth is taken into account for the height calculation (important if you error text exceeds one line)
return LayoutBuilder(builder: (context, constraints) {
// Use tp to calculate the height of the errorText
final textPainter = TextPainter()
..text = TextSpan(text: errorText, style: errorStyle)
..textDirection = TextDirection.ltr
..layout(maxWidth: constraints.maxWidth);
final heightErrorMessage = textPainter.size.height + 8;
return Stack(
children: [
// Separate container with identical height of text field which is placed behind the actual textfield
Container(
height: height,
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.black,
blurRadius: 3,
offset: Offset(3, 3),
),
],
borderRadius: BorderRadius.circular(
10.0,
),
),
),
Container(
// Add height of error message if it is displayed
height: errorText != null ? height + heightErrorMessage : height,
child: TextField(
decoration: InputDecoration(
fillColor: Colors.black,
filled: true,
errorStyle: errorStyle,
errorText: errorText,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
10.0,
),
),
labelText: labelText,
),
controller: controller,
),
),
],
);
});
}
}
The problem when we use a Container, Material or any other Widget to wrap the input text field in order to apply the shadow is if we use the hint text, error text or any other property that change the size of our textbox, the design would be broken.
Intead of wrapping the input in another widget you could use custom painter extending the InputBorder class.
For example:
class MyInputBorder extends OutlineInputBorder {}
Copy the following methods from the OutlineInputBorder implementation (Used for this example) to your new class:
_gapBorderPath
_cornersAreCircular
paint
Then in the paint method you could add the following lines
Path path = Path();
path.addRRect(center);
canvas.drawShadow(path, Colors.black, 4, true);
The above lines must be included before the canvas.drawRRect line:
Example:
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
// paint the shadow for the outlined shape
Path path = Path();
path.addRRect(center);
canvas.drawShadow(path, shadowColor!, elevation, true);
canvas.drawRRect(center, paint);
} else {... other code omitted to keep simple}
Then, in your Widget, use the new Input border:
TextField(
decoration: InputDecoration(
border: MyInputBorder()
),
)
The result produced looks like the following without any of the issues of wrapper solutions:
text box with shadow
Here is a complete sample implementation, the post is in spanish but it has the idea explained: Full article for reference
#mrramos answer was almost complete but this code wouldn't give the desired result, I read the article suggested and implemented my own class, my use case is only a shadow for the text field for when its selected therefore the naming of it.
Quick explanation of this as its a lot to read and most of it is not necessary to understand just to implement a simple shadow.
paint() method is copied from OutlineInputBorder as _cornersAreCircular() and _gapBorderPath()
There is an addition of these few lines in the paint method for it to give the shadow.
Path path = Path();
path.addRRect(center);
canvas.drawShadow(path, Colors.black, 5, true);
final shadowPaint = Paint();
shadowPaint.strokeWidth = 0;
shadowPaint.color = Colors.white;
shadowPaint.style = PaintingStyle.fill;
canvas.drawRRect(center, shadowPaint);
canvas.drawRRect(center, paint);
Complete file class.
import 'package:flutter/material.dart';
import 'dart:ui' show lerpDouble;
import 'dart:math' as math;
class SelectedInputBorderWithShadow extends OutlineInputBorder {
const SelectedInputBorderWithShadow({
BorderSide borderSide = const BorderSide(),
borderRadius = const BorderRadius.all(Radius.circular(5)),
gapPadding = 4.0,
}) : super(
borderSide: borderSide,
borderRadius: borderRadius,
gapPadding: gapPadding,
);
static bool _cornersAreCircular(BorderRadius borderRadius) {
return borderRadius.topLeft.x == borderRadius.topLeft.y &&
borderRadius.bottomLeft.x == borderRadius.bottomLeft.y &&
borderRadius.topRight.x == borderRadius.topRight.y &&
borderRadius.bottomRight.x == borderRadius.bottomRight.y;
}
Path _gapBorderPath(
Canvas canvas, RRect center, double start, double extent) {
// When the corner radii on any side add up to be greater than the
// given height, each radius has to be scaled to not exceed the
// size of the width/height of the RRect.
final RRect scaledRRect = center.scaleRadii();
final Rect tlCorner = Rect.fromLTWH(
scaledRRect.left,
scaledRRect.top,
scaledRRect.tlRadiusX * 2.0,
scaledRRect.tlRadiusY * 2.0,
);
final Rect trCorner = Rect.fromLTWH(
scaledRRect.right - scaledRRect.trRadiusX * 2.0,
scaledRRect.top,
scaledRRect.trRadiusX * 2.0,
scaledRRect.trRadiusY * 2.0,
);
final Rect brCorner = Rect.fromLTWH(
scaledRRect.right - scaledRRect.brRadiusX * 2.0,
scaledRRect.bottom - scaledRRect.brRadiusY * 2.0,
scaledRRect.brRadiusX * 2.0,
scaledRRect.brRadiusY * 2.0,
);
final Rect blCorner = Rect.fromLTWH(
scaledRRect.left,
scaledRRect.bottom - scaledRRect.blRadiusY * 2.0,
scaledRRect.blRadiusX * 2.0,
scaledRRect.blRadiusX * 2.0,
);
// This assumes that the radius is circular (x and y radius are equal).
// Currently, BorderRadius only supports circular radii.
const double cornerArcSweep = math.pi / 2.0;
final double tlCornerArcSweep = math.acos(
(1 - start / scaledRRect.tlRadiusX).clamp(0.0, 1.0),
);
final Path path = Path()..addArc(tlCorner, math.pi, tlCornerArcSweep);
if (start > scaledRRect.tlRadiusX)
path.lineTo(scaledRRect.left + start, scaledRRect.top);
const double trCornerArcStart = (3 * math.pi) / 2.0;
const double trCornerArcSweep = cornerArcSweep;
if (start + extent < scaledRRect.width - scaledRRect.trRadiusX) {
path.moveTo(scaledRRect.left + start + extent, scaledRRect.top);
path.lineTo(scaledRRect.right - scaledRRect.trRadiusX, scaledRRect.top);
path.addArc(trCorner, trCornerArcStart, trCornerArcSweep);
} else if (start + extent < scaledRRect.width) {
final double dx = scaledRRect.width - (start + extent);
final double sweep = math.asin(
(1 - dx / scaledRRect.trRadiusX).clamp(0.0, 1.0),
);
path.addArc(trCorner, trCornerArcStart + sweep, trCornerArcSweep - sweep);
}
return path
..moveTo(scaledRRect.right, scaledRRect.top + scaledRRect.trRadiusY)
..lineTo(scaledRRect.right, scaledRRect.bottom - scaledRRect.brRadiusY)
..addArc(brCorner, 0.0, cornerArcSweep)
..lineTo(scaledRRect.left + scaledRRect.blRadiusX, scaledRRect.bottom)
..addArc(blCorner, math.pi / 2.0, cornerArcSweep)
..lineTo(scaledRRect.left, scaledRRect.top + scaledRRect.tlRadiusY);
}
#override
void paint(
Canvas canvas,
Rect rect, {
double? gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection? textDirection,
}) {
assert(gapExtent != null);
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
assert(_cornersAreCircular(borderRadius));
final Paint paint = borderSide.toPaint();
final RRect outer = borderRadius.toRRect(rect);
final RRect center = outer.deflate(borderSide.width / 2.0);
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
Path path = Path();
path.addRRect(center);
canvas.drawShadow(path, Colors.black, 5, true);
final shadowPaint = Paint();
shadowPaint.strokeWidth = 0;
shadowPaint.color = Colors.white;
shadowPaint.style = PaintingStyle.fill;
canvas.drawRRect(center, shadowPaint);
canvas.drawRRect(center, paint);
} else {
final double extent =
lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage)!;
switch (textDirection!) {
case TextDirection.rtl:
final Path path = _gapBorderPath(canvas, center,
math.max(0.0, gapStart + gapPadding - extent), extent);
canvas.drawPath(path, paint);
break;
case TextDirection.ltr:
final Path path = _gapBorderPath(
canvas, center, math.max(0.0, gapStart - gapPadding), extent);
canvas.drawPath(path, paint);
break;
}
}
}
}
and my result looks like this.
You can wrap TextFormField into Container add shadow too Container this will add shadow to you TextFormFieldbut it also adds color to TextFormField.
To remove Color from TextFormField use fillColor and filled property on TextFormField.
You Can control the opacity of a color line Colors.black.withOpacity(0.3).
Checkout Code Below:
final Widget password = Container(
decoration: BoxDecoration(
boxShadow: [
const BoxShadow(
blurRadius: 8,
),
],
borderRadius: BorderRadius.circular(5.0),
),
child: TextFormField(
obscureText: true,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
prefixIcon: const Icon(
Icons.lock,
color: Color(0xff224597),
),
hintText: 'Password',
contentPadding: const EdgeInsets.fromLTRB(
20.0,
10.0,
20.0,
10.0,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
borderSide: const BorderSide(
color: Colors.white,
width: 3.0,
),
),
),
),
);
YOU CAN CHECKOUT THE OUTPUT HERE
How to decorate text stroke in Flutter?
It's like -webkit-text-stroke - CSS
Stroke has been possible without workarounds since the addition of foreground paints in TextStyle. An explicit example of stroke under fill bordered text has been added in the TextStyle documentation: https://master-api.flutter.dev/flutter/painting/TextStyle-class.html#painting.TextStyle.6
This example is reproduced here:
Stack(
children: <Widget>[
// Stroked text as border.
Text(
'Greetings, planet!',
style: TextStyle(
fontSize: 40,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6
..color = Colors.blue[700],
),
),
// Solid text as fill.
Text(
'Greetings, planet!',
style: TextStyle(
fontSize: 40,
color: Colors.grey[300],
),
),
],
)
Stroke by itself is possible by removing the Stack and just using the first stroke Text widget by itself. The stroke/fill order can also be adjusted by swapping the first and second Text widget.
I was also looking for this, wasn't able to find it. But I did find a workaround using 4 shadows in the TextStyle:
Text("Border test",
style: TextStyle(
inherit: true,
fontSize: 48.0,
color: Colors.pink,
shadows: [
Shadow( // bottomLeft
offset: Offset(-1.5, -1.5),
color: Colors.white
),
Shadow( // bottomRight
offset: Offset(1.5, -1.5),
color: Colors.white
),
Shadow( // topRight
offset: Offset(1.5, 1.5),
color: Colors.white
),
Shadow( // topLeft
offset: Offset(-1.5, 1.5),
color: Colors.white
),
]
),
);
I also opened an Issue on GitHub: https://github.com/flutter/flutter/issues/24108
Inspired by this article, to achieve the effect, I prefer to use a technique that mixes two Text widgets and TextStype.foreground property with custom Paint():
class StrokeText extends StatelessWidget {
final String text;
final double fontSize;
final FontWeight fontWeight;
final Color color;
final Color strokeColor;
final double strokeWidth;
const StrokeText(
this.text, {
Key key,
this.fontSize,
this.fontWeight,
this.color,
this.strokeColor,
this.strokeWidth,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Stack(
children: [
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
foreground: Paint()..color = color,
),
),
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
foreground: Paint()
..strokeWidth = strokeWidth
..color = strokeColor
..style = PaintingStyle.stroke,
),
),
],
);
}
}
If you prefer the shadows method, you can configure the stroke width using :
/// Outlines a text using shadows.
static List<Shadow> outlinedText({double strokeWidth = 2, Color strokeColor = Colors.black, int precision = 5}) {
Set<Shadow> result = HashSet();
for (int x = 1; x < strokeWidth + precision; x++) {
for(int y = 1; y < strokeWidth + precision; y++) {
double offsetX = x.toDouble();
double offsetY = y.toDouble();
result.add(Shadow(offset: Offset(-strokeWidth / offsetX, -strokeWidth / offsetY), color: strokeColor));
result.add(Shadow(offset: Offset(-strokeWidth / offsetX, strokeWidth / offsetY), color: strokeColor));
result.add(Shadow(offset: Offset(strokeWidth / offsetX, -strokeWidth / offsetY), color: strokeColor));
result.add(Shadow(offset: Offset(strokeWidth / offsetX, strokeWidth / offsetY), color: strokeColor));
}
}
return result.toList();
}
Use it like this :
Text(
'My text',
style: TextStyle(shadows: outlinedText(strokeColor: Colors.blue)),
);
Inspired by #Gary Qian's answer
Widget textWithStroke({String text, String fontFamily, double fontSize: 12, double strokeWidth: 1, Color textColor: Colors.white, Color strokeColor: Colors.black}) {
return Stack(
children: <Widget>[
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontFamily: fontFamily,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..color = strokeColor,
),
),
Text(text, style: TextStyle(fontFamily: fontFamily, fontSize: fontSize, color: textColor)),
],
);
}
This is #Aleh's answer migrated to null-safety and with some more flexibility.
Simply paste this inside a new file, and use freely.
import 'package:flutter/widgets.dart';
/// Places a stroke around text to make it appear outlined
///
/// Adapted from https://stackoverflow.com/a/55559435/11846040
class OutlinedText extends StatelessWidget {
/// Text to display
final String text;
/// Original text style (if you weren't outlining)
///
/// Do not specify `color` inside this: use [textColor] instead.
final TextStyle style;
/// Text color
final Color textColor;
/// Outline stroke color
final Color strokeColor;
/// Outline stroke width
final double strokeWidth;
/// Places a stroke around text to make it appear outlined
///
/// Adapted from https://stackoverflow.com/a/55559435/11846040
const OutlinedText(
this.text, {
Key? key,
this.style = const TextStyle(),
required this.textColor,
required this.strokeColor,
required this.strokeWidth,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Stack(
children: [
Text(
text,
style: style.copyWith(foreground: Paint()..color = textColor),
),
Text(
text,
style: style.copyWith(
foreground: Paint()
..strokeWidth = strokeWidth
..color = strokeColor
..style = PaintingStyle.stroke,
),
),
],
);
}
}
I created a package using the same logic shared here.
I also make it possible to add multiple strokes at once.
package: https://pub.dev/packages/outlined_text
DEMO