Flutter学习基本组件之基本列表Gradview组件

Posted lxlx1798

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习基本组件之基本列表Gradview组件相关的知识,希望对你有一定的参考价值。

一,概述  

数据量很大的时用矩阵方式排列比较清晰,此时用网格列表组件,即为GridView组件,可实现多行多列的应用场景。 使用GridView创建网格列表有多种方式:

  • GridView.count 通过单行展示个数创建GridView。
  • GridView.extend通过最大宽度创建GridView。

二,构造函数  

  • GridView

    • 使用场景:使用自定义SliverGridDelegate创建可滚动的2D小部件数组
    • 构造函数
      GridView(Key key, 
      Axis scrollDirection: Axis.vertical,
      bool reverse: false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap: false, EdgeInsetsGeometry padding, @required SliverGridDelegate gridDelegate, bool addAutomaticKeepAlives: true, bool addRepaintBoundaries: true, bool addSemanticIndexes: true, double cacheExtent, List<Widget> children: const [], int semanticChildCount
      )
  • GridView.count

    • 使用场景:创建一个可滚动的2D小部件数组,在横轴上具有固定数量的网格块
    • 构造函数
      GridView.count(Key key, Axis scrollDirection: Axis.vertical, 
        bool reverse: false, ScrollController controller, 
        bool primary, ScrollPhysics physics, bool shrinkWrap: false, 
        EdgeInsetsGeometry padding, @required int crossAxisCount,
        double mainAxisSpacing: 0.0, double crossAxisSpacing: 0.0, 
         double childAspectRatio: 1.0, bool addAutomaticKeepAlives: true,
         bool addRepaintBoundaries: true, 
         bool addSemanticIndexes: true,
         double cacheExtent, List<Widget> children: const [], 
         int semanticChildCount 
      )
    • 分析和使用
      Widget gridViewDefaultCount(List<BaseBean> list) 
          return GridView.count(
      //      padding: EdgeInsets.all(5.0),
            //一行多少个
            crossAxisCount: 5,
            //滚动方向
            scrollDirection: Axis.vertical,
            // 左右间隔
            crossAxisSpacing: 10.0,
            // 上下间隔
            mainAxisSpacing: 10.0,
            //宽高比
            childAspectRatio: 2 / 5,
      
            children: initListWidget(list),
          );
        
      
      List<Widget> initListWidget(List<BaseBean> list) 
          List<Widget> lists = [];
          for (var item in list) 
            lists.add(new Container(
              height: 50.0,
              width: 50.0,
              color: Colors.yellow,
              child: new Center(
                  child: new Text(
                item.age.toString(),
              )),
            ));
          
          return lists;
        
  • GridView.extent

    • 使用场景:使用每个都具有最大横轴范围的 网格块 创建可滚动的2D小部件数组。
    • 构造函数
      GridView.extent(Key key, Axis scrollDirection: Axis.vertical,
         bool reverse: false, ScrollController controller,
         bool primary, ScrollPhysics physics, 
         bool shrinkWrap: false, EdgeInsetsGeometry padding,
         @required double maxCrossAxisExtent,
         double mainAxisSpacing: 0.0, double crossAxisSpacing: 0.0,
         double childAspectRatio: 1.0, 
         bool addAutomaticKeepAlives: true,
         bool addRepaintBoundaries: true, 
         bool addSemanticIndexes: true,
         List<Widget> children: const [], 
         int semanticChildCount 
      )
    • 分析和使用  
      ///GridView.extent 允许您指定项的最大像素宽度
        Widget gridViewDefaultExtent(List<BaseBean> list) 
          return GridView.extent(
            ///设置item的最大像素宽度  比如 130
            maxCrossAxisExtent: 130.0,
            ///其他属性和count一样
            children: initListWidget(list),
          );
        
  • GridView.builder

    • 使用场景:创建按需创建的可滚动的2D小部件数组
    • 构造函数
      GridView.builder(Key key, Axis scrollDirection: Axis.vertical,
         bool reverse: false, ScrollController controller, 
          bool primary, ScrollPhysics physics,
          bool shrinkWrap: false, EdgeInsetsGeometry padding, 
          @required SliverGridDelegate gridDelegate, 
          @required IndexedWidgetBuilder itemBuilder,
          int itemCount, bool addAutomaticKeepAlives: true,
          bool addRepaintBoundaries: true, 
          bool addSemanticIndexes: true, 
          double cacheExtent, int semanticChildCount 
      )
    • 分析和使用
      ///GridView.builder  可以定义gridDelegate的模式
        Widget gridViewDefaultBuilder(List<BaseBean> list) 
          return GridView.builder(
              gridDelegate: MyGridViewDefaultCustom(
                crossAxisCount: 2,
                mainAxisSpacing: 10.0,
                crossAxisSpacing: 10.0,
                childAspectRatio: 1.0,
              ),
              itemBuilder: (context, i) => new Container(
                    child: new Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: <Widget>[
                        new Text(
                          "$list[i].name",
                          style: new TextStyle(fontSize: 18.0, color: Colors.red),
                        ),
                        new Text(
                          "$list[i].age",
                          style: new TextStyle(fontSize: 18.0, color: Colors.green),
                        ),
                        new Text(
                          "$list[i].content",
                          style: new TextStyle(fontSize: 18.0, color: Colors.blue),
                        ),
                      ],
                    ),
               ));
        
      ///自定义SliverGridDelegate
      class MyGridViewDefaultCustom extends SliverGridDelegate 
        ///横轴上的子节点数。  一行多少个child
        final int crossAxisCount;
      
        ///沿主轴的每个子节点之间的逻辑像素数。 默认垂直方向的子child间距  这里的是主轴方向 当你改变 scrollDirection: Axis.vertical,就是改变了主轴发方向
        final double mainAxisSpacing;
      
        ///沿横轴的每个子节点之间的逻辑像素数。默认水平方向的子child间距
        final double crossAxisSpacing;
      
        ///每个孩子的横轴与主轴范围的比率。 child的宽高比  常用来处理child的适配
        final double childAspectRatio;
      
        bool _debugAssertIsValid() 
          assert(mainAxisSpacing >= 0.0);
          assert(crossAxisSpacing >= 0.0);
          assert(childAspectRatio > 0.0);
          return true;
        
      
        const MyGridViewDefaultCustom(
          @required this.crossAxisCount,
          this.mainAxisSpacing = 0.0,
          this.crossAxisSpacing = 0.0,
          this.childAspectRatio = 1.0,
        )  : assert(crossAxisCount != null && crossAxisCount > 0),
              assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
              assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
              assert(childAspectRatio != null && childAspectRatio > 0);
      
        ///  返回值有关网格中图块大小和位置的信息。这里就是处理怎么摆放 我们可以自己定义
        ///   SliverGridLayout是抽象类  SliverGridRegularTileLayout继承于SliverGridLayout是抽象类
        @override
        SliverGridLayout getLayout(SliverConstraints constraints) 
          // TODO: implement getLayout
          assert(_debugAssertIsValid());
      
          ///对参数的修饰 自定义
          final double usableCrossAxisExtent =
              constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
          final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
          final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
          return MySliverGridLayout(
            crossAxisCount: crossAxisCount,
            mainAxisStride: childMainAxisExtent + mainAxisSpacing,
            crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
            childMainAxisExtent: childMainAxisExtent,
            childCrossAxisExtent: childCrossAxisExtent,
            reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
          );
        
      
        /// 和ListView的 shouldRebuild 作用一样   之前的实例和新进来的实例是相同的就返回true
        @override
        bool shouldRelayout(SliverGridDelegate oldDelegate) 
          // TODO: implement shouldRelayout
          return true;
        
      
  • GridView.custom 

    • 使用场景:使用自定义SliverGridDelegate和自定义SliverChildDelegate创建可滚动的2D小部件数组
    • 构造函数
      GridView.custom(Key key, Axis scrollDirection: Axis.vertical,
        bool reverse: false, ScrollController controller, 
        bool primary, ScrollPhysics physics, 
        bool shrinkWrap: false, 
        EdgeInsetsGeometry padding,
        @required SliverGridDelegate gridDelegate,
        @required SliverChildDelegate childrenDelegate, 
        double cacheExtent,
        int semanticChildCount )
    • 分析和使用  
       ///GridView.custom 就是自己定制规则
        /// 这里说一下 GridView.count gridDelegate 其实就是内部实现 SliverGridDelegateWithFixedCrossAxisCount
        /// GridView.extent gridDelegate 其实就是内部实现 SliverGridDelegateWithMaxCrossAxisExtent
        Widget gridViewDefaultCustom(List<BaseBean> list) 
          return GridView.custom(
            gridDelegate: MyGridViewDefaultCustom(
              crossAxisCount: 2,
              mainAxisSpacing: 10.0,
              crossAxisSpacing: 10.0,
              childAspectRatio: 1.0,
            ),
            childrenDelegate: MyGridChildrenDelegate(
              (BuildContext context, int i) 
                return new Container(
                    child: new Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    new Text(
                      "$list[i].name",
                      style: new TextStyle(fontSize: 18.0, color: Colors.red),
                    ),
                    new Text(
                      "$list[i].age",
                      style: new TextStyle(fontSize: 18.0, color: Colors.green),
                    ),
                    new Text(
                      "$list[i].content",
                      style: new TextStyle(fontSize: 18.0, color: Colors.blue),
                    ),
                  ],
                ));
              ,
              childCount: list.length,
            ),
          );
        

       

      /**
       * 继承SliverChildBuilderDelegate  可以对列表的监听
       */
      class MyGridChildrenDelegate extends SliverChildBuilderDelegate 
        MyGridChildrenDelegate(
          Widget Function(BuildContext, int) builder, 
          int childCount,
          bool addAutomaticKeepAlive = true,
          bool addRepaintBoundaries = true,
        ) : super(builder,
                  childCount: childCount,
                  addAutomaticKeepAlives: addAutomaticKeepAlive,
                  addRepaintBoundaries: addRepaintBoundaries);
      
        ///监听 在可见的列表中 显示的第一个位置和最后一个位置
        @override
        void didFinishLayout(int firstIndex, int lastIndex) 
          print(firstIndex: $firstIndex, lastIndex: $lastIndex);
        
      
        ///可不重写 重写不能为null  默认是true  添加进来的实例与之前的实例是否相同 相同返回true 反之false
        ///listView 暂时没有看到应用场景 源码中使用在 SliverFillViewport 中
        @override
        bool shouldRebuild(SliverChildBuilderDelegate oldDelegate) 
          // TODO: implement shouldRebuild
          print("oldDelegate$oldDelegate");
          return super.shouldRebuild(oldDelegate);
        
      

