03_Flutter自定义下拉菜单

这篇具有很好参考价值的文章主要介绍了03_Flutter自定义下拉菜单。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

03_Flutter自定义下拉菜单

在Flutter的内置api中,可以使用showMenu实现类似下拉菜单的效果,或者使用PopupMenuButton组件,PopupMenuButton内部也是使用了showMenu这个api,但是使用showMenu时,下拉面板的显示已经被约定死了,只能放一个简单的列表,没有办法定制下来面板的ui,并且下拉面板的宽高需要通过指定constraints进行限制,下面是一个简单的showMenu的用法:

Container(
  height: 44,
  margin: EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
  color: Colors.red,
  child: Builder(
    builder: (context) {
      return GestureDetector(
        onTap: () {
          final RenderBox button = context.findRenderObject()! as RenderBox;
          final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

          Offset offset = Offset(0.0, button.size.height);

          RelativeRect position = RelativeRect.fromRect(
            Rect.fromPoints(
              button.localToGlobal(offset, ancestor: overlay),
              button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
            ),
            Offset.zero & overlay.size,
          );
          
          showMenu(
            context: context,
            position: position,
            constraints: BoxConstraints(maxWidth: 315, maxHeight: 200),
            items: List.generate(5, (index) => PopupMenuItem(
              child: Container(
                width: 375,
                height: 44,
                alignment: AlignmentDirectional.center,
                child: Text("item"),
              )
            ))
          );
        },
      );
    },
  ),
)

flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

接下来,我们将参照showMenu的源码,依葫芦画个瓢,自定义一个下拉菜单的api,并可自由定制下拉面板的布局内容,篇幅有点长,请耐心观看。

一.确定下拉面板的起始位置

查看PopupMenuButton的源码,可以知道,PopupMenuButton在确定下拉面板的起始位置时,是先获取下拉面板依赖的按钮的边界位置和整个页面的显示区域边界,通过这两个边界计算得到一个RelativeRect,这个RelativeRect就是用来描述下拉面板的起始位置的。

showPopup(BuildContext context) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );
}

注:上述代码中用的的context对象,必须是下拉面板依赖的按钮对应的context,否则最后计算出来的RelativeRect是不对的。计算过程不做过多解释了,直接上图:

flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

二.确定下拉面板的布局约束

  • 水平方向确定最大宽度,比较简单,下拉面板的最大宽度和它所依赖的按钮的宽度一致即可
  • 垂直方向上的最大高度,上一步已经确定了position的值,垂直方向上的最大高度可以取position.top - buttonHeight - padding.top - kToolbarHeight和constraints.biggest.height - position.top - padding.bottom的最大值,padding为安全区域的大小
  • 使用CustomSingleChildLayout作为下拉面板的父容器,并实现一个SingleChildLayoutDelegate,重写getConstraintsForChild,确定约束
EdgeInsets padding = MediaQuery.paddingOf(context);

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;

  _CustomPopupRouteLayout(this.position);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position;
  }
}

三.显示下拉面板

我们先把下拉面板显示出来看看效果,这里的下拉面板其实是一个弹出层,而在Flutter中,所有的弹出层的显示和页面路由是一样的,都是通过Navigator.push进行显示,参照showMenu的源码,这里的弹出层我们让其继承PopupRoute

class _CustomPopupRoute<T> extends PopupRoute<T> {
  final RelativeRect position;
  
  final String? barrierLabel;

  _CustomPopupRoute({
    required this.position,
    required this.barrierLabel,
  });

  
  Color? get barrierColor => null;

  
  bool get barrierDismissible => true;

  
  Duration get transitionDuration => Duration(milliseconds: 200);

  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return CustomSingleChildLayout(
      delegate: _CustomPopupRouteLayout(position),
      child: Material(
        child: Container(
          color: Colors.yellow,
          width: double.infinity,
          height: double.infinity,
          alignment: AlignmentDirectional.center,
          child: Text("popup content"),
        ),
      ),
    );
  }

}
showPopup(BuildContext context) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );
  
  Navigator.of(context).push(_CustomPopupRoute(
      position: position, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
  ));
}

flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

如图,黄色区域就是下拉面板,可以看到,点击按钮下拉面板显示,点击下拉面板以外的区域,下拉面板关闭,但是位置好像不对,因为我们根本就没去确定下拉面板的位置。

四.确定下拉面板的位置


Offset getPositionForChild(Size size, Size childSize) {
	return super.getPositionForChild(size, childSize);
}

