【Flutter 组件】001-关于 Widget 的一切

news2024/12/28 21:26:50

【Flutter 组件】001-关于 Widget 的一切

文章目录

  • 【Flutter 组件】001-关于 Widget 的一切
  • 一、概述
    • 1、Widget 基本概述
    • 2、Flutter Framework 里的 Widget
      • 架构图
      • 说明
    • 3、根 Widget
  • 二、Widget 类
    • 1、Widget 的功能
    • 2、Widget 类
      • 源码
      • 说明
      • Widget 的标识符:Key
      • Flutter 中如何在 diff 过程中判断哪些 Widget 没有变化
      • Key 的分类
      • key 的使用
  • 三、Flutter中的四棵树
    • 1、概述
    • 2、举个例子
      • 一个 Widget 树示例:
      • 说明
      • 代码演示
      • 三棵树图示
  • 四、StatelessWidget
    • 1、概述
    • 2、一个简单的示例
      • 继承 `StatelessElement` 类实现自定义组件
      • 使用自定义的类
      • 运行结果
    • 3、Context
      • 概述
      • 获取父级 widget 代码示例
  • 五、StatefulWidget
    • 1、概述
      • 概述
      • StatefulWidget 类
      • 说明
    • 2、State 状态
      • 简介
      • State生命周期
    • 3、在 widget 树中获取 State 对象
      • 通过 Context 获取
      • 通过 GlobalKey 获取
  • 六、自定义 Widget 的三种方式
    • 1、Flutter 自定义 Widget 的三种方式
    • 2、通过继承实现自定义
      • 概述
      • 一个官方的 Dialog 例子
      • 我们继承 Dialog 来实现一个加载中的对话框
    • 3、通过组合实现自定义
      • 概述
      • 自定义 ToolBar 示例
    • 4、通过 RenderObject 自定义
      • 概述
      • 一个示例
      • CustomPaint 绘制
  • 七、附加内容
    • 1、Flutter 方法的封装示例
  • N、参考资料

一、概述

1、Widget 基本概述

Flutter 中的 Widget 相当于 Android 里的 View,iOS 里的 UIView。

  • Flutter 中几乎所有的对象都是一个 **widget **;

  • Widget 不仅可以表示 UI 元素,还可以表示一些功能性的组件,如用于手势检测的 GestureDetector 、用于APP主题数据传递的 Theme 等等;

  • 我们将 Widget 统称为组件。

界面刷新机制,类似 React

当 Widget 状态发生变化,需要更新界面时,框架会先计算从上一个状态转换到下一个状态所需的最小更改,然后再去刷新界面。

2、Flutter Framework 里的 Widget

架构图

image-20221211115553815

说明

Framework 里面有一层是 Widgets,在 Widgets 层下面,有:

  • Rendering(渲染层)
  • Animation、Painting、Gestures(动画、绘制、手势)
  • Foundation(基础库层)

Widgets 层下面平常使用较少,也比较复杂,常用 Widgets 层上面的 Material 和 Cupertino。

摘录:Material & Cupertino 指的 Widget 的风格是 Material 或 Cupertino 。Flutter 为了减轻开发人员的工作量,实现了两种不同风格的组件:Material 和 Cupertino 。**Material 用于 Android,Cupertino 用于 iOS。**有了这些组件,开发人员不需要再做额外的工作,就可以让 Flutter 的 UI 风格适应不同的平台,让 Flutter UI 获得和 Native UI 一样的使用体验。

3、根 Widget

我们常用的 MaterialApp 就是根(Root)Widget ,Flutter会默认把 根Widget 充满屏幕。

根 Widget 只能是下面三个:

  • WidgetsApp

    WidgetsApp 是可以自定义风格的 根Widget。

  • MaterialApp

    MaterialApp 是在 WidgetsApp 上添加了很多 material-design 的功能,是 Material Design 风格的 根Widget。

  • CupertinoApp

    CupertinoApp 也是基于 WidgetsApp 实现的 iOS 风格的 根Widget。

MaterialApp 是最常用的,也是功能最完善的,且经常与 Scaffold 一起使用。

二、Widget 类

1、Widget 的功能

描述一个UI元素的配置信息

就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。

Widget 描述了他们的视图在给定其当前配置和状态时应该看起来像什么。

2、Widget 类

源码


abstract class Widget extends DiagnosticableTree {
  
  const Widget({ this.key });
  
  final Key? key;
  
  
  
  Element createElement();
  
  
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  
  
  bool operator ==(Object other) => super == other;

  
  
  int get hashCode => super.hashCode;
  
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  
  static int _debugConcreteSubtype(Widget widget) {
    return widget is StatefulWidget ? 1 :
           widget is StatelessWidget ? 2 :
           0;
  }
}

说明

