Flutter控件之基类Widget封装

在短时间的接触Flutter之后,有一个问题一直摆在了明面上,那就是,Flutter中的Widget确实没有Android中的控件好用,在Android中,比如TextView,ImageView等等或者其他View,都有着自己非常广泛的属性和方法,比如宽,高,margin和padding,以及相关的点击事件,这在Flutter,对应的控件中,却少了这些基础又常用的属性,以至于每写一个Widget,如果想要实现点击事件,或者margin,padding,不得不用其他的Widget包裹一层,使用起来很是不方便,基于以上的背景,便萌生了一个封装基类的想法。

虽然之前接触过Flutter,但也是许久不用了,今再拾起,难免有些许不足,如果在封装上有哪些问题,还望不吝赐教。

本篇文章大致概述如下:

1、需要封装哪些属性

2、确定基类Widget

3、基类实现

4、相关总结

一、需要封装哪些属性

具体需要哪些属性,不是越多越好,也不是越少越好,而是基于实际的开发需求,拓展出常用的即可。

一个文本或者图片控件又或者是其他控件,在实际的开发中,哪些是我们需要考虑的?是不是最常见的就是自身的宽高,这是最常见且必须需要的,除了宽高,其自身的点击事件,也是频次居高不下的一个属性,所以,在基类Widget中,其宽、高、点击事件是必须要存在的,说到事件,除了点击事件之外,一些需求中的双击或者长按事件也是存在的,所以,尽量也封到基类中,便于子类控件的使用。

除此之外,像外边距、内边距、也是必不可少的属性,不敢说十个控件有九个用到,起码说也得一半以上的概率,所以,这也是要封装到基类中的;至于背景属性,比如圆角的,圆形的,空心的,实心的,这些看实际的项目使用,如果需要,也可以放到基类中。

初步罗列了一下,大致封装的属性如下,当然了,每个人的封装都有不同,主要还是要看实际的需求。

属性

类型

概述

width

double

height

double

margin

double

外边距统一设置(左上右下)

marginLeft

double

外边距(左)

marginTop

double

外边距(上)

marginRight

double

外边距(右)

marginBottom

double

外边距(下)

padding

double

内边距统一设置(左上右下)

paddingLeft

double

内边距(左)

paddingTop

double

内边距(上)

paddingRight

double

内边距(右)

paddingBottom

double

内边距(下)

onClick

方法

点击事件

onDoubleClick

方法

双击事件

onLongPress

方法

长按事件

backgroundColor

Color

背景颜色 和 decoration 二者取其一

strokeWidth

double

背景边框统一的宽度

strokeColor

Color

背景边框的颜色

solidColor

Color

背景填充颜色

radius

double

背景的角度,统一设置

leftTopRadius

double

背景左上角度

rightTopRadius

double

背景右上角度

leftBottomRadius

double

背景左下角度

rightBottomRadius

double

背景右下角度

isCircle

bool

背景是否是圆形

childWidget

Widget

传递的子控件

alignment

Alignment

位置

gradientColorList

List<Color>

渐变颜色集合

gradientColorStops

List<double>

渐变颜色值梯度,取值范围[0,1]

gradientBegin

Alignment

渐变起始位置

gradientEnd

Alignment

渐变结束位置

二、确定基类Widget

基类的Widget主要确定以下几个方面,第一就是,自定义一个抽象类还是非抽象类,第二、继承方式,采取有状态还是无状态,第三、关于组件的点击方式,如何进行实现。

一开始自己写的是一个抽象基类,毕竟在接下来的操作中,对于各个控件,我都会重新在原生的基础之上进行再次的封装,而不是独立的使用,这种情况下,抽象类是最合适的,向子类拓展出必须要实现的方法即可,但是这种情况下就有一个弊端,那就是,原生的控件无法享有这个基类的各个属性,没办法,最后又改为了非抽象类,这样,两种方式均可满足。

关于继承方式,对于一个页面而言,或多或少都是需要渲染数据,更新UI的,这种情况下继承StatefulWidget是肯定的,但是一般一个控件,都是别人来触发它,而它自己很少主动触发,所以,一般而言,我们继承StatelessWidget即可。

关于组件的点击方式,如果是非Button级别的,很少有控件自带点击事件,所以我们不得不自行实现,而在Flutter中提供了很多可以协助实现点击的组件,比如InkWell,GestureDetector,InkResponse,原始指针事件Listener,都为我们提供了丰富的触摸事件,下面简单的列举一下:

InkWell

InkWell(
      onLongPress: (){
        print("长按事件");
      },
      onDoubleTap: (){
        print("双击事件");
      },
      onTap: (){
        print("点击事件");
      }
      child: Container()
)

GestureDetector

