Flutter移动应用开发 - 04 Flutter 常用 widget 整理


一开始没想写这么多的,但是这种开发类的知识很多都是具有连带性的,看了官网的几个基础 widget 后顺带也学习了其他几种常用 widget,这里按照 Row, Column, GridView, Container, ListView, Stack, Card, ListTile为主线整理。

在这之前原来想学先布局的,但是发现布局之前还是有很多的 widget 不熟悉,就尝试了几种基础的 widget,特此记录。

但是个人认为还是要知道一些布局的知识的,不然看起来还是相对吃力的,毕竟widget相对细节很多,布局对于app界面的设计会重要一点。

1. 基础 widget

widget在flutter中是一个非常重要的概念。Flutter 从 React 中吸取灵感,通过现代化框架创建出精美的组件。它的核心思想是用 widget 来构建你的 UI 界面。 Widget 描述了在当前的配置和状态下视图所应该呈现的样子。当 widget 的状态改变时,它会重新构建其描述(展示的 UI),框架则会对比前后变化的不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。

这么讲可能有点抽象,这里放一个例子HelloWorld

import 'package:flutter/material.dart';

void main() {
  // runApp(const MyApp());
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

效果如下:

在这里插入图片描述

可以看出,这里的runApp持有传入的widget树,这会被作为根widget,这个树内包含一个子widget树Center,Center下包含它的子widget(Text)。

此外,widget最重要的工作之一就是提供build()方法,这可以用更低级的widget的特性来渲染自己的特性。widgets还分有状态无状态两种形式,无状态的widgets内所有的属性都是final性质的,这意味着它的属性不可改变;相对的,有状态的widgets的属性就可以在其生命周期内改变。那怎么区分有状态和无状态?有状态一般有两个类:

  1. StatefulWidget 类,这个类本身不变
  2. State 类,这个类本身存在于整个生命周期中

在flutter中,widget还是很丰富的,我们这里就挑最常用的几个讲,其他的详见Flutter官网

1.1 Text

Text widget 可以用来在应用内创建带样式的文本。这里放上两个例子,第一个例子偏向于文本各个参数编辑,第二个是多行文字和渐变色。

Text各种参数

注释写的满详细了,图是丑了点,但是基本的几个点都用到了。

在这里插入图片描述

import 'package:flutter/material.dart';

void main() {
  // runApp(const MyApp());
  // 变量
  const String _abc = "Clark";
  // Text1
  runApp(
    Text(
      // 文本; 文本加入变量
      'Hello, world! \n Hello, $_abc!',
      // 位置:left, right, center, justify, start, end
      textAlign: TextAlign.justify,
      // 处理溢出文本:clip(剪掉溢出文本), fade(透明化溢出文本), ellipsis(省略号表溢出), visible(容器外的也可以渲染)
      overflow: TextOverflow.fade,
      // 文字方向:ltr左到右,rtl右到左
      textDirection: TextDirection.ltr,
      // 文字风格,这个就很多了。
      // FontWeight.bold:加粗
      // FontStyle.italic:斜体
      // color: Colors.black.withOpacity(0.6):颜色与透明度 => 注意此时不能使用const Text
      // fontFamily: 'Raleway':字体
      // height: 5:列高
      // fontSize: 18:大小
      style: TextStyle(
        color: Colors.yellow.withOpacity(0.6),
        fontWeight: FontWeight.bold,
        fontStyle: FontStyle.italic,
        fontFamily: 'Raleway',
        height: 5,
        fontSize: 30,
      ),
      // 可以在默认字体下修改
      // style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0)
    ),
  );
}

多种效果合体

为了在一句话中显示不同样式的文字。这里主要强调一个RichText。

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() {
  // Text2
  runApp(
    // 注意:RichText一定要一个textDirection
    RichText(
      textDirection: TextDirection.ltr,
      textAlign: TextAlign.center,
      text: TextSpan(
        children: <TextSpan>[
          TextSpan(
            text: "Hello Clark",
            style: TextStyle(color: Colors.white.withOpacity(0.2), fontSize: 60),
          ),
          TextSpan(
            text: "!!!!!\n",
            style: TextStyle(
                color: Colors.yellow.withOpacity(0.6),
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.italic,
                fontFamily: 'Raleway',
                height: 5,
                fontSize: 30,
              ),
          ),
          TextSpan(
            text: "1234567898765432345678\n",
            style: TextStyle(color: Colors.white.withOpacity(1.0)),
          ),
        ],
      ),
    ),
  );
}

效果如下:

在这里插入图片描述

实例:俩花活(艺术字)

这里有点点超纲,有用到Stack的知识,但是问题不大。这里的stack就是为了让这俩字体合到一起,大致思路是写一个粗一点的蓝字,然后再给它stark一个细一点的白字。

在这里插入图片描述

// Text3
  runApp(
    Stack(
      alignment: Alignment.center,
      children: <Widget>[
        // Stroked text as border.
        Text(
          textDirection: TextDirection.ltr,
          'SCQ CXN FOREVER!!!!!',
          style: TextStyle(
            fontSize: 40,
            foreground: Paint()
              ..style = PaintingStyle.stroke
              ..strokeWidth = 6
              ..color = Colors.blue[700]!,
          ),
        ),
        // Solid text as fill.
        Text(
          textDirection: TextDirection.ltr,
          'SCQ CXN FOREVER!!!!!',
          style: TextStyle(
            fontSize: 40,
            color: Colors.grey[300],
          ),
        ),
      ],
    )
  );

这个也有点超纲,用到了ShaderMask,既然做到了,就在这里讲一下。

这个东西有点像ps里的蒙板,这里面有三种方法:LinearGradientRadialGradientSweepGradient。第一种是线性变化,顺着一个方向渐变颜色,可以横着也可以斜着;第二种是辐射式的变化,有点像中间淡色外边深色的那种;第三种是通过设置开始角度和结束角度,设置渐变范围,按照角度设置。

看上去这仨没啥用,实际上还是非常好用的,在图片处理的时候总有起效。

当然我们这里就是想做个渐变色,上述的一大堆操作都没啥用,就是用了个LinearGradient

在这里插入图片描述

import 'package:flutter/material.dart';

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



class MyApp extends StatelessWidget {

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        primaryColor: Colors.blue,
      ),
      home: Gredient(),
    );
  }
}