三,参数详解

  • gridDelegate:

   构造 GridView 的委托者,GridView.count 就相当于指定 gridDelegate 为 SliverGridDelegateWithFixedCrossAxisCount,GridView.extent 就相当于指定 gridDelegate 为 SliverGridDelegateWithMaxCrossAxisExtent,它们相当于对普通构造方法的一种封装。它的值是一个 SliverGridDelegate 对象,参考 2.1 SliverGridDelegate。

  • cacheExtent:同 ListView,预加载的区域。
  • controller:同 ListView,滑动监听,值为一个 ScrollController 对象,这个属性应该可以用来做下拉刷新和上垃加载,后面详细研究。
  • padding:同 ListView,整个 GridView 的内间距。
  • physics:同 ListView,设置 GridView 如何响应用户的滑动行为,值为一个 ScrollPhysics 对象,它的实现类常用的有:
    • AlwaysScrollableScrollPhysics:总是可以滑动。
    • NeverScrollableScrollPhysics:禁止滚动。
    • BouncingScrollPhysics:内容超过一屏,上拉有回弹效果。
    • ClampingScrollPhysics:包裹内容,不会有回弹,感觉跟 AlwaysScrollableScrollPhysics 差不多。
  • reverse:Item 的顺序是否反转,若为 true 则反转,这个翻转只是行翻转,即第一行变成最后一行,但是每一行中的子组件还是从左往右摆放的,用到该属性的开发情景较少。
  • scrollDirection:GirdView 的方向,为 Axis.vertical 表示纵向,为 Axis.horizontal 表示横向,横向的话 CrossAxis 和 MainAxis 表示的轴也会调换,为 Axis.Horizontal 的情况也较少。
  • semanticChildCount:不太清楚。
  • shrinkWrap:不太清楚。
  • children:子组件,不用多说。

