MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter组件库源码分析:Material与Cupertino的实现

2024-07-097.1k 阅读

Flutter组件库概述

Flutter作为一款跨平台的移动应用开发框架,其组件库是构建丰富用户界面的基石。Flutter提供了两种主要的设计风格组件库:Material和Cupertino。Material Design是谷歌推出的一套视觉设计语言,强调物理隐喻和直观交互,广泛应用于Android应用以及部分网页应用中。而Cupertino则是苹果公司iOS操作系统的设计语言,具有简洁、圆润、拟物等特点。这两种设计风格组件库在Flutter中都有对应的实现,开发者可以根据目标平台的设计规范或自己的需求选择合适的组件来构建应用界面。

Material组件库源码分析

Material组件库的核心结构

Material组件库的核心是围绕MaterialApp展开的。MaterialApp是一个继承自WidgetsApp的组件,它为应用提供了Material Design风格的主题、路由、导航等功能。在MaterialApp内部,有一系列用于构建Material风格界面的重要组件和属性。

class MaterialApp extends WidgetsApp {
  const MaterialApp({
    Key? key,
    this.navigatorKey,
    this.builder,
    this.title = '',
    this.onGenerateTitle,
    this.theme,
    this.darkTheme,
    this.themeMode = ThemeMode.system,
    this.color,
    this.home,
    this.initialRoute,
    this.onGenerateRoute,
    this.onUnknownRoute,
    this.navigatorObservers = const <NavigatorObserver>[],
    this.builder,
    this.debugShowMaterialGrid = false,
    this.showPerformanceOverlay = false,
    this.checkerboardRasterCacheImages = false,
    this.checkerboardOffscreenLayers = false,
    this.showSemanticsDebugger = false,
    this.debugShowCheckedModeBanner = true,
  }) : super(
          key: key,
          navigatorKey: navigatorKey,
          builder: builder,
          title: title,
          onGenerateTitle: onGenerateTitle,
          theme: theme,
          home: home,
          initialRoute: initialRoute,
          onGenerateRoute: onGenerateRoute,
          onUnknownRoute: onUnknownRoute,
          navigatorObservers: navigatorObservers,
          builder: builder,
          debugShowMaterialGrid: debugShowMaterialGrid,
          showPerformanceOverlay: showPerformanceOverlay,
          checkerboardRasterCacheImages: checkerboardRasterCacheImages,
          checkerboardOffscreenLayers: checkerboardOffscreenLayers,
          showSemanticsDebugger: showSemanticsDebugger,
          debugShowCheckedModeBanner: debugShowCheckedModeBanner,
        );
}

从上述代码可以看出,MaterialApp接受了众多参数,其中theme用于设置应用的主题,包括颜色、字体、形状等样式信息。home指定应用的初始页面,onGenerateRoute等属性用于路由管理。

主题(Theme)的实现

Material Design的主题在ThemeData类中定义。ThemeData包含了各种与外观相关的属性,例如颜色、字体、组件样式等。

class ThemeData extends Diagnosticable {
  const ThemeData({
    this.brightness,
    this.primaryColor,
    this.primaryColorVariant,
    this.secondaryColor,
    this.secondaryColorVariant,
    this.errorColor,
    this.canvasColor,
    this.scaffoldBackgroundColor,
    this.backgroundColor,
    this.dialogBackgroundColor,
    this.focusColor,
    this.hoverColor,
    this.selectionColor,
    this.disabledColor,
    this.toggleableActiveColor,
    this.accentColor,
    this.accentColorBrightness,
    this.textSelectionColor,
    this.textSelectionHandleColor,
    this.highlightColor,
    this.splashColor,
    this.hoverColor,
    this.splashFactory,
    this.rippleRadius,
    this.primaryTextTheme,
    this.accentTextTheme,
    this.textTheme,
    this.captionTheme,
    this.iconTheme,
    this.accentIconTheme,
    this.sliderTheme,
    this.elevatedButtonTheme,
    this.filledButtonTheme,
    this.textButtonTheme,
    this.outlinedButtonTheme,
    this.toggleButtonsTheme,
    this.chipTheme,
    this.toolbarTheme,
    this.bottomAppBarTheme,
    this.drawerTheme,
    this.dialogTheme,
    this.popupMenuTheme,
    this.cardTheme,
    this.dividerTheme,
    this.listTileTheme,
    this.radioTheme,
    this.checkboxTheme,
    this.switchTheme,
    this.textFormFieldTheme,
    this.inputDecorationTheme,
    this.bottomSheetTheme,
    this.snackBarTheme,
    this.expansionTileTheme,
    this.tabBarTheme,
    this.appBarTheme,
    this.badgeTheme,
    this.bottomNavigationBarTheme,
    this.navigationBarTheme,
    this.pageTransitionsTheme,
    this.scrollbarTheme,
    this.typography,
    this.useMaterial3 = false,
  });
}

