Flutter UI - Clip 裁剪系 Widget

5,097 阅读4分钟

Flutter 提供了一些可供裁剪的 Widget,这可比 android 动不动就得自定义方便多了,这些 Widget 大多以 Clip 开头,可以裁剪图片、布局成圆形、圆角,矩形

  • ClipOval
  • CircleAvatar
  • ClipRRect
  • ClipPath
  • CustomClipper
  • ``

CustomClipper 是自定义裁剪,是一个接口,在 ClipPath 中已经使用过来,同样适用于其他 Clip 打头的 Widget


ClipOval

ClipOval 看名字大家应该能猜到,对 -> 就是圆形裁剪

ClipOval 的属性很简单,大家看一下:

ClipOval({ 
Key key,
this.clipper, //裁剪路径
this.clipBehavior = Clip.antiAlias, 
Widget child 
}) 

代码:

class DD extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ClipOval(
          child: Container(
            child: Image(
              image: AssetImage("assets/icons/icon_2.jpg"),
              height: 300,
              width: 300,
              fit: BoxFit.cover,
            ),
          ),
        ),
      ],
    );
  }
}


CircleAvatar

CircleAvatar其实和 ClipOval 一样,都是裁剪成圆形的,区别是 CircleAvatar 可以裁剪复杂布局,不过我觉得这名字起的真是蛋疼,怎么叫别人记啊

class FF extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      backgroundImage: AssetImage(
        "assets/icons/icon_3.png",
      ),
      maxRadius: 200,
      child: Text(
        "AAAAAAAAAA",
        style: TextStyle(
          fontSize: 30,
          color: Colors.lightGreenAccent,
        ),
      ),
    );
  }
}


ClipRRect

ClipRRect 裁剪圆角矩形,矩形也可以,不过单纯矩形的话也不用他了

class FF extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: Image(
        image: AssetImage("assets/icons/icon_3.png"),
        height: 300,
        width: 300,
        fit: BoxFit.cover,
      ),
    );
  }
}


ClipPath

ClipPath 哈哈,熟悉 path 的同学是不是又找到感觉啦,android 里 path 可是虐的我不要不要的,Flutter 这里也是一样啊,好在基本的 path 操作都 OK 了

先看下 ClipPath 的属性:

  const ClipPath({
    Key key,
    this.clipper,
    this.clipBehavior = Clip.antiAlias,
    Widget child,
  }) : super(key: key, child: child);

其实上面几个 clip widget 属性都一样,clipper 这个属性可以让我们自己提供裁剪区域,具体到 ClipPath 这里就是提供一个 path 路径了

clipper 属性需要我们自己实现一个 CustomClipper 的实现类:

abstract class CustomClipper<T> {

  T getClip(Size size);

  Rect getApproximateClipRect(Size size) => Offset.zero & size;

  bool shouldReclip(covariant CustomClipper<T> oldClipper);
}

我们需要重写 CustomClipper 里面的2个方法:

  • CustomClipper 接收一个泛型,泛型类型表示返回图形的形状,比如我们我们返回 path 路径
  • getClip 方法里 size 我们可以拿到 widget 的大小,饭后根据这个尺寸去裁剪图形并返回
  • shouldReclip 方法是询问我们要不要重新裁剪

这里我提供2个例子:

1. ClipPath 实现复杂图形背景

来源:futter ClipPath 使用

先看图:

这个常用吧,产品不这么设计就不舒服斯基.......这个其实就是先用Stack实现重叠布局,然后背景 Widget 用ClipPath做的裁剪

  • 先看怎么裁剪的:
class HeaderColor extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    //x坐标为0.0 y坐标为手机高度一半
    //到x坐标为手机宽度 到 手机宽度的一半减去100 达到斜线的结果
    //到x坐标为手机宽度 到 y坐标为手机宽度
    //完成
    var path = Path()
      ..lineTo(0.0, size.height / 2)
      ..lineTo(size.width, size.height / 2 - 100)
      ..lineTo(size.width, 0)
      ..close();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
  • 然后是综合布局:
  Widget build(BuildContext context) {
    return new Scaffold(
        body: ClipPath(//剪切区域
      child: Stack(
        fit: StackFit.expand,
        children: [
          Container(
            decoration: BoxDecoration(
                color: Color(0xff622F74),
                gradient: LinearGradient(//渐变色
                    colors: [Colors.blue, Colors.deepOrangeAccent],//  blue deepOrangeAccent
                    begin: Alignment.centerRight, //起点
                    end: Alignment(-1.0, -1.0))),//终点
          ),
          Column(
            children: [
              Padding(
                  padding: EdgeInsets.only(top: 50.0),
                  child: Container(
                      width: 100.0,
                      height: 100.0,
                      decoration: new BoxDecoration(
                          border: Border.all(color: Colors.amber, width: 2.0),
                          color: const Color(0xFFFFFFFF), // border color
                          shape: BoxShape.circle,
                          image: DecorationImage(
                              image: AssetImage("images/avatar.jpeg"))))),
            ],
          ),
        ],
      ),
      clipper: HeaderColor(),//主要部分
    ));

2. ClipPath 实现图片五角星

五角星 path 来源:flutter使用剪裁制作评分控件

继续看图:

  • 直接看怎么裁剪的吧:
import 'package:flutter/material.dart';
import 'dart:math' as Math;

class StarCliper extends CustomClipper<Path>{

  final double radius;

  StarCliper({this.radius});

  /// 角度转弧度公式
  double degree2Radian(int degree) {
    return (Math.pi * degree / 180);
  }

  @override
  Path getClip(Size size) {
    double radius = this.radius;
    Path path = new Path();
    double radian = degree2Radian(36);// 36为五角星的角度
    double radius_in = (radius * Math.sin(radian / 2) / Math
        .cos(radian)); // 中间五边形的半径

    path.moveTo((radius * Math.cos(radian / 2)), 0.0);// 此点为多边形的起点
    path.lineTo((radius * Math.cos(radian / 2) + radius_in
        * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) * 2),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) + radius_in
        * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo(
        (radius * Math.cos(radian / 2) + radius
            * Math.sin(radian)), (radius + radius
        * Math.cos(radian)));
    path.lineTo((radius * Math.cos(radian / 2)),
        (radius + radius_in));
    path.lineTo(
        (radius * Math.cos(radian / 2) - radius
            * Math.sin(radian)), (radius + radius
        * Math.cos(radian)));
    path.lineTo((radius * Math.cos(radian / 2) - radius_in
        * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo(0.0, (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) - radius_in
        * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));

    path.close();// 使这些点构成封闭的多边形

    return path;
  }

  @override
  bool shouldReclip(StarCliper oldClipper) {
    return this.radius != oldClipper.radius;
  }

}
  • 综合布局:
class FF extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: StarCliper(radius: 150.0),
      child: Image(
        image: AssetImage("assets/icons/icon_3.png"),
        height: 300,
        width: 300,
        fit: BoxFit.cover,
      ),
    );
  }
}