Widget 类本身是一个抽象类,其中最核心的就是定义了 createElement() 接口。我们在开发中一般不直接继承 Widget 而是 Widget 的子类:StatelessWidget StatefulWidget来间接继承widget类。

  • @immutable :代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final)为什么?因为 Flutter 中的属性发生变化会导致重新构建 Widget 树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换

  • widget 类继承自 DiagnosticableTreeDiagnosticableTree即“诊断树”,主要作用是提供调试信息

  • Key : 这个key属性类似于 React/Vue 中的key,主要的作用是决定是否在下一次 build复用旧的 widget ,决定的条件在canUpdate()方法中。

  • createElement() :正如前文所述“一个 widget 可以对应多个Element”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element对象。 此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到

  • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性

  • canUpdate(...) 是一个静态方法,它主要用于在 widget 树重新 build复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧 UI 树上所对应的 Element 对象的配置;通过其源码我们可以看到,只要 newWidget oldWidgetruntimeTypekey 同时相等时就会用 new widget 去更新 Element 对象的配置,否则就会创建新的 Element

Widget 的标识符:Key

diff 简介: 因为 Flutter 采用的是 react-style 的框架,每次刷新 UI 的时候,都会重新构建新的 Widget树,然后和之前的 Widget树 进行对比计算出变化的部分,这个计算过程叫做 diff 。

标识符: 在 diff 过程中,如果能提前知道哪些 Widget 没有变化,无疑会提高 diff 的性能,这时候就需要使用到标识符

为了在 diff 过程中,知道 Widget 有没有变化,就需要给 Widget 添加一个唯一的标识符,然后在 Widget树 的 diff 过程中,查看刷新前后的 Widget树 有没有相同标识符的 Widget,如果标识符相同,则说明 Widget 没有变化否则说明 Widget 有变化

假设 UI 刷新前,Widget树 是 A,在 A 里有一个标识符为 a 的 Widget,在 UI 刷新后,重建的 Widget树 是 B,如果 B 里还有标识符为 a 的 Widget,则说明这个 Widget 没变,但是如果 B 里没有标识符为 a 的 Widget,那么说明这个 Widget 发生了变化。

这个标识符在 Flutter 中就是 Key所有 Widget 都有 Key 这一个属性。

Flutter 中如何在 diff 过程中判断哪些 Widget 没有变化

稍微有些复杂,有两种情况:

  • 默认情况下( Widget 没有设置 Key

    当没有给 Widget 设置 Key 时,Flutter 会根据 Widget 的 runtimeType 和显示顺序是否相同来判断 Widget 是否有变化。

    runtimeType 是 Widget 的类型,例如 Text 和 RaisedButton 就是不同的类型。

  • Widget 有 Key

    当给 Widget 设置了 Key 时,Flutter 是根据 Key 和 runtimeType 是否相同来判断 Widget 是否有变化。

Key 的分类

Key 总共分为两类:

  1. Local Key(局部Key)
  2. Global Key(全局Key)

1. Local Key(局部Key)

在有相同父级的 Widget 中,Key 必须是唯一的,这样的 Key 叫做 局部 Key。

局部Key 在 Flutter 中对应的抽象类是 LocalKey。LocalKey 有不同的实现,主要的区别就是使用什么值来作为 Key 的值:

  • ObjectKey

    对象作为 Key 的值。

  • ValueKey

    使用特定类型的值来作为 Key 的值。

  • UniqueKey

    使用 UniqueKey 自己的对象作为 Key 的值,所以只与自身相等,称为 唯一 Key。

2.Global Key(全局Key)

全局 Key 是在整个 APP 中唯一的 Key

全局 Key 在 Flutter 中对应的抽象类GlobalKey。GlobalKey 有不同的实现,主要区别是使用的场景不同:

  • LabeledGlobalKey

    LabeledGlobalKey 用于调试,不会用来比较 Widget 是否有变化

  • GlobalObjectKey

    将对象作为 Global Key 的值

key 的使用

一般不用,页面复杂时用。

一般情况下我们需要使 Key,但是当页面比较复杂时,就需要使 Key 去提升渲染性能。

三、Flutter中的四棵树

1、概述

Flutter 中的 Widget 是用来秒数 UI 元素的配置信息的,那么真正的布局绘制由谁来完成呢?

Flutter 框架的的处理流程:

根据  Widget 树生成 Element 树,根据 Element 树生成 Render 树,根据 Render 树生成 Layer 树。

  1. 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  2. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  3. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。

2、举个例子

一个 Widget 树示例:

Container( // 一个容器 widget
  color: Colors.blue, // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 显示图片的 widget
      const Text('A'),
    ],
  ),
);

说明

如上 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:

// 源代码位置:D:/MySoft/Flutter/SDK/flutter/packages/flutter/lib/src/widgets/container.dart:403
if (color != null) {
	current = ColoredBox(color: color!, child: current);
}

代码演示

