本文仅做分析。项目作者见 github

本文重点:使用 ClipPath 搭配自定义路径 CustomClipper<Path> ,最后结合 自定义滑块 SliderTheme

GitHub 源地址: Before After

先上预览图:

预览图

原理分析图:

原理图

源码共两个文件:

  • rect_clipper.dart
  • custom_widget.dart

custom_widget.dart 是控件的主要实现。

rect_clipper.dart 是对不同方向 (裁剪)的实现。继承于 CustomClipper<Path> 


rect_clipper.dart: SizedImage

/// 使用 [SizedBox] 来限制图像大小
class SizedImage extends StatelessWidget {
  /// 传入的 WIDGET
  final Widget _image;

  /// 用作 [SizedBox] 的宽高,以限制 [_image] 的大小
  final double _height, _width, _imageCornerRadius;

  const SizedImage(
      this._image, this._height, this._width, this._imageCornerRadius,
      {Key key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 裁剪
    return ClipRRect(
      borderRadius: BorderRadius.circular(_imageCornerRadius),
      // 固定宽高
      child: SizedBox(
        height: _height,
        width: _width,
        child: _image,
      ),
    );
  }
}

这里使用 SizedBox 来限制传入的 widget 宽高。


rect_clipper.dart: CustomThumbShape

class CustomThumbShape extends SliderComponentShape {
  /// 滑块半径
  final double _thumbRadius;
  /// 滑块颜色
  final Color _thumbColor;

  CustomThumbShape(this._thumbRadius, this._thumbColor);

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
    return Size.fromRadius(_thumbRadius);
  }

  @override
  void paint(PaintingContext context, Offset center,
      {Animation<double> activationAnimation,
      Animation<double> enableAnimation,
        bool isDiscrete,
      TextPainter labelPainter,
      RenderBox parentBox,
      SliderThemeData sliderTheme,
      TextDirection textDirection,
      double value}) {
    final Canvas canvas = context.canvas;

    // 内圈
    final Paint paint = Paint()
    // 抗锯齿
      ..isAntiAlias = true
      // 描边宽度
      ..strokeWidth = 4.0
      ..color = _thumbColor
      ..style = PaintingStyle.fill;

    // 外圈
    final Paint paintStroke = Paint()
      ..isAntiAlias = true
      ..strokeWidth = 4.0
      ..color = _thumbColor
      ..style = PaintingStyle.stroke;

    canvas.drawCircle(
      center,
      _thumbRadius,
      paintStroke,
    );

    canvas.drawCircle(
      center,
      _thumbRadius - 6,
      paint,
    );

    // 画出一条"直线"
    canvas.drawRect(
        Rect.fromCenter(
            center: center, width: 4.0, height: parentBox.size.height),
        paint);
  }
}

CustomThumbShape 就是这个(绿圈内):

CustomThumbShape
CustomThumbShape

至于 CustomThumbShape 用在何处下面解释。


rect_clipper.dart: BeforeAfter, _BeforeAfter

class BeforeAfter extends StatefulWidget {
  /// image1
  final Widget beforeImage;
  // image2
  final Widget afterImage;

  /// 图像宽高
  final double imageHeight;
  final double imageWidth;

  /// 图像四角裁剪半径
  final double imageCornerRadius;

  /// 拖动指示器颜色
  final Color thumbColor;

  /// 指示器半径
  final double thumbRadius;

  /// 点击指示器的背景颜色
  final Color overlayColor;

  /// 拖动方向
  final bool isVertical;

  const BeforeAfter({
    Key key,
    @required this.beforeImage,
    @required this.afterImage,
    this.imageHeight,
    this.imageWidth,
    this.imageCornerRadius = 8.0,
    this.thumbColor = Colors.white,
    this.thumbRadius = 16.0,
    this.overlayColor,
    this.isVertical = false,
  })  : assert(beforeImage != null),
        assert(afterImage != null),
        super(key: key);

  @override
  _BeforeAfterState createState() => _BeforeAfterState();
}