四,关于SliverGridDelegate

构造 GridView 的委托者,它有两个实现类:

  • SliverGridDelegateWithFixedCrossAxisCount

      该委托者通常用于每一行的子组件个数固定的情况,它可以指定如下几个属性:

     

    • crossAxisCount:必传参数,Cross 轴(在 GridView 中通常是横轴,即每一行)子组件个数。

       

    • childAspectRatio:子组件宽高比,如 2 表示宽:高=2:1,如 0.5 表示宽:高=0.5:1=1:2,简单来说就是值大于 1 就会宽大于高,小于 1 就会宽小于高。

       

    • crossAxisSpacing:Cross 轴子组件的间隔,一行中第一个子组件左边不会添加间隔,最后一个子组件右边不会添加间隔,这一点很棒。

       

    • mainAxisSpacing:Main 轴(在 GridView 中通常是纵轴,即每一列)子组件间隔,也就是每一行之间的间隔,同样第一行的上边和最后一行的下边不会添加间隔。

  • SliverGridDelegateWithMaxCrossAxisExtent
    • maxCrossAxisExtent:必传参数,
    • Cross 轴(在 GridView 中通常是横轴,即每一行)子组件最大宽度,会根据该值来决定一行摆放几个子组件。

    其余属性 childAspectRatio、crossAxisSpacing、mainAxisSpacing 同 SliverGridDelegateWithFixedCrossAxisCount。