只需要重写SingleChildLayoutDelegate的getPositionForChild方法,返回一个Offset对象,Offset的x、y的值就代表下拉面板左上角的位置,那么问题来了,x、y的值怎么确定?

  • 确定x

    x = position.left

  • 确定y

    • position.top + constraintsHeight > size.height - paddingBottom

    flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

    • position.top + constraintsHeight <= size.height - paddingBottom

    flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

EdgeInsets padding = MediaQuery.paddingOf(context);

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;
  EdgeInsets padding;

  _CustomPopupRouteLayout(this.position, this.padding);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
    
  Offset getPositionForChild(Size size, Size childSize) {
    double x = position.left;
    double y = position.top;
    final double buttonHeight = size.height - position.top - position.bottom;
    double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom);
    if(position.top + constraintsHeight > size.height - padding.bottom) {
      y = position.top - childSize.height - buttonHeight;
    }

    return Offset(x, y);
  }
  

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position || padding != oldDelegate.padding;
  }
}
flutter 下拉菜单,重学Flutter,flutter,dart,移动开发flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

六.下拉动画实现

创建动画插值器,其值从0 ~ 1之间变化,动画时长为PopupRoute中重写的transitionDuration,及200ms时间内,从0变到1,或者从1变到0

final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));

使用AnimatedBuilder改造PopupRoute的布局结构,根据heightFactorTween的动画执行值 * 下拉菜单内容容器的高度,改变拉菜单内容的高度即可,这里暂时将高度设置为固定值300。

class _CustomPopupRoute<T> extends PopupRoute<T> {
  
  ...
  
  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    EdgeInsets padding = MediaQuery.paddingOf(context);
    final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
    return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      removeBottom: true,
      removeLeft: true,
      removeRight: true,
      child: CustomSingleChildLayout(
        delegate: _CustomPopupRouteLayout(position, padding),
        child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            return Material(
              child: Container(
                height: 300*heightFactorTween.evaluate(animation),
                child: child,
              )
            );
          },
          child: Container(
            color: Colors.yellow,
            width: double.infinity,
            height: 300,
            alignment: AlignmentDirectional.center,
            child: Text("popup content"),
          ),
        ),
      ),
    );
  }
}
flutter 下拉菜单,重学Flutter,flutter,dart,移动开发flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

下拉动画效果已经出来了,但是实际情况下,下拉面板的高度是不能直接在组件层固定写死的,所以这里需要动态计算出下拉面板的高度。

七.下拉面板动态高度,支持下拉动画

想要获取组件的高度,需要等到组件的layout完成后,才能获取到组件的大小,因此,我们需要自定义一个RenderObject,重写其performLayout,在子控件第一次layout完后,获取到子控件的初始高度,子控件的初始化高度结合动画的高度比例系数来最终确定自身的大小。

class _RenderHeightFactorBox extends RenderShiftedBox {
  double _heightFactor;
  _RenderHeightFactorBox({
    RenderBox? child,
    double? heightFactor,
  }):_heightFactor = heightFactor ?? 1.0, super(child);

  double get heightFactor => _heightFactor;

  set heightFactor(double value) {
    if (_heightFactor == value) {
      return;
    }
    _heightFactor = value;
    markNeedsLayout();
  }

  
  void performLayout() {
    final BoxConstraints constraints = this.constraints;

    if (child == null) {
      size = constraints.constrain(Size.zero);
      return;
    }

    child!.layout(constraints, parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));

    child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));
  }
}

接着定义一个SingleChildRenderObjectWidget,并引用_RenderHeightFactorBox

class _HeightFactorBox extends SingleChildRenderObjectWidget {
  final double? heightFactor;

  const _HeightFactorBox({
    super.key,
    this.heightFactor,
    super.child,
  });

  
  RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);

  
  void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
    renderObject.heightFactor = heightFactor ?? 1.0;
  }
}

最后把下拉面板中,执行动画的child使用_HeightFactorBox包裹,并传入heightFactorTween的执行结果即可。文章来源地址https://www.toymoban.com/news/detail-772716.html


Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
  EdgeInsets padding = MediaQuery.paddingOf(context);
  final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
  return MediaQuery.removePadding(
    context: context,
    removeTop: true,
    removeBottom: true,
    removeLeft: true,
    removeRight: true,
    child: CustomSingleChildLayout(
      delegate: _CustomPopupRouteLayout(position, padding),
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return Material(
            child: _HeightFactorBox(
              heightFactor: heightFactorTween.evaluate(animation),
              child: child,
            )
          );
        },
        child: Container(
          color: Colors.yellow,
          width: double.infinity,
          height: double.infinity,
          alignment: AlignmentDirectional.center,
          child: Text("popup content"),
        ),
      ),
    ),
  );
}
flutter 下拉菜单,重学Flutter,flutter,dart,移动开发flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