class Gredient extends StatelessWidget {
  // const ({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Love Story'),
      ),
      body: Center(
        child: ShaderMask(shaderCallback: (Rect bounds) {
          return LinearGradient(
            begin: Alignment.centerLeft,
            end: Alignment.centerRight,
            colors: [Colors.white, Color(0xFFFFBDE9)],
          ).createShader(Offset.zero & bounds.size);
          },
          child: Text(
            "YWH MJT FOREVER!!!",
            style: TextStyle(
              color: Colors.white,
              fontSize: 50
            ),
          ),
        )

      )
    );
  }
}

1.2 Row, Column

这两个 flex widgets 可以让你在水平 ( Row ) 和垂直( Column ) 方向创建灵活的布局。它是基于 web 的 flexbox 布局模型设计的。

这俩我们不会像Text那样详细尝试,毕竟是个布局相关的组件,细节没多少。主要以Row为主。

row示例

顾名思义,Row就是为了横向排列的,以下图为例,我们完成了三个板块的横向排列。

在这里插入图片描述

代码如下,内含注释:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        // brightness: Brightness.dark,
        primaryColor: Colors.yellowAccent,
      ),
      home: TryRow(),
    );
  }
}

class TryRow extends StatelessWidget {
  // const ({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Love Story'),
        ),
        // 主体部分展示Row
        body: Center(
          child: Row(
            // 这个仍然适用,这里右到左,显示顺序与代码顺序颠倒
            textDirection: TextDirection.rtl,
            children: const <Widget>[
              Expanded(
                child: Text('Deliver features faster', textAlign: TextAlign.center, style: TextStyle(fontSize: 20),),
              ),
              Expanded(
                child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
              ),
              // Expanded的好处是不会存在溢出的情况,溢出部分将自己调整
              // 如果是一个简单的const将会导致溢出的文字之类的超出空间
              Expanded(
                child: FittedBox(
                  child: FlutterLogo(),
                ),
              ),
            ],
          )
        )
    );
  }
}

主轴

row是横向分布的,那所有的元素就是沿着一条横着的主轴排列的,默认情况下,它们将整个轴布满,而轴一般来说是跟屏幕同宽或者同长,跟上一个案例一样。但是可以通过mainAxisSize设置轴的长度、mainAxisAlignment设置对齐关系、使用CrossAxisAlignment设置children在横轴中的定位 。column同理。

具体用法见代码:

Row(
    // max, min
    mainAxisSize: MainAxisSize.max,
    // start, end; children从主轴起点(终点)开始对其
    // center: children设置到主轴中心
    // spaceBetween: children间平分额外空间
    // spaceEvenly: children间,第一个children前最后一个children后评分额外空间
    // spaceAround: spaceEvenly种第一个和最后一个children少一半空间
    mainAxisAlignment: MainAxisAlignment.spaceAround,
    // 这个仍然适用,这里右到左,显示顺序与代码顺序颠倒
    textDirection: TextDirection.rtl,
    // start, end, center, stretch, baseline(仅限Text类,并要求 textBaseline 属性设置为 TextBaseline.alphabetic)
    crossAxisAlignment: CrossAxisAlignment.end,
    children: const <Widget>[
        Expanded(
            child: Text('Deliver features faster', textAlign: TextAlign.center, style: TextStyle(fontSize: 20),),
        ),
        Expanded(
            child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
        ),
        // Expanded的好处是不会存在溢出的情况,溢出部分将自己调整
        // 如果是一个简单的const将会导致溢出的文字之类的超出空间
        Expanded(
            child: FittedBox(
                child: FlutterLogo(),
            ),
        ),
    ],
)