代码

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // 此处返回的是根组件
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container( // 一个容器 widget
        color: Colors.blue, // 设置容器背景色
        child: Row( // 可以将子widget沿水平方向排列
          children: [
            Image.network('https://p3-passport.byteimg.com/img/user-avatar/f755e80a8a455a4d9de36f5f096784e6~100x100.awebp'), // 显示图片的 widget
            const Text('訾博', style: TextStyle(color: Colors.white)), // 显示文本的 widget
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

图示

image-20221208183219508

三棵树图示

image-20221208183529564

注意:

  1. 三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如 StatelessWidgetStatefulWidget 都没有对应的 RenderObject。
  2. 渲染树在上屏前会生成一棵 Layer 树,前期只需要记住以上三棵树就行

四、StatelessWidget

1、概述

无状态的组件!

StatelessWidget 相对比较简单,它继承自widget类,重写了createElement()方法:


StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自 Element 类,与 StatelessWidget 相对应(作为其配置数据)。

StatelessElement 用于不需要维护状态的场景,它通常在 build 方法中通过嵌套其他 widget 来构建 UI,在构建过程中会递归的构建其嵌套的 widget 。

2、一个简单的示例

继承 StatelessElement 类实现自定义组件

import 'package:flutter/material.dart';

class Echo extends StatelessWidget  {
  const Echo({
    Key? key,
    required this.text,
    this.backgroundColor = Colors.grey,
  }):super(key:key);

  final String text;
  final Color backgroundColor;

  
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

使用自定义的类

import 'package:flutter/material.dart';
import 'package:study/echo.dart';

// 省略其他内容......

class _MyHomePageState extends State<MyHomePage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: const Echo(text: '訾博', backgroundColor: Colors.red,),
    );
  }
}

运行结果

image-20221208184853365

3、Context

概述

build方法有一个context参数,它是 BuildContext 类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。

实际上,context 是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。

获取父级 widget 代码示例

重视这段代码的写法!

class ContextRoute extends StatelessWidget  {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        // 主义这个 Builder ,一般我们返回的是一个 widget 但有时需要逻辑计算,就可以使用这个 Builder
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级 `Scaffold` widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar 的 title, 此处实际上是 Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

五、StatefulWidget

1、概述

概述

有状态的组件!

StatelessWidget 一样,StatefulWidget 也是继承自 widget 类,并重写了 createElement() 方法,不同的是返回的 Element 对象并不相同;另外 StatefulWidget 类中添加了一个新的接口createState()

StatefulWidget 类

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ super.key });
  
  
  StatefulElement createElement() => StatefulElement(this);
  
  
  
  State createState();
}

说明

  • StatefulElement 间接继承自 Element 类,与 StatefulWidget 相对应(作为其配置数据)。StatefulElement 中可能会多次调用 createState() 来创建状态(State)对象。

  • createState() 用于创建和 StatefulWidget 相关的状态,它在 StatefulWidget 的生命周期中可能会被多次调用

    例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的 State 实例,其实,本质上就是一个 StatefulElement 对应一个 State 实例。

2、State 状态

简介

State 是其对用的 StatefulWidget 要维护的状态:

一个 StatefulWidget 类会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:

  1. 在 widget 构建时可以被同步读取。
  2. 在 widget 生命周期中可以被改变,当State被改变时,可以手动调用setState()方法通知 Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其 build 方法重新构建 widget 树,从而达到更新 UI 的目的。

State 中有两个常用属性:

  1. widget,它表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI 树上的某一个节点的 widget 实例在重新构建时可能会变化,但 State 实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置 State. widget 为新的 widget 实例。
  2. context,StatefulWidget 对应的 BuildContext,作用同 StatelessWidget 的 BuildContext。

State生命周期

图示

state

说明

  • initState当 widget 第一次插入到 widget 树时会被调用,对于每一个 State 对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget),原因是在初始化完成后, widget 树中的InheritFrom widget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。

    当 widget 第一次插入到 widget 树时会被调用,只会调用一次,常用于初始化状态。

  • didChangeDependencies():当 State 对象的依赖发生变化时会被调用;例如:在之前 build() 中包含了一个InheritedWidget ,然后在之后的 build()Inherited widget 发生了变化,那么此时 InheritedWidget 的子 widget 的didChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的 didChangeDependencies 也会被调用。

  • build():此回调读者现在应该已经相当熟悉了,它主要是用于构建 widget 子树的,会在如下场景被调用:

    1. 在调用 initState() 之后;
    2. 在调用 didUpdateWidget() 之后;
    3. 在调用 setState() 之后;
    4. 在调用 didChangeDependencies() 之后;
    5. 在 State 对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  • reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在 Release 模式下永远不会被调用。

  • didUpdateWidget ():在 widget 重新构建时,Flutter 框架会调用 widget.canUpdate 来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果 widget.canUpdate 返回 true 则会调用此回调。正如之前所述,widget.canUpdate会在新旧 widget 的 keyruntimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用。

  • deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用 dispose() 方法。

  • dispose()当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

3、在 widget 树中获取 State 对象

由于 StatefulWidget 的的具体逻辑都在其 State 中,所以很多时候,我们需要获取 StatefulWidget 对应的State 对象来调用一些方法,比如 Scaffold 组件对应的状态类 ScaffoldState 中就定义了打开 SnackBar(路由页底部提示条)的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。