五,示例demo  

import package:flutter/material.dart;
import package:flutter/src/rendering/sliver.dart;
import package:flutter/src/rendering/sliver_grid.dart;
import package:flutter_vscode/listview_demo.dart;

class GridViewDemo extends StatefulWidget 
  @override
  _GridViewDemoState createState() => new _GridViewDemoState();


class _GridViewDemoState extends State<GridViewDemo> 
  List<BaseBean> gridList;

  @override
  void initState() 
    // TODO: implement initState
    super.initState();
    gridList = new List<BaseBean>.generate(
        32, (i) => new BaseBean("name$i", i, "content=$i"));
  

  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: "",
      home: new Scaffold(
        appBar: new AppBar(
          centerTitle: true,
          title: new Text("GridView"),
        ),
        body: gridViewDefaultCount(gridList),
      ),
    );
  

  List<Widget> initListWidget(List<BaseBean> list) 
    List<Widget> lists = [];
    for (var item in list) 
      lists.add(new Container(
        height: 50.0,
        width: 50.0,
        color: Colors.yellow,
        child: new Center(
            child: new Text(
          item.age.toString(),
        )),
      ));
    
    return lists;
  

  Widget gridViewDefaultCount(List<BaseBean> list) 
    return GridView.count(
//      padding: EdgeInsets.all(5.0),
      crossAxisCount: 5,
      //一行多少个
      scrollDirection: Axis.vertical,
      //滚动方向
      crossAxisSpacing: 10.0,
      // 左右间隔
      mainAxisSpacing: 10.0,
      // 上下间隔
      childAspectRatio: 2 / 5,
      //宽高比
      children: initListWidget(list),
    );
  

  ///GridView.extent 允许您指定项的最大像素宽度
  Widget gridViewDefaultExtent(List<BaseBean> list) 
    return GridView.extent(
      ///设置item的最大像素宽度  比如 130
      maxCrossAxisExtent: 130.0,

      ///其他属性和count一样
      children: initListWidget(list),
    );
  

  ///GridView.custom 就是自己定制规则
  /// 这里说一下 GridView.count gridDelegate 其实就是内部实现 SliverGridDelegateWithFixedCrossAxisCount
  /// GridView.extent gridDelegate 其实就是内部实现 SliverGridDelegateWithMaxCrossAxisExtent
  Widget gridViewDefaultCustom(List<BaseBean> list) 
    return GridView.custom(
      gridDelegate: MyGridViewDefaultCustom(
        crossAxisCount: 2,
        mainAxisSpacing: 10.0,
        crossAxisSpacing: 10.0,
        childAspectRatio: 1.0,
      ),
      childrenDelegate: MyGridChildrenDelegate(
        (BuildContext context, int i) 
          return new Container(
              child: new Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              new Text(
                "$list[i].name",
                style: new TextStyle(fontSize: 18.0, color: Colors.red),
              ),
              new Text(
                "$list[i].age",
                style: new TextStyle(fontSize: 18.0, color: Colors.green),
              ),
              new Text(
                "$list[i].content",
                style: new TextStyle(fontSize: 18.0, color: Colors.blue),
              ),
            ],
          ));
        ,
        childCount: list.length,
      ),
    );
  

  ///GridView.builder  可以定义gridDelegate的模式
  Widget gridViewDefaultBuilder(List<BaseBean> list) 
    return GridView.builder(
        gridDelegate: MyGridViewDefaultCustom(
          crossAxisCount: 2,
          mainAxisSpacing: 10.0,
          crossAxisSpacing: 10.0,
          childAspectRatio: 1.0,
        ),
        itemBuilder: (context, i) => new Container(
              child: new Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  new Text(
                    "$list[i].name",
                    style: new TextStyle(fontSize: 18.0, color: Colors.red),
                  ),
                  new Text(
                    "$list[i].age",
                    style: new TextStyle(fontSize: 18.0, color: Colors.green),
                  ),
                  new Text(
                    "$list[i].content",
                    style: new TextStyle(fontSize: 18.0, color: Colors.blue),
                  ),
                ],
              ),
            ));
  