在这里插入图片描述

调整大小

一般来说是用flexible调整大小的,因为 widget 大小在设置flexible前不可变。

在这里插入图片描述

在losse时,方块大小取我们设定的50,出现tight则强制占据剩下的空间,若出现tight+flex的情况,则根据flex大小计算占据的空间。

Expanded

Expanded widget 能够包裹一个 widget 并强制其填满剩余空间,与 Flexible 非常相似。

效果如下:

在这里插入图片描述

SizedBox

这个可以用来调整大小,也可以用来创造空间。

在这里插入图片描述

Spacer

如果要造空位,Spacer比楼上更合适一点,它可以用flex,且放在children中效果优先级高于mainAxisAlignment

在这里插入图片描述

Icon

插入图标的 widget,没什么注意点,会用就行。

在这里插入图片描述

Image

放图片的控件,还是非常重要的。

在放图片前先配置下。先创建assets文件夹,并补上俩子文件夹用于存放图标和图片;再将这俩文件夹写入pubspec.yaml里。

在这里插入图片描述

这边就介绍俩放图片的方法,一个是放网上图片的,一个是放项目图片的。

class TryImg extends StatelessWidget {
  // const TryIcon({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 网图
        Flexible(
          child: Image.network(
              'https://pic2.zhimg.com/80/v2-7091ef428b0b3cd8570f6851e0a4e811_720w.webp',
            ),
          fit: FlexFit.loose
        ),
        // 资源图
        Flexible(
            child: Image.asset(
                'assets/images/18.jpg'
            ),
            fit: FlexFit.loose
        ),

      ],
    );
  }
}

效果如下:

在这里插入图片描述

实例:小黑子界面

顺便学习下Scaffold。

在这里插入图片描述

也有另一个版本,更需要布局这方面的知识

在这里插入图片描述

代码如下

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.dark,
      ),
      home: Scaffold(
        body: Center(
          child: _Commodity()
        ),
        // 悬浮符号
        floatingActionButton:FloatingActionButton(
          onPressed: (){
            // 这里放点击后的事件
          },
          // 悬浮符号的符号
          child: const Icon(Icons.send),
        ),
        // 悬浮符号放中间
        floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
        // 侧拉部分导航
        drawer: Drawer(
          child: SafeArea(
            child:Column( //column widget
              children: const [
                ListTile(
                  leading: Icon(Icons.home),
                  title: Text("Home Page"),
                  subtitle: Text("Subtitle menu 1"),
                ),
                ListTile(
                  leading: Icon(Icons.search),
                  title: Text("Search Page"),
                  subtitle: Text("Subtitle menu 1"),
                ),
                //put more menu items here
              ],
            ),
          ),
        ),
        // 底部导航
        bottomNavigationBar: BottomNavigationBar( //bottom navigation bar on scaffold
          items: const [ //items inside navigation bar
            BottomNavigationBarItem(
              icon: Icon(Icons.add),
              label: "Button 1",
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.search),
              label: "Button 2",
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.camera),
              label: "Button 3",
            ),
            // 要啥再写
          ],
        ),
      ),
    );
  }
}

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

  
  State<_Commodity> createState() => _CommodityState();
}

class _CommodityState extends State<_Commodity> {

  // 存图片 + 文字
  final List goods = [];
  // 商品名字体
  final _GoodsNameFont = const TextStyle(fontSize: 18);
  // 商品简介字体
  final _GoodsIntroduceFont = const TextStyle(fontSize: 18);

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _buildFrame(),
    );
  }

  Widget _buildFrame() {
    final namelist = ["只因", "你", "太美", "迎面走来的你", "让我", "蠢蠢欲动", "就会", "爆炸"];
    final introductionlist = ["小黑子露出鸡脚了吧,你食不食油饼", "你干嘛啊啊啊啊啊啊啊   哎呦!!!"];
    return ListView.builder(
      padding: const EdgeInsets.all(26.0),
      // 对每一个商品都用itemBuilder
      itemBuilder: (context, i) {
        // 加分割线
        if (i.isOdd) return const Divider();
        i = i % (namelist.length * 2);
        final index = i ~/ 2;
        // 加货物
        return _buildline(namelist[index], introductionlist[index % 2], index);
      });
  }

  Widget _buildline(name, introduction, index) {
    // 返回一个Card
    // return Card(
    //     child: ListTile(
    //       leading: Image.asset("assets/images/"+ index.toString() +".jpg"),
    //       title: Text(name, style: _GoodsNameFont),
    //       subtitle: Text(introduction),
    //       trailing: Icon(Icons.more_vert),
    //       isThreeLine: true,
    //     ),
    // );


    // 也可以返回一个ListTile
    return MyListTile(
      thumbnail: Container(
        child: Image.asset("assets/images/"+ index.toString() +".jpg"),
        // 背景色
        decoration: const BoxDecoration(color: Colors.pink),
      ),
      // leading: Image.asset("assets/images/"+ index.toString() +".jpg"),
      title: name,
      subtitle: introduction,
      author: 'Dash',
      publishDate: 'Dec 28',
      readDuration: '5 mins'
    );

  }
}

