如何在 Flutter 中为 TextFormField 添加阴影

How to add drop shadow to TextFormField In Flutter

我有一个 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))
     ),
   );

您可以用 Material 小部件包装 TextFormField 并编辑其属性,例如 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))
                ),
              ),
            )  

您将获得类似于下图的内容。

使用容器小部件的投影效果

我们可以传颜色,设置偏移值,blurRadius和spreadRadius。让我们看看代码

Container(
  child: TextField(
    decoration: InputDecoration(
      fillColor: Colors.white,
      filled: true,
    ),
  ),
  
  decoration: BoxDecoration(
    boxShadow: [
      BoxShadow(
        color: Colors.black38,
        blurRadius: 25,
        offset: const Offset(0, 10),
      ),
    ],
  ),
);

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

我的解决方案是使用 Stack 小部件在负责显示阴影的实际 TextField 后面创建一个额外的 Container

一个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,
            ),
          ),
        ],
      );
    });
  }
}

您可以使用 PhysicalModel 在每个小部件上添加阴影,如下所示:

PhysicalModel(
   borderRadius: BorderRadius.circular(25),
   color: Colors.white,
   elevation: 5.0,
   shadowColor: Color(0xff44BD32),
   child: CustomTextField(...
  • 您可以将 TextFormField 包装到 Container 中,也可以添加阴影 Container这将为您添加阴影 TextFormFieldbut 它还会为 TextFormField.

    添加颜色
  • 要从 TextFormField 中删除颜色,请使用 fillColor 并在 TextFormField 上填充 属性。

  • 您可以控制颜色线的不透明度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,
        ),
      ),
    ),
  ),
);

YOU CAN CHECKOUT THE OUTPUT HERE

当我们使用容器、Material 或任何其他小部件来包裹输入文本字段以应用阴影时的问题是,如果我们使用提示文本、错误文本或任何其他 属性 改变文本框的大小,设计会被破坏。

您可以使用自定义画家扩展 InputBorder class,而不是将输入包装在另一个小部件中。 例如:

class MyInputBorder extends OutlineInputBorder {}

将以下方法从 OutlineInputBorder 实现(用于此示例)复制到新的 class: _gapBorderPath _cornersAreCircular 油漆

然后在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}

然后,在您的 Widget 中,使用新的输入边框:

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

生成的结果如下所示,没有任何包装解决方案的问题:

text box with shadow

这是一个完整的示例实现,post 是西班牙语,但它解释了这个想法:Full article for reference

@mrramos 的回答几乎完成,但这段代码不会给出预期的结果,我阅读了建议的文章并实现了我自己的 class,我的用例只是文本字段的阴影它的选择因此命名它。 对此的快速解释,因为它需要阅读很多内容,而且大部分内容都不需要理解,只是为了实现一个简单的阴影。

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

完成文件 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;
      }
    }
  }
}

我的结果是这样的。

您可以使用此 class 作为元素边框的包装器。它采用控件的边框并在控件上方的边框上绘制阴影。为了营造阴影在控件后面的错觉,控件上方的阴影区域被截断。

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。它实现了这种方法的更深层次的实现。