///自定义SliverGridDelegate
class MyGridViewDefaultCustom extends SliverGridDelegate 
  ///横轴上的子节点数。  一行多少个child
  final int crossAxisCount;

  ///沿主轴的每个子节点之间的逻辑像素数。 默认垂直方向的子child间距  这里的是主轴方向 当你改变 scrollDirection: Axis.vertical,就是改变了主轴发方向
  final double mainAxisSpacing;

  ///沿横轴的每个子节点之间的逻辑像素数。默认水平方向的子child间距
  final double crossAxisSpacing;

  ///每个孩子的横轴与主轴范围的比率。 child的宽高比  常用来处理child的适配
  final double childAspectRatio;

  bool _debugAssertIsValid() 
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childAspectRatio > 0.0);
    return true;
  

  const MyGridViewDefaultCustom(
    @required this.crossAxisCount,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
  )  : assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
        assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
        assert(childAspectRatio != null && childAspectRatio > 0);

  ///  返回值有关网格中图块大小和位置的信息。这里就是处理怎么摆放 我们可以自己定义
  ///   SliverGridLayout是抽象类  SliverGridRegularTileLayout继承于SliverGridLayout是抽象类
  @override
  SliverGridLayout getLayout(SliverConstraints constraints) 
    // TODO: implement getLayout
    assert(_debugAssertIsValid());

    ///对参数的修饰 自定义
    final double usableCrossAxisExtent =
        constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
    return MySliverGridLayout(
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  

  /// 和ListView的 shouldRebuild 作用一样   之前的实例和新进来的实例是相同的就返回true
  @override
  bool shouldRelayout(SliverGridDelegate oldDelegate) 
    // TODO: implement shouldRelayout
    return true;
  


///自定义SliverGridLayout
class MySliverGridLayout extends SliverGridLayout 
  final int crossAxisCount;

  final double mainAxisStride;

  final double crossAxisStride;

  final double childMainAxisExtent;

  final double childCrossAxisExtent;

  final bool reverseCrossAxis;

  const MySliverGridLayout(
    @required this.crossAxisCount,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
    @required this.reverseCrossAxis,
  )  : assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisStride != null && mainAxisStride >= 0),
        assert(crossAxisStride != null && crossAxisStride >= 0),
        assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
        assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
        assert(reverseCrossAxis != null);

  ///如果有,则完全显示所有图块所需的滚动范围
  ///“childCount”儿童总数。
  ///
  ///子计数永远不会为空。
  @override
  double computeMaxScrollOffset(int childCount) 
    // TODO: implement computeMaxScrollOffset
    return null;
  

  ///具有给定索引的子项的大小和位置。
  @override
  SliverGridGeometry getGeometryForChildIndex(int index) 
    // TODO: implement getGeometryForChildIndex
    return null;
  

  ///在此滚动偏移处(或之前)可见的最大子索引。
  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) 
    // TODO: implement getMaxChildIndexForScrollOffset
    return null;
  

  ///在此滚动偏移处(或之后)可见的最小子索引。
  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) 
    // TODO: implement getMinChildIndexForScrollOffset
    return null;
  


// ignore: slash_for_doc_comments
/**
 * 继承SliverChildBuilderDelegate  可以对列表的监听
 */
class MyGridChildrenDelegate extends SliverChildBuilderDelegate 
  MyGridChildrenDelegate(
    Widget Function(BuildContext, int) builder, 
    int childCount,
    bool addAutomaticKeepAlive = true,
    bool addRepaintBoundaries = true,
  ) : super(builder,
            childCount: childCount,
            addAutomaticKeepAlives: addAutomaticKeepAlive,
            addRepaintBoundaries: addRepaintBoundaries);

  ///监听 在可见的列表中 显示的第一个位置和最后一个位置
  @override
  void didFinishLayout(int firstIndex, int lastIndex) 
    print(firstIndex: $firstIndex, lastIndex: $lastIndex);
  

  ///可不重写 重写不能为null  默认是true  添加进来的实例与之前的实例是否相同 相同返回true 反之false
  ///listView 暂时没有看到应用场景 源码中使用在 SliverFillViewport 中
  @override
  bool shouldRebuild(SliverChildBuilderDelegate oldDelegate) 
    // TODO: implement shouldRebuild
    print("oldDelegate$oldDelegate");
    return super.shouldRebuild(oldDelegate);
  

五,官方文档

  官方文档

以上是关于Flutter学习基本组件之基本列表Gradview组件的主要内容,如果未能解决你的问题,请参考以下文章

Flutter学习基本组件之Webview组件

Flutter学习基本组件之文本组件Text

Flutter学习基本组件之弹窗和提示(SnackBarBottomSheetDialog)

flutter学习ListView

flutter学习:目录结构及基本组件

flutter学习:目录结构及基本组件