// 练布局用的,手写一个布局
class MyListTile extends StatelessWidget {
  // 重构函数
  const MyListTile({
    super.key,
    required this.thumbnail,
    required this.title,
    required this.subtitle,
    required this.author,
    required this.publishDate,
    required this.readDuration,
  });

  final Widget thumbnail;
  final String title;
  final String subtitle;
  final String author;
  final String publishDate;
  final String readDuration;

  
  Widget build(BuildContext context) {
    // 给一个padding,用来处理容器和组件之间的距离的。
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 10.0),
      // 定死每个栏目的高度
      child: SizedBox(
        height: 100,
        // 先放方块(图片),再放文字堆叠
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          // 分两块,一块放图片,一块放_ArticleDescription => 那堆文字的合集
          children: <Widget>[
            AspectRatio(
              // 长宽比
              aspectRatio: 1.0,
              child: thumbnail,
            ),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.fromLTRB(20.0, 0.0, 2.0, 0.0),
                child: _ArticleDescription(
                  title: title,
                  subtitle: subtitle,
                  author: author,
                  publishDate: publishDate,
                  readDuration: readDuration,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 那一堆文字的合集
class _ArticleDescription extends StatelessWidget {
  const _ArticleDescription({
    required this.title,
    required this.subtitle,
    required this.author,
    required this.publishDate,
    required this.readDuration,
  });

  final String title;
  final String subtitle;
  final String author;
  final String publishDate;
  final String readDuration;

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                title,
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 18,
                ),
              ),
              // 放一小段空白
              const Padding(padding: EdgeInsets.only(bottom: 2.0)),
              Text(
                subtitle,
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
                style: const TextStyle(
                  fontSize: 14.0,
                  color: Colors.white70,
                ),
              ),
            ],
          ),
        ),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
              Text(
                author,
                style: const TextStyle(
                  fontSize: 12.0,
                  color: Colors.white70,
                ),
              ),
              Text(
                '$publishDate - $readDuration',
                style: const TextStyle(
                  fontSize: 12.0,
                  color: Colors.white70,
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

1.3 Container

Container 这里跟 bootstrap 里面真的很像,在布局方面,个人感觉官网的这个图讲的还是比较清楚的,其实跟 html 大差不差,都是在调这些东西。

在这里插入图片描述

在这里插入图片描述

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildImageColumn();
  }
}

// Container框架
Widget _buildImageColumn() {
  return Container(
    height: 350,
    decoration: const BoxDecoration(
      color: Colors.white30,
    ),
    child: Column(
      children: [
        _buildImageRow(1),
        _buildImageRow(3),
      ],
    ),
  );
}

Widget _buildDecoratedImage(int imageIndex) => Expanded(
  child: Container(
    decoration: BoxDecoration(
      border: Border.all(width: 10, color: Colors.white70),
      borderRadius: const BorderRadius.all(Radius.circular(8)),
    ),
    margin: const EdgeInsets.all(4),
    child: Image.asset('assets/images/$imageIndex.jpg'),
  ),
);

Widget _buildImageRow(int imageIndex) => Row(
  children: [
    _buildDecoratedImage(imageIndex),
    _buildDecoratedImage(imageIndex + 1),
  ],
);

强调几个特点:

  • 在没有限制的情况下,Container 大小会尽可能大。以上图为例,如果没有 height 的限制,这个 Container 将布满屏幕。
  • Container 需要遵守对齐方式,根据子项调整大小,尊重宽度、高度和约束,扩展以适合父项,尽可能小。
  • 如果 widget 没有子项且没有对齐方式,但提供了高度、宽度或约束,Container 会尝试尽可能小,给定这些约束和父约束的组合。

旋转

最基础的操作,顺便提一下 Theme.of(context).textTheme.headline n!transform

在这里插入图片描述

代码如下

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildInline(context);
  }
}

// 斜体 Container
Widget _buildInline(BuildContext context){
  return Center(
    child: Container(
      // 调用主题标题 Theme.of(context).textTheme.headline4!
      // 限制大小
      constraints: BoxConstraints.expand(
        height: Theme.of(context).textTheme.headline4!.fontSize! * 1.1 + 200.0,
      ),
      padding: const EdgeInsets.all(8.0),
      color: Colors.blue[600],
      // 内容居中
      alignment: Alignment.center,
      // 旋转效果
      transform: Matrix4.rotationZ(0.1),
      child: Text('Hello World',
          style: Theme.of(context)
              .textTheme
              .headline4!
              .copyWith(color: Colors.white)),
    )
  );
}