八.完整代码

class TestPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("下拉菜单"),
        backgroundColor: Colors.blue,
      ),
      body: Container(
        width: 375,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Container(
              height: 44,
              margin: const EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
              color: Colors.red,
              child: Builder(
                builder: (context) {
                  return GestureDetector(
                    onTap: () {
                      showPopup(context: context, builder: (context) {
                        return Container(
                          height: 400,
                          decoration: const BoxDecoration(
                            color: Colors.yellow
                          ),
                          child: SingleChildScrollView(
                            physics: const ClampingScrollPhysics(),
                            child: Column(
                              mainAxisSize: MainAxisSize.max,
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: List<Widget>.generate(29, (index) {
                                int itemIndex = index ~/ 2;
                                if(index.isEven) {
                                  return Container(
                                    height: 44,
                                    alignment: AlignmentDirectional.center,
                                    child: Text("item$itemIndex"),
                                  );
                                } else {
                                  return Container(
                                    height: 1,
                                    color: Colors.grey,
                                  );
                                }
                              }),
                            ),
                          ),
                        );
                      });
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

}

showPopup({
  required BuildContext context,
  required WidgetBuilder builder,
  double? elevation,
  Color? shadowColor,
  Duration animationDuration = const Duration(milliseconds: 200)
}) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );

  Navigator.of(context).push(_CustomPopupRoute(
      position: position,
      builder: builder,
      elevation: elevation,
      shadowColor: shadowColor,
      animationDuration: animationDuration,
      barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
  ));
}

class _CustomPopupRoute<T> extends PopupRoute<T> {

  final WidgetBuilder builder;
  final RelativeRect position;
  final double? elevation;
  final Color? shadowColor;
  
  final String? barrierLabel;
  final Duration animationDuration;

  _CustomPopupRoute({
    required this.builder,
    required this.position,
    required this.barrierLabel,
    this.elevation,
    this.shadowColor,
    Duration? animationDuration
  }): animationDuration = animationDuration ?? const Duration(milliseconds: 200),
  super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);

  
  Color? get barrierColor => null;

  
  bool get barrierDismissible => true;

  
  Duration get transitionDuration => animationDuration;

  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    EdgeInsets padding = MediaQuery.paddingOf(context);
    final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
    return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      removeBottom: true,
      removeLeft: true,
      removeRight: true,
      child: CustomSingleChildLayout(
        delegate: _CustomPopupRouteLayout(position, padding),
        child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            return Material(
              child: _HeightFactorBox(
                heightFactor: heightFactorTween.evaluate(animation),
                child: child,
              )
            );
          },
          child: builder(context),
        ),
      ),
    );
  }

}

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;
  EdgeInsets padding;
  double childHeightMax = 0;

  _CustomPopupRouteLayout(this.position, this.padding);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
  Offset getPositionForChild(Size size, Size childSize) {
    double x = position.left;
    double y = position.top;
    final double buttonHeight = size.height - position.top - position.bottom;
    double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom);
    if(position.top + constraintsHeight > size.height - padding.bottom) {
      y = position.top - childSize.height - buttonHeight;
    }

    return Offset(x, y);
  }

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position || padding != oldDelegate.padding;
  }
}

class _RenderHeightFactorBox extends RenderShiftedBox {
  double _heightFactor;
  _RenderHeightFactorBox({
    RenderBox? child,
    double? heightFactor,
  }):_heightFactor = heightFactor ?? 1.0, super(child);

  double get heightFactor => _heightFactor;

  set heightFactor(double value) {
    if (_heightFactor == value) {
      return;
    }
    _heightFactor = value;
    markNeedsLayout();
  }

  
  void performLayout() {
    final BoxConstraints constraints = this.constraints;

    if (child == null) {
      size = constraints.constrain(Size.zero);
      return;
    }

    child!.layout(constraints, parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));

    child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));
  }
}

class _HeightFactorBox extends SingleChildRenderObjectWidget {

  final double? heightFactor;