通过 Context 获取

context 对象有一个 findAncestorStateOfType() 方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。

代码示例

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取 State 对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级【最近的】 Scaffold 对应的 ScaffoldState 对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

约定

如果希望暴露状态,则提供一个静态的 of 方法来获取其 State 状态,反之则不提供!

一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其 State 对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:**如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State 不希望暴露,则不提供of 方法。**这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:

代码示例 1

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过 of 静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
    },
    child: Text('打开抽屉菜单2'),
  );
}),

代码示例 2

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("我是SnackBar")),
      );
    },
    child: Text('显示SnackBar'),
  );
}),

通过 GlobalKey 获取

开销较大,如果有其他可选方案,应尽量避免使用它!

GlobalKey 不能重复!

Flutter还有一种通用的获取 State 对象的方法——通过 GlobalKey 来获取! 步骤分两步:

  1. 给目标StatefulWidget添加GlobalKey

    //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
    static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
    ...
    Scaffold(
        key: _globalKey , //设置key
        ...  
    )
    
  2. 通过GlobalKey来获取State对象

    _globalKey.currentState.openDrawer()
    

GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了 GlobalKey,那么我们便可以通过globalKey.currentWidget 获得该 widget 对象、globalKey.currentElement 来获得 widget 对应的 element 对象,如果当前 widget 是 StatefulWidget,则可以通过 globalKey.currentState 来获得该 widget 对应的 state 对象。

注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。

六、自定义 Widget 的三种方式

1、Flutter 自定义 Widget 的三种方式

  • 通过继承 Widget 来修改和扩展它的功能;

  • 通过组合 Widget 来扩展功能;

  • 使用 CustomPaint 绘制自定义 Widget。

    CustomPaint 继承自 SingleChildRenderObjectWidget

这几种方式都有各自的优势和特点,相对来说 CustomPaint 绘制实现自定义是这里面比较复杂的一种自定义 Widget 方式。Flutter 中的很多基础 Widget 也是通过继承 Widget 进行扩展形成新的 Widget 或者是自己绘制 Widget。

2、通过继承实现自定义

概述

首先我们看下通过 Widget 的继承来实现自定义 Widget 组件。这种例子在 Flutter 中不在少数,例如:NetworkImage 和 AssetImage 都是继承 ImageProvider 来实现不同场景功能的、Center 是继承自 Align 来实现的等等。所以我们可以根据具体的使用场景、特点来选择基础 Widget 进行继承,从而实现我们想要的功能。

一个官方的 Dialog 例子

// 继承自StatelessWidget
class Dialog extends StatelessWidget {
  // 构造方法,设置传参
  const Dialog({
    Key key,
    this.backgroundColor,
    this.elevation,
    this.insetAnimationDuration = const Duration(milliseconds: 100),
    this.insetAnimationCurve = Curves.decelerate,
    this.shape,
    this.child,
  }) : super(key: key);

  // 设置属性
  final Color backgroundColor;

  final double elevation;

  final Duration insetAnimationDuration;

  final Curve insetAnimationCurve;

  final ShapeBorder shape;

  final Widget child;

  static const RoundedRectangleBorder _defaultDialogShape =
    RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0)));
  static const double _defaultElevation = 24.0;
  // 构建布局样式
  
  Widget build(BuildContext context) {
    final DialogTheme dialogTheme = DialogTheme.of(context);
    // 具体构建布局样式
    return AnimatedPadding(
      padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
      duration: insetAnimationDuration,
      curve: insetAnimationCurve,
      child: MediaQuery.removeViewInsets(
        removeLeft: true,
        removeTop: true,
        removeRight: true,
        removeBottom: true,
        context: context,
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(minWidth: 280.0),
            child: Material(
              color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor,
              elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation,
              shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
              type: MaterialType.card,
              child: child,
            ),
          ),
        ),
      ),
    );
  }
}

我们继承 Dialog 来实现一个加载中的对话框

import 'package:flutter/material.dart';

// 继承我们的Dialog组件,这样它就具有Dialog的一些特性和方法属性
class LoadingDialog extends Dialog {
  String text;