return GestureDetector(
      child: const Text("首页"),
  		onLongPress: (){
        print("长按事件");
      },
      onDoubleTap: (){
        print("双击事件");
      },
      onTap: (){
        print("点击事件");
      },
      onPanDown: (DragDownDetails detail) {
        // 手指按下的相对于屏幕的位置
        print("手指按下回调");
      },
      onPanUpdate: (DragUpdateDetails detail) {
        print("手指滑动回调");
      },
      onPanEnd: (DragEndDetails detail) {
        print("手指停止滑动回调");
      },
  		// 垂直方向拖动事件
      onVerticalDragUpdate: (DragUpdateDetails details) {
      
      },
      // 水平方向拖动事件
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        
      },
    );

InkResponse

return InkResponse(
      child: const Text("点击"),
      onTap: () {
        //点击事件
        print("点击事件");
      },
      onLongPress: () {
        //长按事件
        print("长按事件");
      },
      onDoubleTap: () {
        //双击事件
        print("双击事件");
      },
    );

原始指针事件

return Listener(
      child: Container(
        child: const Text("测试"),
      ),
  		//手指按下回调
      onPointerDown: (PointerDownEvent event) {},
  		//手指移动回调
      onPointerMove: (PointerMoveEvent event) {},
  		//手指抬起回调
      onPointerUp: (PointerUpEvent event) {},
  		//触摸事件取消回调
      onPointerCancel: (PointerCancelEvent event) {},
    );

相关的属性有很多,大家可以看下相关源码,具体用哪个,我是认为,前三个都可以,毕竟都有相关的点击,双击,长按事件,如果你想要获取更多的触摸事件,那么就可以使用GestureDetector,如果只是点击,长按和双击,比较推荐InkWell,相对点击比较灵敏,当然了,具体使用哪个,还是要看自己。

三、基类实现

基类实现就比较的简单了,build方法中最外层用点击事件包裹,再往下用Container组件来包裹,目的用于宽高,margin,padding和背景等实现,圆角和圆形以及渐变用的是Container的属性decoration。

全部的源码如下,都是系统的api调用,没有特别难的。

import 'package:flutter/material.dart';

///AUTHOR:AbnerMing
///DATE:2023/5/11
///INTRODUCE:控件无状态基类

class BaseWidget extends StatelessWidget {
  final VoidCallback? onClick; //点击事件
  final VoidCallback? onDoubleClick; //双击事件
  final VoidCallback? onLongPress; //长按事件
  final double? width; //宽度
  final double? height; //高度
  final double? margin; //外边距,左上右下
  final double? marginLeft; //外边距,距离左边
  final double? marginTop; //外边距,距离上边
  final double? marginRight; //外边距,距离右边
  final double? marginBottom; //外边距,距离下边
  final double? padding; //内边距,左上右下
  final double? paddingLeft; //内边距,距离左边
  final double? paddingTop; //内边距,距离上边
  final double? paddingRight; //内边距,距离右边
  final double? paddingBottom; //内边距,距离下边
  final Color? backgroundColor; //背景颜色 和 decoration 二者取其一
  final double? strokeWidth; //背景边框统一的宽度
  final Color? strokeColor; //背景边框的颜色
  final Color? solidColor; //背景填充颜色
  final double? radius; //背景的角度
  final bool? isCircle; //背景是否是圆形
  final double? leftTopRadius; //背景左上角度
  final double? rightTopRadius; //背景 右上角度
  final double? leftBottomRadius; //背景 左下角度
  final double? rightBottomRadius; //背景 右下角度
  final Widget? childWidget; //子控件
  final Alignment? alignment; //位置
  final int? gradient; //渐变方式,为支持后续拓展,用int类型
  final List<Color>? gradientColorList; //渐变颜色
  final List<double>? gradientColorStops; //颜色值梯度,取值范围[0,1]
  final Alignment? gradientBegin; //渐变起始位置
  final Alignment? gradientEnd; //渐变结束位置

  //边框的颜色
  const BaseWidget(
      {super.key,
      this.width,
      this.height,
      this.margin,
      this.marginLeft,
      this.marginTop,
      this.marginRight,
      this.marginBottom,
      this.padding,
      this.paddingLeft,
      this.paddingTop,
      this.paddingRight,
      this.paddingBottom,
      this.backgroundColor,
      this.strokeWidth,
      this.strokeColor,
      this.solidColor,
      this.radius,
      this.isCircle,
      this.leftTopRadius,
      this.rightTopRadius,
      this.leftBottomRadius,
      this.rightBottomRadius,
      this.childWidget,
      this.alignment,
      this.gradient,
      this.gradientColorList,
      this.gradientColorStops,
      this.gradientBegin,
      this.gradientEnd,
      this.onClick,
      this.onDoubleClick,
      this.onLongPress});