以颜色为例,primaryColor定义了应用的主色调,通常用于AppBar、重要按钮等。canvasColor用于定义画布颜色,即大多数组件的背景颜色。字体方面,textTheme定义了不同文本样式,如标题、正文等的字体大小、颜色和粗细。

按钮组件的实现

在Material组件库中,按钮是常见的交互组件。以ElevatedButton为例,其源码实现如下:

class ElevatedButton extends StatelessWidget {
  const ElevatedButton({
    Key? key,
    required this.onPressed,
    this.onLongPress,
    this.style,
    this.focusNode,
    this.autofocus = false,
    required this.child,
  }) : super(key: key);

  final VoidCallback? onPressed;
  final VoidCallback? onLongPress;
  final ButtonStyle? style;
  final FocusNode? focusNode;
  final bool autofocus;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final ButtonStyle effectiveStyle = style ?? ElevatedButton.styleFrom();
    return RawMaterialButton(
      onPressed: onPressed,
      onLongPress: onLongPress,
      focusNode: focusNode,
      autofocus: autofocus,
      child: child,
      elevation: effectiveStyle.elevation?.resolve({}) ?? 0.0,
      shape: effectiveStyle.shape?.resolve({}) ?? const RoundedRectangleBorder(),
      fillColor: effectiveStyle.backgroundColor?.resolve({}),
      textStyle: effectiveStyle.textStyle?.resolve({}),
      splashColor: effectiveStyle.splashColor?.resolve({}),
      highlightColor: effectiveStyle.highlightColor?.resolve({}),
      hoverColor: effectiveStyle.hoverColor?.resolve({}),
      constraints: effectiveStyle.minimumSize?.resolve({}) ?? const Size(88.0, 36.0),
    );
  }
}

ElevatedButton通过RawMaterialButton来实现其外观和交互逻辑。style属性可以用于定制按钮的样式,包括背景颜色、形状、文本样式等。当按钮被按下时,onPressed回调函数会被触发,开发者可以在这个回调中处理业务逻辑。

导航栏(AppBar)的实现

AppBar是Material Design应用中常见的顶部导航栏组件。其源码如下:

class AppBar extends StatefulWidget implements PreferredSizeWidget {
  const AppBar({
    Key? key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
    this.shadowColor,
    this.backgroundColor,
    this.foregroundColor,
    this.iconTheme,
    this.actionsIconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    this.toolbarHeight,
    this.toolbarOpacity = 1.0,
    this.bottomOpacity = 1.0,
  }) : super(key: key);

  //... 众多属性定义

  @override
  _AppBarState createState() => _AppBarState();

  @override
  Size get preferredSize {
    final double toolbarHeight = this.toolbarHeight?? kToolbarHeight;
    final double bottomHeight = bottom?.preferredSize.height?? 0.0;
    return Size.fromHeight(toolbarHeight + bottomHeight);
  }
}

class _AppBarState extends State<AppBar> with TickerProviderStateMixin {
  //... 状态管理相关代码

  @override
  Widget build(BuildContext context) {
    //... 构建AppBar的具体逻辑
    return Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      child: ColoredBox(
        color: effectiveBackgroundColor,
        child: ConstrainedBox(
          constraints: BoxConstraints(minHeight: kToolbarHeight),
          child: Stack(
            children: <Widget>[
              //... 构建leading、title、actions等子组件
            ],
          ),
        ),
      ),
    );
  }
}

AppBar继承自StatefulWidget,这意味着它具有可变状态。leading通常是导航栏左侧的按钮,如返回按钮;title是导航栏中间的标题;actions是导航栏右侧的操作按钮。flexibleSpace可以用于自定义导航栏的背景区域,bottom可以添加一个底部栏,如TabBar。通过这些属性的组合,开发者可以构建出符合Material Design规范的各种导航栏样式。