BoxDecoration

这个就是外面的那个壳子。

在这里插入图片描述

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildBox(context);
  }
}


// box例子
Widget _buildBox(BuildContext context){
  return Center(
      child: Container(
        height: Theme.of(context).textTheme.headline1!.fontSize! * 3.5,
        decoration: BoxDecoration(
          color: const Color(0xff7c94b6),
          image: const DecorationImage(
            // 这里跟正常的image操作有点不一样
            image: AssetImage("assets/images/4.jpg"),
            // BoxFit.fill:充满父容器,所以会变形、拉伸
            // BoxFit.contain:尽可能大,保持图片分辨率,适应宽度或长度,会有留白
            // BoxFit.fitWidth:图片填满宽度,高度可能会被截断
            // BoxFit.fitHeight:图片填满高度,宽度可能会被截断
            // BoxFit.cover:充满容器,可能会被截断
            fit: BoxFit.fitHeight,
          ),
          border: Border.all(
            width: 8,
          ),
          borderRadius: BorderRadius.circular(12),
          boxShadow:  const [
            // 这几个参数自己搞,我是真没感觉,会搞就好了
            BoxShadow(
              color: Colors.purple,
              offset: Offset(5.0, 5.0),
              blurRadius: 10.0,
              spreadRadius: 2.0,
            ),
            BoxShadow(
              color: Colors.white,
              offset: Offset(100.0, 50.0),
              blurRadius: 200.0,
              spreadRadius: 10.0,
            ),
          ],
        ),
      )
  );
}

1.4 GridView

这个倒是跟正常安卓程序的网格布局大差不差。它将 widget 作为二维列表展示,提供两个预制的列表,或者你可以自定义网格。当检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。最经典的网格布局的案例就是相册了:

在这里插入图片描述

几个注意点:

  • 内容超出 height 时会产生滚动效果。
  • 一般来说这个是由二维行列表组成的,这个时候 Table 和 DataTable 就比较常用了。

实例:ikun 相册

尝试一个相册实例,注意图片的标题操作和网格操作的几个参数,不熟悉面向对象编程的读者还可以注意下面向对象的类创建与使用。这段代码是我从 gallery 上扒下来的,稍作修改。类似的样式可以复现。

在这里插入图片描述

import 'package:flutter/material.dart';


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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        body: GridGallery(type: GridListDemoType.header),
      )
    );
  }
}

// 一共三种模式可供选择
enum GridListDemoType {
  imageOnly,
  header,
  footer,
}

// 网格
class GridGallery extends StatelessWidget {
  // const GridGallery({Key? key}) : super(key: key);
  const GridGallery({super.key, required this.type});
  final String Title = "你干嘛啊啊啊啊啊~~~";
  final String Subtitle = "小鸡子露出黑脚了吧,你食不食油饼,我抱井惹";

  final GridListDemoType type;

  // 存一个类的列表
  List<_Photo> _photos(BuildContext context) {
    return [
      _Photo(assetName: "assets/images/1.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/2.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/3.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/4.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/5.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/6.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/7.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/8.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/9.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/10.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/11.jpg", title: Title, subtitle: Subtitle),
      _Photo(assetName: "assets/images/0.jpg", title: Title, subtitle: Subtitle),
    ];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: const Text("<   我家giegie"),
      ),
      body: GridView.count(
        restorationId: 'grid_view_demo_grid_offset',
        //
        crossAxisCount: 2,

        mainAxisSpacing: 8,

        crossAxisSpacing: 8,
        // 格子之间的padding
        padding: const EdgeInsets.all(8),
        // 圆角
        childAspectRatio: 1,
        // 一整个输出
        children: _photos(context).map<Widget>((photo) {
          return _GridDemoPhotoItem(
            photo: photo,
            tileStyle: type,
          );
        }).toList(),
      ),
      bottomNavigationBar: BottomNavigationBar( //bottom navigation bar on scaffold
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.collections),
            label: "图库",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.collections_bookmark),
            label: "为您推荐",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.photo_library),
            label: "相簿",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: "搜索",
          ),
        ],
        // 被选中图标颜色
        fixedColor: Colors.greenAccent,
        // 底部背景色
        backgroundColor: Colors.white30,
        // 显示所有图标的label
        showUnselectedLabels: true,
        // 未选中文字颜色
        unselectedItemColor:Colors.white70,
        // 阴影长度
        elevation: 20,
      )
    );
  }
}

// 定义类
class _Photo {
  _Photo({
    required this.assetName,
    required this.title,
    required this.subtitle,
  });

  final String assetName;
  final String title;
  final String subtitle;
}