  // 建立构造方法,传递参数
  LoadingDialog({Key key,  this.text}) : super(key: key);

  
  Widget build(BuildContext context) {
    // 具体构建逻辑
    return Material(
      type: MaterialType.transparency,
      child: Center(
        child: SizedBox(
          width: 120.0,
          height: 120.0,
          child: Container(
            decoration: ShapeDecoration(
              color: Color(0xffffffff),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular(8.0),
                ),
              ),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                CircularProgressIndicator(),
                Padding(
                  padding: const EdgeInsets.only(
                    top: 20.0,
                  ),
                  child: Text(
                    text,
                    style: TextStyle(fontSize: 12.0),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// 调用使用的地方
class CustomWidgetSamples extends StatefulWidget {
  
  State<StatefulWidget> createState() {
    return CustomWidgetSamplesState();
  }
}

class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('CustomWidget'), primary: true),
        body: Container(
          child: Align(
            alignment: Alignment.center,
            // 构造并传递参数
            child: LoadingDialog(text: '加载中...'),
          ),
        ));
  }
}

// 我们只需传递我们的text参数即可
LoadingDialog(text: '加载中...'),

3、通过组合实现自定义

概述

Widget 组合,顾名思义,就是将各种 Flutter 的基础 Widget,进行不同的选择、组合拼装,来实现一个可以满足我们需求的、新的 Widget。Flutter 的基础 Widget 中,也有很多是通过组合来实现的。

自定义 ToolBar 示例

// 自定义一个ToolBar
import 'package:flutter/material.dart';

class ToolBar extends StatefulWidget implements PreferredSizeWidget {
  // 构造方法,设置传递参数
  ToolBar({ this.onTap}) : assert(onTap != null);
  // 属性参数,点击回调
  final GestureTapCallback onTap;

  
  State createState() {
    return ToolBarState();
  }
  // AppBar需要实现 PreferredSizeWidget
  
  Size get preferredSize {
    return Size.fromHeight(56.0);
  }
}

class ToolBarState extends State<ToolBar> {
  
  Widget build(BuildContext context) {
    // 设置布局
    return SafeArea(
      top: true,
      child: Container(
        color: Colors.blue,
        child: Row(
          children: <Widget>[
            Icon(
              Icons.menu,
              color: Colors.white,
              size: 39,
            ),
            Expanded(
              child: Container(
                color: Colors.white,
                padding: EdgeInsets.all(5),
                margin: EdgeInsets.all(5),
                child: Text(
                  '搜索...',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
            GestureDetector(
              onTap: this.widget.onTap,
              child: Icon(
                Icons.photo_camera,
                color: Colors.white,
                size: 39,
              ),
            )
          ],
        ),
      ),
    );
  }
}

// 调用自定义ToolBar
class CustomWidgetSamples extends StatefulWidget {
  
  State<StatefulWidget> createState() {
    return CustomWidgetSamplesState();
  }
}

class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        // 设置自定义ToolBar
        appBar:ToolBar(
          onTap: () {
            print('click');
          },
        ),
        primary: true,
        body: Column(
          children: <Widget>[
            Container(
              child: Align(
                alignment: Alignment.center,
                child: LoadingDialog(text: '加载中...'),
              ),
            )
          ],
        ));
  }
}

4、通过 RenderObject 自定义

通过 CustomPaint 绘制 Widget

概述

StatelessWidgetStatefulWidget 都是用于组合其他组件的,它们本身没有对应的 RenderObject。Flutter 组件库中的很多基础组件都不是通过 StatelessWidgetStatefulWidget 来实现的,比如 Text 、Column、Align等,就好比搭积木StatelessWidgetStatefulWidget 可以将积木搭成不同的样子,但前提是得有积木,而这些积木都是通过自定义 RenderObject 来实现的。实际上Flutter 最原始的定义组件的方式就是通过定义 RenderObject 来实现,而StatelessWidgetStatefulWidget 只是提供的两个帮助类。

一个示例

class CustomWidget extends LeafRenderObjectWidget{
  
  RenderObject createRenderObject(BuildContext context) {
    // 创建 RenderObject
    return RenderCustomObject();
  }
  
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  
  void performLayout() {
    // 实现布局逻辑
  }

  
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget ,我们可以看一下它的实现:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  const LeafRenderObjectWidget({ Key? key }) : super(key: key);

  
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

很简单,就是帮 widget 实现了createElement 方法,它会为组件创建一个 类型为 LeafRenderObjectElement 的 Element 对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承 SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。

然后我们重写了 createRenderObject 方法,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用(构建渲染树时)用于生成渲染对象。我们的主要任务就是来实现 createRenderObject 返回的渲染对象类,本例中是 RenderCustomObject 。updateRenderObject 方法是用于在组件树状态发生变化但不需要重新创建 RenderObject 时用于更新组件渲染对象的回调

RenderCustomObject 类是继承自 RenderBox,而 RenderBox 继承自 RenderObject,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑。

CustomPaint 绘制

CustomPaint 的构造方法

const CustomPaint({
    Key key,
    // CustomPainter背景
    this.painter,
    // CustomPainter前景画笔
    this.foregroundPainter,
    // 画布尺寸,
    this.size = Size.zero,
    // 是否是复杂的绘制,Flutter会设置一些缓存优化策略
    this.isComplex = false,
    // 下一帧是否会改变
    this.willChange = false,
    // 子元素,可以为空
    Widget child,
  })

绘制的核心就是自定义 CustomPainter,我们简单看下 CustomPainter 里面的方法结构:

class Sky extends CustomPainter {
  // 绘制方法
  
  void paint(Canvas canvas, Size size) {
    // canvas为画布
    // size为画布大小
  }

  // 刷新布局时是否重绘,可以根据实际情况进行返回值
  
  bool shouldRepaint(Sky oldDelegate) => false;

}

 // Canvas和其他平台的Canvas功能和作用基本一样,包含很多绘制方法

  void save() native 'Canvas_save';

  void saveLayer(Rect bounds, Paint paint);

  void _saveLayerWithoutBounds(List<dynamic> paintObjects, ByteData paintData)
      native 'Canvas_saveLayerWithoutBounds';

  void restore() native 'Canvas_restore';

  int getSaveCount() native 'Canvas_getSaveCount';

  void translate(double dx, double dy) native 'Canvas_translate';

  void scale(double sx, [double sy]) => _scale(sx, sy ?? sx);

  void rotate(double radians) native 'Canvas_rotate';

  void skew(double sx, double sy) native 'Canvas_skew';

  void transform(Float64List matrix4);

  void clipRect(Rect rect, { ClipOp clipOp: ClipOp.intersect, bool doAntiAlias = true });

  void clipRRect(RRect rrect, {bool doAntiAlias = true});

  void clipPath(Path path, {bool doAntiAlias = true});

  void drawColor(Color color, BlendMode blendMode);

  void drawLine(Offset p1, Offset p2, Paint paint);

  void drawPaint(Paint paint);

  void drawRect(Rect rect, Paint paint);

  void drawRRect(RRect rrect, Paint paint);

  void drawDRRect(RRect outer, RRect inner, Paint paint);

  void drawOval(Rect rect, Paint paint);

  void drawCircle(Offset c, double radius, Paint paint);

  void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint);

  void drawPath(Path path, Paint paint);

  void drawImage(Image image, Offset p, Paint paint);

  void drawImageRect(Image image, Rect src, Rect dst, Paint paint);

  void drawImageNine(Image image, Rect center, Rect dst, Paint paint);

  void drawPicture(Picture picture);

  void drawParagraph(Paragraph paragraph, Offset offset);

  void drawPoints(PointMode pointMode, List<Offset> points, Paint paint);

  void drawRawPoints(PointMode pointMode, Float32List points, Paint paint);

  void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint);

  void drawAtlas(Image atlas,
                 List<RSTransform> transforms,
                 List<Rect> rects,
                 List<Color> colors,
                 BlendMode blendMode,
                 Rect cullRect,
                 Paint paint);

  void drawRawAtlas(Image atlas,
                    Float32List rstTransforms,
                    Float32List rects,
                    Int32List colors,
                    BlendMode blendMode,
                    Rect cullRect,
                    Paint paint);

  void drawShadow(Path path, Color color, double elevation, bool transparentOccluder);

在进行 Canvas 画布绘制时,我们就需要画笔 Paint,我们需要创建相应的画笔来绘制到 Canvas 上。Paint 画笔也有很多可以设置的属性,常用的有:

color:画笔颜色
style:绘制模式,画线 or 充满
maskFilter:绘制完成,还没有被混合到布局上时,添加的遮罩效果,比如blur效果
strokeWidth:线条宽度
strokeCap:线条结束时的绘制样式
shader:着色器,一般用来绘制渐变效果或ImageShader
... ...

// 可以这样使用
Paint myPaint = Paint()
    ..color = Colors.blueAccent // 画笔颜色
    ..strokeCap = StrokeCap.round // 画笔笔触类型
    ..isAntiAlias = true // 是否启动抗锯齿
    ..blendMode = BlendMode.exclusion // 颜色混合模式
    ..style = PaintingStyle.fill // 绘画风格,默认为填充
    ..colorFilter = ColorFilter.mode(Colors.blueAccent,
        BlendMode.exclusion) // 颜色渲染混合模式
    ..maskFilter = MaskFilter.blur(BlurStyle.inner, 2.0) // 模糊遮罩效果
    ..filterQuality = FilterQuality.high // 颜色渲染模式的质量
    ..strokeWidth = 10.0; // 画笔的宽度

通过 CustomPaint 自定义的 Widget

class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('CustomWidget'),
        ),
        body: Column(
          children: <Widget>[
            CustomPaint(
              painter: Sky(),
              child: Center(
                child: Text(
                  '文字',
                ),
              ),
            )
          ],
        ));
  }
}