Cupertino组件库源码分析

Cupertino组件库的核心结构

Cupertino组件库的核心是CupertinoAppCupertinoApp同样继承自WidgetsApp,为应用提供了Cupertino风格的主题、路由、导航等功能。

class CupertinoApp extends WidgetsApp {
  const CupertinoApp({
    Key? key,
    this.navigatorKey,
    this.builder,
    this.title = '',
    this.onGenerateTitle,
    this.theme,
    this.home,
    this.initialRoute,
    this.onGenerateRoute,
    this.onUnknownRoute,
    this.navigatorObservers = const <NavigatorObserver>[],
    this.builder,
    this.showPerformanceOverlay = false,
    this.checkerboardRasterCacheImages = false,
    this.checkerboardOffscreenLayers = false,
    this.showSemanticsDebugger = false,
    this.debugShowCheckedModeBanner = true,
  }) : super(
          key: key,
          navigatorKey: navigatorKey,
          builder: builder,
          title: title,
          onGenerateTitle: onGenerateTitle,
          theme: theme,
          home: home,
          initialRoute: initialRoute,
          onGenerateRoute: onGenerateRoute,
          onUnknownRoute: onUnknownRoute,
          navigatorObservers: navigatorObservers,
          builder: builder,
          showPerformanceOverlay: showPerformanceOverlay,
          checkerboardRasterCacheImages: checkerboardRasterCacheImages,
          checkerboardOffscreenLayers: checkerboardOffscreenLayers,
          showSemanticsDebugger: showSemanticsDebugger,
          debugShowCheckedModeBanner: debugShowCheckedModeBanner,
        );
}

MaterialApp类似,CupertinoApp也接受了许多参数,其中theme用于设置Cupertino风格的主题,home指定初始页面,路由相关属性用于管理页面导航。

主题(Theme)的实现

Cupertino的主题在CupertinoThemeData类中定义。

class CupertinoThemeData extends Diagnosticable {
  const CupertinoThemeData({
    this.brightness,
    this.primaryColor,
    this.primaryContrastingColor,
    this.barBackgroundColor,
    this.backgroundColor,
    this.scaffoldBackgroundColor,
    this.textTheme,
    this.actionTextTheme,
    this.navBarTheme,
    this.tabBarTheme,
    this.pickerTheme,
    this.sliderTheme,
    this.switchTheme,
    this.textSelectionColor,
    this.textSelectionHandleColor,
    this.indicatorColor,
    this.focusColor,
    this.scrollbarTheme,
  });
}

brightness定义了主题的明暗模式,primaryColor是应用的主色调,常用于按钮、选中状态等。barBackgroundColor用于设置导航栏、标签栏等栏的背景颜色。与Material的主题相比,Cupertino的主题更侧重于简洁、明快的风格,并且在颜色和组件样式上更符合iOS的设计规范。

按钮组件的实现

CupertinoButton为例,其源码如下:

class CupertinoButton extends StatelessWidget {
  const CupertinoButton({
    Key? key,
    this.padding = const EdgeInsets.symmetric(horizontal: 16.0),
    this.minSize = 44.0,
    this.pressedOpacity = 0.5,
    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
    this.color,
    this.disabledColor,
    this.splashColor,
    required this.onPressed,
    this.onLongPress,
    this.child,
  }) : assert(pressedOpacity >= 0.0 && pressedOpacity <= 1.0),
       super(key: key);

  //... 属性定义

  @override
  Widget build(BuildContext context) {
    final CupertinoThemeData theme = CupertinoTheme.of(context);
    final Color? effectiveColor = color?? theme.primaryColor;
    final Color? effectiveDisabledColor = disabledColor?? theme.disabledColor;
    return SizedBox(
      height: minSize,
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          borderRadius: borderRadius,
          onTap: onPressed,
          onLongPress: onLongPress,
          splashColor: splashColor?? effectiveColor.withOpacity(0.12),
          child: Padding(
            padding: padding,
            child: DefaultTextStyle(
              style: TextStyle(
                color: onPressed!= null? effectiveColor : effectiveDisabledColor,
                fontSize: CupertinoTheme.of(context).textTheme.textStyle.fontSize,
              ),
              child: child,
            ),
          ),
        ),
      ),
    );
  }
}