  @override
  Widget build(BuildContext context) {
    return InkWell(
        highlightColor: Colors.transparent,
        // 透明色
        splashColor: Colors.transparent,
        // 透明色
        onTap: onClick,
        onDoubleTap: onDoubleClick,
        onLongPress: onLongPress,
        child: Container(
          width: width,
          height: height,
          alignment: alignment,
          margin: margin != null
              ? EdgeInsets.all(margin!)
              : EdgeInsets.only(
                  left: marginLeft != null ? marginLeft! : 0,
                  top: marginTop != null ? marginTop! : 0,
                  right: marginRight != null ? marginRight! : 0,
                  bottom: marginBottom != null ? marginBottom! : 0),
          padding: padding != null
              ? EdgeInsets.all(padding!)
              : EdgeInsets.only(
                  left: paddingLeft != null ? paddingLeft! : 0,
                  top: paddingTop != null ? paddingTop! : 0,
                  right: paddingRight != null ? paddingRight! : 0,
                  bottom: paddingBottom != null ? paddingBottom! : 0,
                ),
          color: backgroundColor,
          decoration: backgroundColor != null ? null : getDecoration(),
          child: childWidget ?? getWidget(context),
        ));
  }

  /*
  * 获取Decoration
  * */
  Decoration? getDecoration() {
    BorderRadiusGeometry? borderRadiusGeometry;
    if (radius != null) {
      //所有的角度
      borderRadiusGeometry = BorderRadius.all(Radius.circular(radius!));
    } else {
      //否则就是,各个角度
      borderRadiusGeometry = BorderRadius.only(
          topLeft: Radius.circular(leftTopRadius != null ? leftTopRadius! : 0),
          topRight:
              Radius.circular(rightTopRadius != null ? rightTopRadius! : 0),
          bottomLeft:
              Radius.circular(leftBottomRadius != null ? leftBottomRadius! : 0),
          bottomRight: Radius.circular(
              rightBottomRadius != null ? rightBottomRadius! : 0));
    }
    Gradient? tGradient;
    if (gradient != null) {
      tGradient = LinearGradient(
        colors: gradientColorList != null ? gradientColorList! : [],
        // 设置有哪些渐变色
        begin: gradientBegin != null ? gradientBegin! : Alignment.centerLeft,
        // 渐变色开始的位置,默认 centerLeft
        end: gradientEnd != null ? gradientEnd! : Alignment.centerRight,
        // 渐变色结束的位置,默认 centerRight
        stops: gradientColorStops, // 颜色值梯度,取值范围[0,1],长度要和 colors 的长度一样
      );
    }
    Decoration? widgetDecoration = BoxDecoration(
      gradient: tGradient,
      //背景颜色
      color: solidColor != null ? solidColor! : Colors.transparent,
      //圆角半径
      borderRadius: isCircle == true ? null : borderRadiusGeometry,
      //是否是圆形
      shape: isCircle == true ? BoxShape.circle : BoxShape.rectangle,
      //边框线宽、颜色
      border: Border.all(
          width: strokeWidth != null ? strokeWidth! : 0,
          color: strokeColor != null ? strokeColor! : Colors.transparent),
    );
    return widgetDecoration;
  }

  /*
  * 获取控件
  * */
  Widget? getWidget(BuildContext context) {
    return null;
  }
}

具体使用

使用方式有两种,一种是直接使用,用BaseWidget包裹你的组件即可,相关属性和方法就可以直接调用了。

return BaseWidget(
      childWidget: const Text("测试文本"),
      margin: 10,
      onClick: () {
        //点击事件
      },
    );

第二种就是,自己定义组件,继承BaseWidget,可扩展自己想要实现的属性,之后直接用自己定义的组件即可,比如我想自定义一个Text,如下所示:

class SelfText extends BaseWidget {
  final String? text;

  const SelfText(this.text,
      {super.key,
      super.width,
      super.height,
      super.margin,
      super.marginLeft,
      super.marginTop,
      super.marginRight,
      super.marginBottom,
      super.padding,
      super.paddingLeft,
      super.paddingTop,
      super.paddingRight,
      super.paddingBottom,
      super.backgroundColor,
      super.strokeWidth,
      super.strokeColor,
      super.solidColor,
      super.radius,
      super.isCircle,
      super.leftTopRadius,
      super.rightTopRadius,
      super.leftBottomRadius,
      super.rightBottomRadius,
      super.childWidget,
      super.alignment,
      super.onClick,
      super.onDoubleClick,
      super.onLongPress});

  @override
  Widget? getWidget(BuildContext context) {
    return Text(text!);
  }
}

具体使用的时候,直接使用,就不用在外层包裹BaseWidget,而且你还可以在自定义类中随意扩展自己的属性。

return SelfText(
      "测试文本",
      margin: 10,
      onClick: () {
        //点击事件
      },
    );

四、相关总结

在实际的开发中,Widget的基类还是很有必要存在的,不然就会存在很多的冗余嵌套代码,具体如何去封装,还要根据相关的需求和业务来实际的操作。好了铁子们,本篇文章就到这里,不管封装的好与坏,都希望可以帮助到大家。

猜你喜欢

转载自blog.csdn.net/ming_147/article/details/130752881