class Sky extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    // 绘制圆角矩形
    // 用Rect构建一个边长50,中心点坐标为150,150的矩形
    Rect rectCircle =
        Rect.fromCircle(center: Offset(150.0, 150.0), radius: 60.0);
    // 根据上面的矩形,构建一个圆角矩形
    RRect rrect = RRect.fromRectAndRadius(rectCircle, Radius.circular(30.0));
    canvas.drawRRect(rrect, Paint()..color = Colors.yellow);
  }

  
  bool shouldRepaint(Sky oldDelegate) => false;
  
  bool shouldRebuildSemantics(Sky oldDelegate) => false;
}

七、附加内容

1、Flutter 方法的封装示例

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

class Utils {
  BuildContext context;

  // 可以设置构造方法,传递参数,参数传递时有区别,通过key:value形式
  Utils({ this.context}) : assert(context != null);

  // 首先指定返回类型,然后定义方法名
  /// 获取时间戳毫秒数,13位
  int getMilliseconds() {
    return DateTime.now().millisecondsSinceEpoch;
  }
  // 方法名后可以设置传递的参数
  /// 复制到剪贴板
  void setClipData(String text) {
    Clipboard.setData(ClipboardData(text: text));
  }

  // 以下划线开始的方法名这个类的外部不可以调用,只能内部进行调用使用
  /// 获取屏幕宽度
  double _getScreenWidth(BuildContext context) {
    return MediaQuery.of(context).size.width;
  }