class _BeforeAfterState extends State<BeforeAfter> {
  /// 裁剪程度
  double _clipFactor = 0.5;

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: <Widget>[
        // AFTER
        Padding(
          padding: widget.isVertical
              ? const EdgeInsets.symmetric(vertical: 24.0)
              : const EdgeInsets.symmetric(horizontal: 24.0),
          child: SizedImage(
            widget.afterImage,
            widget.imageHeight,
            widget.imageWidth,
            widget.imageCornerRadius,
          ),
        ),
        /// BEFORE
        Padding(
          padding: widget.isVertical
              ? const EdgeInsets.symmetric(vertical: 24.0)
              : const EdgeInsets.symmetric(horizontal: 24.0),
          child: ClipPath(
            clipper: widget.isVertical
                ? RectClipperVertical(_clipFactor)
                : RectClipper(_clipFactor),
            child: SizedImage(
              widget.beforeImage,
              widget.imageHeight,
              widget.imageWidth,
              widget.imageCornerRadius,
            ),
          ),
        ),
        Positioned.fill(
          child: SliderTheme(
            data: SliderThemeData(
              // slider 宽度调整为 0
              trackHeight: 0,
              overlayColor: widget.overlayColor,
              thumbShape:
                  CustomThumbShape(widget.thumbRadius, widget.thumbColor),
            ),
            child: widget.isVertical
                ? RotatedBox(
                    quarterTurns: 1,
                    child: Slider(
                      value: _clipFactor,
                      onChanged: (double factor) =>
                          setState(() => this._clipFactor = factor),
                    ),
                  )
                : Slider(
                    value: _clipFactor,
                    onChanged: (double factor) =>
                        setState(() => this._clipFactor = factor),
                  ),
          ),
        ),
      ],
    );
  }
}

从中我们可以看出两张图片置于 Stack 中,底部图片用 SizedImage 进行限制大小,顶部图片通用限制大小后被 ClipPath 包裹,ClipPath 顾名思义,用于裁剪 widget

我们根据是否垂直来使用不同的 clipper 。什么是垂直,见下图:

垂直

我们可以看到 RectClipper 传入了一个值,_clipFactor ,此值用于控制裁剪的程度(裁剪的百分比),后面又具体解释。


custom_widget.dart: RectClipper

class RectClipper extends CustomClipper<Path> {
  final double clipFactor;

  RectClipper(this.clipFactor);

  @override
  Path getClip(Size size) {
    Path path = Path();
    path.lineTo(size.width * clipFactor, 0.0);
    path.lineTo(size.width * clipFactor, size.height);
    path.lineTo(0.0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}

 这里代码用一张图解释:

clipFactor 代码图解

clipFactor 取值 0 ~ 1 。

至于这三行代码,我们继续用图表示:

    path.lineTo(size.width * clipFactor, 0);
    path.lineTo(size.width * clipFactor, size.height);
    path.lineTo(0.0, size.height);
RectClipper 图解

下面具体说一下  CustomThumbShape 

        Positioned.fill(
          child: SliderTheme(
            data: SliderThemeData(
              // slider 宽度调整为 0
              trackHeight: 0,
              overlayColor: widget.overlayColor,
              thumbShape:
                  CustomThumbShape(widget.thumbRadius, widget.thumbColor),
            ),
            child: widget.isVertical
                ? RotatedBox(
                    quarterTurns: 1,
                    child: Slider(
                      value: _clipFactor,
                      onChanged: (double factor) =>
                          setState(() => this._clipFactor = factor),
                    ),
                  )
                : Slider(
                    value: _clipFactor,
                    onChanged: (double factor) =>
                        setState(() => this._clipFactor = factor),
                  ),
          ),
        ),

 使用 SliderTheme 加上 SliderThemeData 创造一个自定义 slider, trackHeight: 0, 把 slider 进度条调整为 0 。

SliderTheme

上图是不调整 trackHeight 的效果。

然后通过判断是否为垂直,来决定是否使用 RotatedBox 旋转 Slider

以上就是 Before After 的分析笔记。


August

Android 开发者、影视后期内容(包装)制作者、Unity 2D游戏开发者

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注