// 细节操作:调整图片大小适应窗口
class _GridTitleText extends StatelessWidget {
  // 初始化
  const _GridTitleText(this.text);

  final String text;

  
  Widget build(BuildContext context) {
    return FittedBox(
      // 适应大小
      fit: BoxFit.scaleDown,
      // 对齐方向:
      // bottomCenter bottomEnd bottomStart 底部居中,底部结尾,底部开始
      // center centerEnd centerStart
      // topCenter topEnd topStart
      alignment: AlignmentDirectional.centerStart,
      child: Text(text),
    );
  }
}


class _GridDemoPhotoItem extends StatelessWidget {
  const _GridDemoPhotoItem({
    required this.photo,
    required this.tileStyle,
  });

  final _Photo photo;
  final GridListDemoType tileStyle;

  
  Widget build(BuildContext context) {
    final Widget image = Semantics(
      label: '${photo.title} ${photo.subtitle}',
      child: Material(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
        clipBehavior: Clip.antiAlias,
        child: Image.asset(
          photo.assetName,
          // package: 'flutter_gallery_assets',
          fit: BoxFit.cover,
        ),
      ),
    );

    switch (tileStyle) {
      case GridListDemoType.imageOnly:
        return image;
      case GridListDemoType.header:
        return GridTile(
          header: Material(
            color: Colors.transparent,
            shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
            ),
            clipBehavior: Clip.antiAlias,
            child: GridTileBar(
              title: _GridTitleText(photo.title),
              backgroundColor: Colors.black45,
            ),
          ),
          child: image,
        );
      case GridListDemoType.footer:
        return GridTile(
          footer: Material(
            color: Colors.transparent,
            shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.vertical(bottom: Radius.circular(4)),
            ),
            clipBehavior: Clip.antiAlias,
            child: GridTileBar(
              backgroundColor: Colors.black45,
              title: _GridTitleText(photo.title),
              subtitle: _GridTitleText(photo.subtitle),
            ),
          ),
          child: image,
        );
    }
  }
}

1.5 ListView

有点像被定义好的下拉列表,好用的。一般常与 Divider 分割线联用。

布局那篇博客涉及了此部分内容,这里不重复,放一个例子:

在这里插入图片描述

代码如下

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.light,
        // primaryColor: Colors.yellowAccent,
      ),
      home: ColumnListView(),
    );
  }
}

class ColumnListView extends StatelessWidget {
  const ColumnListView({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final List<String> entries = <String>['A', 'B', 'C', 'D', 'E', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'];
    final List<int> colorCodes = <int>[600, 500, 400, 300, 200, 100];
    return Scaffold(
      body: Column(
          children: [
            Column(
              children: [
                Text("1111111111111111111111111111111111111111111111111111111111111111"),
                Text("222222"),
              ],
            ),

            ListView.separated(
                // 这个参数绝对不能省略
              shrinkWrap: true,
              padding: const EdgeInsets.all(8),
                // 自带的迭代器,可以实现循环类似的功能
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  height: 50,
                  color: Colors.amber[colorCodes[index]],
                  child: Center(child: Text('Entry ${entries[index]}')),
                );
              },
                // 分割符号
              separatorBuilder: (BuildContext context, int index) => const Divider(),
                // 提前告诉ListView下有几个items
              itemCount: entries.length
            ),
          ]
      ),
    );
  }
}

1.6 Stack

堆叠。顾名思义,就是把几个控件放到一起,有点像PS里各个图层叠到一起,因此有些妙用。

最经典的案例就是头像,在图片上套一个圆形的边框就好了:

在这里插入图片描述

几个注意点:

  • Stack 是定死的,不能像之前 ListView, Container 那样可以在内容数据溢出时可滚动。
  • 可以剪切掉超出渲染框的子项,所以才有上面圆形头像的操作。

简单实现一个:

在这里插入图片描述

import 'package:flutter/material.dart';


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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        // body: GridGallery(type: GridListDemoType.header),
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildStack();
  }
}

// 头像
Widget _buildStack() {
  return Center(
    child: Stack(
      alignment: const Alignment(0.6, 0.6),
      children: [
        const CircleAvatar(
          backgroundImage: AssetImage('assets/images/2.jpg'),
          radius: 100,
        ),
        Container(
          decoration: const BoxDecoration(
            color: Colors.black45,
          ),
          child: const Text(
            'Olsen',
            style: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
        ),
      ],
    )
  );
}

2. 进阶widget (Material widget)

2.1 Card

有一说一这玩意还是被好用的。

注意点:

  • 默认情况下 Card 是无限小的。
  • Card 可识别为单个包含的单元。
  • Card 可以独立存在,而不依赖于周围的元素作为上下文。
  • 一个 Card 不能与另一个 Card 合并,也不能分成多个 Card 。

这边尝试探索下 Card 的几个参数,做一个列表,因为可能会在之后的大作业中用到:

在这里插入图片描述

import 'package:flutter/material.dart';


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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        // body: GridGallery(type: GridListDemoType.header),
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildCard2();
  }
}