  /// 获取屏幕高度
  double getScreenHeight(BuildContext context) {
    return MediaQuery.of(context).size.height;
  }

  /// 获取屏幕状态栏高度
  double getStatusBarTop(BuildContext context) {
    return MediaQuery.of(context).padding.top;
  }

  /// 获取屏幕方向
  Orientation getScreenOrientation(BuildContext context) {
    return MediaQuery.of(context).orientation;
  }

  Future<String> getBatteryLevel() async {
    var batteryLevel = 'unknown';
    MethodChannel methodChannel = MethodChannel('samples.flutter.io/battery');
    try {
      int result = await methodChannel.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level: $result%';
    } on PlatformException {
      batteryLevel = 'Failed to get battery level.';
    }
    return batteryLevel;
  }
}

// 使用时,构造方法传参通过key:value形式传递设置
Utils utils = Utils(context: context);

// 调用方法
utils.getScreenHeight(context);

N、参考资料

Flutter Widget详解

https://blog.csdn.net/u011578734/article/details/108781671

Flutter实战

https://book.flutterchina.club/chapter2/flutter_widget_intro.html#_2-2-1-widget-概念

Flutter 完全手册

https://juejin.cn/book/6844733786626719757

Flutter学习记录——24.自定义 Widget 及方法封装

https://blog.51cto.com/u_15781233/5654671

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/80464.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【云原生 | Kubernetes 实战】10、K8s 控制器 Deployment 入门到企业实战应用(上)

目录 一、Deployment 控制器&#xff1a;概念、原理解读 1.1 Deployment 概述 1.2 Deployment 工作原理&#xff1a;如何管理 rs 和 Pod&#xff1f; 补充&#xff1a;什么叫做更新节奏和更新逻辑呢&#xff1f; 二、Deployment 资源清单文件编写技巧 三、Deployment 使…

Java线程 (使用Callable实现多线程),看完你发现多线程多么简单!

1.Thread 与 Runnable 的关系 2.Callable实现多线程 3.线程运行状态 1.Thread 与 Runnable 的关系 经过一系列的分析之后可以发现&#xff0c;在多线程的实现过程之中已经有了两种做法&#xff1a;Thread类、Runnable接口&#xff0c;如果从代码结构本身来讲&#xff0c;使…

电子学会2020年9月青少年软件编程(图形化)等级考试试卷(一级)答案解析

青少年软件编程&#xff08;图形化&#xff09;等级考试试卷&#xff08;一级A卷&#xff09; 分数&#xff1a;100.00 题数&#xff1a;37 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 1. 下面哪个积木能够调节左右声道的音…

疯狂加持,腾讯大佬的“百万级”JVM学习笔记,从思维图+核心+架构让你一步到位

前言 毫不夸张地说&#xff0c;JVM是现代软件工程最成功的案例之一。因为它自带GC&#xff0c;又有无数可以微调的参数&#xff0c;且运行极其稳定可靠&#xff0c;所以&#xff0c;许多厂商的核心业务系统&#xff0c;才敢放心地用Java编写&#xff0c;运行在JVM之上。 近几…

中英翻译《Thailand泰国》

Thailand 泰国 一、Pre-reading activity 阅前思考 1.Find Thailand in an atlas. 在地图册上找到泰国。 2.What are the names of the countries next to it? 与它相邻的国家都叫什么名字&#xff1f; 3.Is your country near Thailand? 你的国家靠近泰国吗&#xff1f; …

Java基于JSP的小区物业管理系统

经济的飞速发展,促使着城市化进程的加快,随之而来的则是人们生活水平日益提高,并促进住宅小区建设的飞速发展。大量住宅小区投入使用后,加大了管理者的工作难度,小区物业除了要对房屋本身进行修缮外,还需对场地、住户信息、附属设备、环卫绿化、收费情况、治安等方面进行专业化…

全国高校计算机能力挑战赛初赛试题全记录

今天搞了mini版蓝桥杯比赛&#xff0c;有时间的话&#xff0c;我们每天都会补充15道主观题。废话不多说&#xff0c;开整&#xff01;&#xff01;&#xff01; 2022.10.11 抽象&#xff1a;在C中&#xff0c;如果一个类并没有什么实际信息&#xff0c;那么就是一个抽象类 多态…

基于java+springboot+mybatis+vue+mysql的招生管理系统

