如何在Flutter中为TextFormField添加阴影效果

33

我在Flutter中有一个文本表单字段,我想为其添加投影。我该怎么做?

 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))
     ),
   );
7个回答

48

您可以将TextFormField包装在Material小部件中,并编辑其属性,例如elevationshadowColor

示例:

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))
                ),
              ),
            )  

您将获得类似下面图片的东西。
enter image description here


不提升权限,直接指定阴影的偏移量/透明度是否可能? - Qiming
30
如果不使用helperTexterrorText,它看起来很好。否则,阴影会占用额外的空间,看起来不太好。:( - BambinoUA
color: Colors.transparent 添加到 Material 中,可以在使用更高的 BorderRadius.circular 值时移除方形形状。 - lolelo
1
文本字段/文本表单字段的边界半径也需要与设置为“Material”的边界半径相匹配。 - lolelo

16
您可以使用这个类作为元素边框的包装器。它将控件的边框绘制在控件之上,并在该边框上方绘制一个阴影。为了营造阴影在控件后面的假象,阴影在控件上方的区域被切断。
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,
        ),
      ),
    ),
  ),

它应该看起来像这样:

在此输入图片描述

交互示例:https://dartpad.dev/?id=35f1249b52d177d47bc91c87d0a8c08c

或者,您可以使用我的包 control_style。它实现了更深入的方法。


10
你可以使用 PhysicalModel 来为每个小部件添加阴影,就像这样:
PhysicalModel(
   borderRadius: BorderRadius.circular(25),
   color: Colors.white,
   elevation: 5.0,
   shadowColor: Color(0xff44BD32),
   child: CustomTextField(...

这绝对是最优秀的答案! - undefined

5
这里有一个可能的解决方案,其中 BoxShadow 仅显示在 TextField 后面,但如果显示错误文本,则不会垂直扩展。

enter image description here

我的解决方案是使用Stack小部件创建额外的Container,放在实际TextField后面,负责显示阴影。
使用TextPainter根据其样式确定错误文本的高度:
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,
            ),
          ),
        ],
      );
    });
  }
}

4
当我们使用容器、材料或任何其他小部件来包装输入文本字段以应用阴影时,存在一个问题,即如果我们使用提示文本、错误文本或任何其他会改变文本框大小的属性,设计将被破坏。
相反,您可以使用扩展InputBorder类的自定义绘图器来代替将输入内容包装在另一个小部件中。例如:
class MyInputBorder extends OutlineInputBorder {}

将以下方法从默认提供的《边框装饰实现》(在本例中使用)复制到您的新类中:

_gapBorderPath _cornersAreCircular paint

然后在paint方法中,您可以添加以下几行代码:

Path path = Path();
path.addRRect(center);
canvas.drawShadow(path, Colors.black, 4, true);

上述代码必须在canvas.drawRRect代码之前添加: 示例:
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}

接下来,在您的小部件中使用新的输入边框:

TextField(
         decoration: InputDecoration(
           border: MyInputBorder()
         ),
       )

使用这种方法,生成的结果如下图所示,没有任何包装器解决方案所遇到的问题:

带阴影的文本框

以下是一个完整的示例实现,该文章是用西班牙语写的,但已经解释了其中的思路:全文参考


非常好的答案,但它是不完整的,而且你建议的代码行并不能得到你想要的结果。 - Santiago

1
@mrramos的答案几乎完整,但这段代码不会产生期望的结果。我阅读了建议的文章并实现了自己的类。我的用例仅是文本字段选中时的阴影,因此命名为ShadowTextField。
简要说明如下,因为很多内容不必理解就可以实现一个简单的阴影效果: paint()方法从OutlineInputBorder复制而来,包括_cornersAreCircular()和_gapBorderPath()。
在paint方法中添加了以下几行代码以产生阴影效果。
     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);

完整的文件类。

    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;
      }
    }
  }
}

我的结果看起来像这样。

在此输入图像描述


当我在文本字段中输入数据然后取消焦点时,阴影不会显示。如何始终显示阴影? - Abir Ahsan
当我在文本字段中输入数据然后取消焦点时,阴影不会显示。如何始终显示阴影? - undefined

1
  • 您可以将TextFormField包装到Container中,并向Container添加阴影,这将为您的TextFormField添加阴影,但它也会为TextFormField添加颜色。

  • 要从TextFormField中删除颜色,请在TextFormField上使用fillColor和filled属性。

  • 您可以控制颜色线的不透明度Colors.black.withOpacity(0.3)

请查看以下代码:

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,
        ),
      ),
    ),
  ),
);

您可以在此处查看输出结果


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接