// Card
Widget _buildCard2() {
  return Center(
    child: MaterialApp(
      theme: ThemeData(
          colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
      home: Scaffold(
        appBar: AppBar(title: const Text('Card Examples')),
        body: Column(
          children: const <Widget>[
            Spacer(),
            ElevatedCardExample(),
            FilledCardExample(),
            OutlinedCardExample(),
            MyCardExample(),
            Spacer(),
          ],
        ),
      ),
    ),
  );
}

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

  
  Widget build(BuildContext context) {
    return const Center(
      child: Card(
        // Card 没大小,要靠里面的东西撑起来
        child: SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Elevated Card')),
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        // 阴影
        elevation: 1,
        color: Theme.of(context).colorScheme.surfaceVariant,
        child: const SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Filled Card')),
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        elevation: 0,
        // 边框
        shape: RoundedRectangleBorder(
          side: BorderSide(
            color: Theme.of(context).colorScheme.outline,
          ),
          // 圆角
          borderRadius: const BorderRadius.all(Radius.circular(12)),
        ),
        child: const SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Outlined Card')),
        ),
      ),
    );
  }
}

class MyCardExample extends StatelessWidget {
  const MyCardExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        child: SizedBox(
          height: 500,
          width: 300,
          child: Column(
            children: <Widget>[
              Padding(
                padding: EdgeInsets.fromLTRB(5, 5, 5, 0),
                child: Container(
                    width: 280,
                    height: 190,
                    // child: Image.asset("assets/images/3.jpg"),
                    decoration: BoxDecoration(
                        image: DecorationImage(image: AssetImage('assets/images/3.jpg')),
                        borderRadius:const BorderRadius.all(Radius.circular(10))
                    )
                ),

              ),
              Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Padding(
                      padding: EdgeInsets.fromLTRB(20, 20, 6, 0),
                      child: Text("Headline", style: TextStyle(fontSize: Theme.of(context).textTheme.headline4!.fontSize))
                    ),
                    Padding(
                      padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
                      child: Text("SubHead", style: TextStyle(fontSize: Theme.of(context).textTheme.headline6!.fontSize))
                    ),
                    Padding(
                      padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
                      child: Text("Supporting text Supporting text Supporting text Supporting text Supporting text", style: TextStyle(fontSize: Theme.of(context).textTheme.bodyText1!.fontSize))
                    ),
                    SizedBox(
                      height: 30,
                    ),
                    ButtonBar(
                        children: <Widget>[
                          FloatingActionButton.extended(
                            onPressed: () {},
                            backgroundColor: Colors.purple.shade100,
                            icon: const Icon(Icons.save),
                            label: const Text("Buy"),
                          ),
                          FloatingActionButton.extended(
                            onPressed: () {},
                            backgroundColor: Colors.purple.shade100,
                            icon: const Icon(Icons.save),
                            label: const Text("Save"),
                          )
                        ]
                    )
                  ],
                ),
            ],
          ),
        )

      ),
    );
  }
}

实例:鸡哥商品

图片 + 介绍卡片,也不错,可能会在大作业中用到,商品介绍之类的里面可能会有用,加个图片轮播就变成淘宝复现了。

在这里插入图片描述

import 'package:flutter/material.dart';
// 图片轮播的包
import 'package:flutter_swiper/flutter_swiper.dart';



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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        // body: GridGallery(type: GridListDemoType.header),
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildCard2();
  }
}


// Card
Widget _buildCard2() {
  return Center(
    child: MaterialApp(
      theme: ThemeData(
          colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
      home: Scaffold(
        appBar: AppBar(title: const Text('Card Examples')),
        body: Column(
          children: <Widget>[
            Spacer(),
            ElevatedCardExample(),
            FilledCardExample(),
            OutlinedCardExample(),
            MyCardExample(),
            Spacer(),
          ],
        ),
      ),
    ),
  );
}

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

  
  Widget build(BuildContext context) {
    return const Center(
      child: Card(
        // Card 没大小,要靠里面的东西撑起来
        child: SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Elevated Card')),
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        // 阴影
        elevation: 1,
        color: Theme.of(context).colorScheme.surfaceVariant,
        child: const SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Filled Card')),
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        elevation: 0,
        // 边框
        shape: RoundedRectangleBorder(
          side: BorderSide(
            color: Theme.of(context).colorScheme.outline,
          ),
          // 圆角
          borderRadius: const BorderRadius.all(Radius.circular(12)),
        ),
        child: const SizedBox(
          width: 300,
          height: 75,
          child: Center(child: Text('Outlined Card')),
        ),
      ),
    );
  }
}