项目介绍 招生管理系统采用java技术&#xff0c;基于springboot框架&#xff0c;vue技术&#xff0c;mysql数据库进行开发。本系统主要包括管理员和学生两个角色组成&#xff0c;主要包括以下功能&#xff1a; &#xff08;1&#xff09;前台&#xff1a;首页、专业信息、招生…

Online DDL和Cardinality

Online DDL和Cardinality前言Fast Index CreationOnline Schema ChangeOnline DDLCardinality什么是CardinalityCardinality是如何进行统计的前言 本文来聊聊关于Mysql索引管理方面的一些内容&#xff0c;首先我们先准备一张表: CREATE DATABASE IF NOT EXISTS test;USE test…

JVM,你是不是过分了?

以我的经验加上和同行们的交流&#xff0c;我认为学 JVM 最好的方法是&#xff1a; 在程序员不同的水平段&#xff0c;做精准的学习。 所谓的精准学习&#xff0c;就是学习对自己工作有巨大帮助的知识点。以工作内容带动学习&#xff0c;等到积累多了&#xff0c;再一举攻克所…

D-027 SerDes详解

SerDes详解1 SerDes简介1.1 并行总线接口1.2 SerDes接口1.3 SerDes的特点2 发送均衡技术1 SerDes简介 1.1 并行总线接口 在SerDes流行之前&#xff0c;芯片之间的互联时通过系统同步或者源同步的并行接口进行接口传输数据。 并行接口定义图片系统同步发送端和接收端都是由系…

影驰H610MK主板在MBR硬盘上安装系统(可用于安装WIN7)

记录一次MBR格式的机械硬盘安装WIN10系统。 一、进入BIOS界面 二、设置BIOS界面 1、设置系统模式&#xff1a;高级->系统模式选择&#xff0c;选择Legacy and UEFI &#xff08;默认模式为UEFI&#xff09; 2、设置CSM配置 1&#xff09;、进入CMS配置界面&#xff1a;高级…

Js实现简单的文件类型、文件大小、图片像素校验

文章目录1、简单的小Demo2、更多说明2.1 利用循环判断DOM是否渲染完成2.2 利用MutationObserver监听DOM树变化2.3 关于节点的宽高属性2.4 关于页面的宽高属性2.5 关于FileReader提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 1、简单的小Demo <!DO…

“互联网+”时代保险公司经营管理模式研究

目 录 摘 要 3 引言 4 一、公司经营管理模式的理论概述 5 &#xff08;一&#xff09;“互联网”的概念 5 &#xff08;二&#xff09;企业经营管理的主要模式 5 二、众安保险公司的基本情况 6 三、众安公司经营管理模式面临的困难 8 &#xff08;一&#xff09;经营管理方式滞…

关于SpringBoot集成ES Scroll API(滚动查询)的实践

待到秋来九月八&#xff0c;我花开后百花杀背景&#xff1a;大胆尝试实践&#xff1a;学习踩坑最终解决背景&#xff1a; 那是年初在某个交付项目&#xff0c;从用户侧获知了一个elastic search作为分布式数据库的一个瓶颈&#xff0c;那就是单次查询量超过了ES的默认单次查询…

DirectX12 - Pipeline(管线)之IA

这里是SunshineBooming&#xff0c;GPU公司一枚小小的Driver工程师&#xff0c;主要工作是写DirectX12 Driver&#xff0c;我会持续更新这个DX12 Spec系列&#xff0c;可能比较冷门&#xff0c;但是都是干货和工作中的心得体会&#xff0c;有任何GPU相关的问题都可以在评论区互…

原来服务端的退出姿势也可以这么优雅

最简单的 http 服务端 咱们来写一个简单的 http 服务器 func main() { srvMux : http.NewServeMux() srvMux.HandleFunc("/getinfo", getinfo) http.ListenAndServe(":9090", srvMux)}func getinfo(w http.ResponseWriter, r *http.Request) { fmt.Printl…

程序猿入门|编程注重写注释,代码规范注释有哪些讲究?

注释风格 1.总述 一般使用 // 或 /* */,只要统一就好。 2.说明 // 或 /* */ 都可以,但 // 更 常用,要在如何注释及注释风格上确保统一。 文件注释 1.总述 在每一个文件开头加入版权、作者、时间等描述。 文件注释描述了该文件的内容,如果一个文件只声明,或实现,或测试…

JVM运行时参数

3.类型三&#xff1a;-XX参数选项 特点 作用 用于开发和调试jvm 分类 特别地 二、添加jvm参数选项 1.运行jar包 2.通过Tomcat运行war包 3.程序运行过程中 三、常用的JVM参数选项 1.打印设置的XX选项及值 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210219105124…

window安装torch环境--不踩坑须知!

window安装torch环境–不踩坑须知&#xff01; 1. 查看电脑安装的cudn版本 进入cmd输入&#xff1a; nvidia-smi2.查找cuda对应的torch版本 在官网查找&#xff1a;https://pytorch.org/get-started/previous-versions/ 如果呢你在新建的conda环境里输入该命令&#xff0c;…