  const _HeightFactorBox({
    super.key,
    this.heightFactor,
    super.child,
  });

  
  RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);

  
  void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
    renderObject.heightFactor = heightFactor ?? 1.0;
  }
}
flutter 下拉菜单,重学Flutter,flutter,dart,移动开发flutter 下拉菜单,重学Flutter,flutter,dart,移动开发

到了这里,关于03_Flutter自定义下拉菜单的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Flutter】下载安装Flutter并使用学习dart语言

    安装flutter, 并使用flutter内置的dartSDK学习使用dart语言。 编辑器: Android Studio fluuter 版本 : flutter_windows_3.13.1 内置dartSDK : 3.1.0 dart路径路径: flutter安装路径bincachedart-sdk flutter下载地址 官网的下载描述蛮详细的,直接用就行。 Android Studio 需要到官网下载安装包。 如果你c盘容

    2024年02月09日
    浏览(33)
  • Flutter系列文章-Flutter环境搭建和Dart基础

    Flutter是Google推出的一个开源的、高性能的移动应用开发框架,可以用一套代码库开发Android和iOS应用。Dart则是Flutter所使用的编程语言。让我们来看看如何搭建Flutter开发环境,并了解Dart语言的基础知识。 1. 安装Flutter SDK 首先,访问Flutter官网下载Flutter SDK。选择适合你操作系统

    2024年02月15日
    浏览(35)
  • Flutter学习2 - Dart

    Dart语言特点: Dart 2.0 开始便是强类型语言,而且是静态类型的(可以类比Java,C#等) 强类型语言的优点:所有类型的检查必须在编译的时候完成 前端开发的 js 语言是弱类型的语言 强类型的语言需要在定义的时候指定类型,如果不指定,编译器也可以在赋值的时候推断出变

    2024年02月21日
    浏览(33)
  • 【Flutter】Dio 强大的Dart/Flutter HTTP客户端

    Dio是一个强大的Dart/Flutter HTTP客户端,支持全局配置、拦截器、FormData、请求取消、文件上传/下载、超时等功能。 首先,

    2024年02月11日
    浏览(37)
  • 无涯教程-Flutter - Dart简介

    Dart是一种开源通用编程语言,它最初是由Google开发的, Dart是一种具有C样式语法的面向对象的语言,它支持诸如接口,类之类的编程概念,与其他编程语言不同,Dart不支持数组, Dart集合可用于复制数据结构,例如数组,泛型和可选类型。 以下代码显示了一个简单的Dart程序

    2024年02月10日
    浏览(42)
  • Flutter Dart语言(05)异步

    该系列教程主要是为有一定语言基础 C/C++的程序员,快速学习一门新语言所采用的方法,属于在C/C++基础上扩展新语言的模式。 在Dart语言中,虽然没有像其他语言(如Java、C++、Python)中的传统多线程概念,但它采用了异步(asynchronous)编程模型来处理并发任务。Dart使用asy

    2024年02月14日
    浏览(29)
  • 【Flutter】Dart/Flutter SDK如何降低版本、回退到指定版本

    因为dart3.0以后不再支持 no-sound-null-safety;但是有些项目不得以切换到dart3.0以前继续使用运行项目 方法1: 通过 命令,将flutter降级为当前通道的上一个活动版本; 如果没有存在老版本则会提示 flutter downgrade There is no previously recorded version for channel “stable”. 这样的话则可以通

    2024年02月16日
    浏览(27)
  • Flutter Dart语言(04)库操作

    该系列教程主要是为有一定语言基础 C/C++的程序员,快速学习一门新语言所采用的方法,属于在C/C++基础上扩展新语言的模式。 引入代码如下所示: 一般从官方网站:Page 1 | Top packages中 搜索需要的第三方库,打开项目中的配置文件,名为:pubspec.yaml,找到dependencies选项,这

    2024年02月14日
    浏览(32)
  • flutter的引擎,Dart语言概括

    Dart是谷歌开发的, 类型安全的 , 面向对象 的编程语言,被应用于 Web、服务器、移动应用和物联网 等领域。 dart是谷歌在2011年推出的编程语言。谷歌希望使用dart来取代JavaScript。谷歌是一个颠覆式创新公司,谷歌退出golang是为了取代java,c++。谷歌退出flutter就是为了取代R

    2023年04月22日
    浏览(35)
  • Flutter 四:main.dart简单介绍

    main.dart简单介绍 运行结果

    2024年02月03日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包