class MyCardExample extends StatelessWidget {
  // const MyCardExample({Key? key}) : super(key: key);

  List<Map> imageList = [
    {
      "asset":"assets/images/8.jpg"
    },
    {
      "asset":"assets/images/2.jpg"
    },
    {
      "asset":"assets/images/3.jpg"
    },
    {
      "asset":"assets/images/4.jpg"
    }
  ];

  
  Widget build(BuildContext context) {
    return Center(
      child: Card(
        shape: RoundedRectangleBorder(//设置圆角
          borderRadius: BorderRadius.circular(5),
        ),
        child: SizedBox(
          height: 500,
          width: 300,
          child: Column(
            children: <Widget>[
              Padding(
                padding: EdgeInsets.fromLTRB(5, 5, 5, 0),
                child: Container(
                    width: 300,
                    height: 190,
                    // child: Image.asset("assets/images/3.jpg"),

                    child: Container(
                      decoration: const BoxDecoration(
                        color: Colors.deepPurple,
                        borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(5), topRight: Radius.circular(5)),
                      ),
                      child: Swiper(
                        itemBuilder: (BuildContext context, int index){
                          return Image.asset(imageList[index]["asset"], fit: BoxFit.fill,);
                        },
                        itemCount: imageList.length,
                        pagination: SwiperPagination(),
                        control: SwiperControl(),
                        loop: true,
                        autoplay: true,
                      ),
                    ),
                    // decoration: BoxDecoration(
                    //     image: DecorationImage(image: AssetImage('assets/images/3.jpg')),
                    //     borderRadius:const BorderRadius.all(Radius.circular(10))
                    // )
                ),

              ),
              Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Padding(
                      padding: EdgeInsets.fromLTRB(0, 20, 6, 0),
                      child: ListTile(
                        leading: CircleAvatar(
                          backgroundImage: AssetImage("assets/images/ji.jpg"),
                        ),
                        title: Text("只因你们太美", style: TextStyle(fontSize: Theme.of(context).textTheme.headline4!.fontSize)),
                      )
                    ),
                    Padding(
                      padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
                      child: Text("就会爆炸", style: TextStyle(fontSize: Theme.of(context).textTheme.headline6!.fontSize))
                    ),
                    Padding(
                      padding: EdgeInsets.fromLTRB(20, 10, 6, 0),
                      child: Text("只因你实在是太美 baby 只因你太美 baby 迎面走来的你让我如此蠢蠢欲动 这种感觉我从未有", style: TextStyle(fontSize: Theme.of(context).textTheme.bodyText1!.fontSize))
                    ),
                    // SizedBox(
                    //   height: 30,
                    // ),
                    Align(
                      alignment: Alignment.bottomRight,
                      child:
                      ButtonBar(
                          children: <Widget>[
                            FloatingActionButton.extended(
                              onPressed: () {},
                              backgroundColor: Colors.purple.shade100,
                              icon: const Icon(Icons.save),
                              label: const Text("Buy"),
                            ),
                            FloatingActionButton.extended(
                              onPressed: () {},
                              backgroundColor: Colors.purple.shade100,
                              icon: const Icon(Icons.save),
                              label: const Text("Save"),
                            )
                          ]
                      )
                    )

                  ],
                ),
            ],
          ),
        )

      ),
    );
  }
}

2.2 ListTile

这个已经被多次使用了,最经典的案例就是个人名片,因为这个自带 heading 放头像, title, subtitle, text 这种可以放不同层级内容的部分。

在这里插入图片描述

代码如下

import 'package:flutter/material.dart';


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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Love Story',
      theme: ThemeData(
        brightness: Brightness.dark,
        // primaryColor: Colors.yellowAccent,
      ),
      home: const Scaffold(
        // body: GridGallery(type: GridListDemoType.header),
        body: layout(),
      )
    );
  }
}

class layout extends StatelessWidget {
  const layout({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return _buildCard();
  }
}


// Card
Widget _buildCard() {
  return Center(
    child: SizedBox(
      height: 210,
      child: Card(
        child: Column(
          children: [
            ListTile(
              title: const Text(
                '1625 Main Street',
                style: TextStyle(fontWeight: FontWeight.w500),
              ),
              subtitle: const Text('My City, CA 99984'),
              leading: Icon(
                Icons.restaurant_menu,
                color: Colors.blue[500],
              ),
            ),
            const Divider(),
            ListTile(
              title: const Text(
                '(408) 555-1212',
                style: TextStyle(fontWeight: FontWeight.w500),
              ),
              leading: Icon(
                Icons.contact_phone,
                color: Colors.blue[500],
              ),
            ),
            ListTile(
              title: const Text('costa@example.com'),
              leading: Icon(
                Icons.contact_mail,
                color: Colors.blue[500],
              ),
            ),
          ],
        ),
      ),
    )
  );
}


  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铖铖的花嫁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值