CupertinoButton通过InkWell来实现点击交互,利用Material组件设置透明背景,以符合Cupertino风格的简洁外观。paddingminSize等属性用于控制按钮的大小和内边距。onPressedonLongPress回调函数分别处理按钮的点击和长按事件。

导航栏(NavigationBar)的实现

Cupertino的导航栏在CupertinoNavigationBar中实现。

class CupertinoNavigationBar extends StatelessWidget {
  const CupertinoNavigationBar({
    Key? key,
    this.previousPageTitle,
    this.middle,
    this.automaticallyImplyLeading = true,
    this.leading,
    this.trailing,
    this.backgroundColor,
    this.border,
    this.padding,
    this.obscureBackground = false,
    this.translucent = true,
    this.semanticTitle,
  }) : super(key: key);

  //... 属性定义

  @override
  Widget build(BuildContext context) {
    final CupertinoThemeData theme = CupertinoTheme.of(context);
    final Color backgroundColor = this.backgroundColor?? theme.barBackgroundColor;
    final EdgeInsetsGeometry effectivePadding = padding?? const EdgeInsetsDirectional.only(top: kSafeAreaTop);
    return Container(
      height: kNavigationBarHeight,
      padding: effectivePadding,
      decoration: BoxDecoration(
        color: backgroundColor,
        border: border,
      ),
      child: Row(
        children: <Widget>[
          //... 构建leading、middle、trailing子组件
        ],
      ),
    );
  }
}

CupertinoNavigationBar通过Row来水平排列leading(通常是返回按钮)、middle(标题)和trailing(操作按钮)。backgroundColor用于设置导航栏的背景颜色,border可以设置导航栏的边框。automaticallyImplyLeading属性可以自动根据页面层级决定是否显示返回按钮,符合iOS导航栏的交互习惯。

Material与Cupertino对比与选择

设计风格对比

Material Design强调物理隐喻,通过光影效果和动画来营造出层次感和立体感,色彩丰富且具有鲜明的视觉特征。例如,卡片组件通过阴影来体现其在页面上的层级关系。而Cupertino设计风格则更加简洁、圆润,注重拟物风格,色彩相对柔和,以贴合iOS系统的整体风格。例如,Cupertino的按钮通常没有明显的阴影,而是通过颜色变化和点击反馈来实现交互。

交互体验对比

在交互方面,Material Design具有丰富的动画效果,如涟漪效果用于按钮点击反馈,转场动画用于页面切换等,这些动画效果提升了用户操作的流畅感和趣味性。Cupertino的交互则更加直接和简洁,例如导航栏的返回按钮操作简单明了,列表的滑动删除等交互也遵循iOS系统的标准操作方式,让iOS用户感到熟悉和舒适。

如何选择

当开发面向Android平台或者需要遵循谷歌设计规范的应用时,Material组件库是首选,它能够提供与Android原生应用一致的用户体验。而当开发面向iOS平台或者追求iOS风格用户体验的应用时,Cupertino组件库是更好的选择。当然,在一些跨平台应用中,如果希望在不同平台上呈现不同的设计风格,也可以根据平台判断来动态切换使用Material或Cupertino组件库。例如:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Material App'),
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: () {},
              child: Text('Click me'),
            ),
          ),
        ),
      );
    } else {
      return CupertinoApp(
        home: CupertinoPageScaffold(
          navigationBar: CupertinoNavigationBar(
            middle: Text('Cupertino App'),
          ),
          child: Center(
            child: CupertinoButton(
              onPressed: () {},
              child: Text('Click me'),
            ),
          ),
        ),
      );
    }
  }
}

上述代码通过defaultTargetPlatform判断当前运行平台,然后选择使用Material或Cupertino组件库来构建应用,从而为不同平台的用户提供符合其平台习惯的用户体验。

总结

Material和Cupertino组件库是Flutter框架中构建用户界面的重要组成部分。深入分析它们的源码,有助于开发者理解其实现原理,更好地定制和扩展组件,以满足不同应用的需求。在实际开发中,根据应用的目标平台和设计需求,合理选择和使用这两种组件库,能够为用户带来更加优质和一致的体验。无论是追求Material Design的丰富动画和物理隐喻,还是Cupertino的简洁直接和iOS风格,Flutter都提供了强大的工具和灵活的实现方式,让开发者能够快速构建出高质量的跨平台移动应用。