[译] MDC-104 Flutter:Material 高级组件(Flutter)

class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;

const Backdrop({
@required this.currentCategory,
@required this.frontLayer,
@required this.backLayer,
@required this.frontTitle,
@required this.backTitle,
}) : assert(currentCategory != null),
assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null);

@override
_BackdropState createState() => _BackdropState();
}

// TODO:添加 _FrontLayer 类(104)
// TODO:添加 _BackdropTitle 类(104)
// TODO:添加 _BackdropState 类(104)

导入 meta 包来添加 @required 标记。当构造函数中的属性没有默认值且不能为空的时候,用它来提醒你不能遗漏。注意,我们在构造方法后再一次声明了传入的值的确不是 null

在 Backdrop 类定义下添加 _BackdropState 类:

// TODO:添加 _BackdropState 类(104)
class _BackdropState extends State
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: ‘Backdrop’);

// TODO:添加 AnimationController 部件(104)

// TODO:为 _buildStack 添加 BuildContext 和 BoxConstraints 参数(104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: [
widget.backLayer,
widget.frontLayer,
],
);
}

@override
Widget build(BuildContext context) {
var appBar = AppBar(
brightness: Brightness.light,
elevation: 0.0,
titleSpacing: 0.0,
// TODO:用 IconButton 替换 leading 菜单图标(104)
// TODO:移除 leading 属性(104)
// TODO:使用 _BackdropTitle 参数创建标题(104)
leading: Icon(Icons.menu),
title: Text(‘SHRINE’),
actions: [
// TODO:添加从尾部图标到登陆页面的快捷方式(104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: ‘search’,
),
onPressed: () {
// TODO:打开登录(104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: ‘filter’,
),
onPressed: () {
// TODO:打开登录(104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO:返回一个 LayoutBuilder 部件(104)
body: _buildStack(),
);
}
}

build() 方法像 HomePage 一样返回一个带有 app bar 的 Scaffold。但是 Scaffold 的主体是一个 Stack。Stack 的孩子可以重叠。每个孩子的大小和位置都是相对于 Stack 的父级指定的。

现在在 ShrineApp 中添加一个 Backdrop 实例。

app.dart 中引入 backdrop.dartmodel/product.dart:

import ‘backdrop.dart’; // 新增代码
import ‘colors.dart’;
import ‘home.dart’;
import ‘login.dart’;
import ‘model/product.dart’; // 新增代码
import ‘supplemental/cut_corners_border.dart’;

app.dart 中修改 ShrineApp 的 build() 方法。将 home: 改成以 HomePage 为 frontLayer 的 Backdrop。

// TODO:将 home: 改为使用 HomePage frontLayer 的 Backdrop(104)
home: Backdrop(
// TODO:使 currentCategory 持有 _currentCategory (104)
currentCategory: Category.all,
// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(),
// TODO:将 backLayer 的值改为 CategoryMenuPage(104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text(‘SHRINE’),
backTitle: Text(‘MENU’),
),

如果你点击运行按钮,你将会看到主页与应用栏已经出现了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

backLayer 在 frontLayer 的主页后面插入了一个新的粉色背景。

你可以使用 Flutter Inspector 来验证在 Stack 里的主页后面确实有一个容器。就像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在你可以调整两个层的设计和内容。

5. 添加形状(Shape)

在本小节,你将为 frontLayer 设置样式以在其左上角添加一个切片。

Material Design 将此类定制称为形状。Material 表面可以具有任意形状。形状为表面增加了重点和风格,可用于表达品牌特点。普通的矩形形状可以定制使其具有弯曲或成角度的角和边缘,以及任意数量的边。它们可以是对称的或不规则的。

为 front layer 添加一个形状(Shape)

斜角 Shrine logo 激发了 Shrine 应用的形状故事。形状故事是应用程序中应用的形状的常见用法。例如,徽标形状在应用了形状的登录页面元素中回显。在本小节,您将在左上角使用倾斜切片做为前层设置样式。

backdrop.dart 中,添加新的 _FrontLayer 类:

// TODO:添加 _FrontLayer 类(104)
class _FrontLayer extends StatelessWidget {
// TODO:添加 on-tap 回调(104)
const _FrontLayer({
Key key,
this.child,
}) : super(key: key);

final Widget child;

@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// TODO:添加 GestureDetector(104)
Expanded(
child: child,
),
],
),
);
}
}

然后在 BackdropState 的 _buildStack() 方法里将 front layer 包裹在 _FrontLayer 内:

Widget _buildStack() {
// TODO:创建一个 RelativeRectTween 动画(104)

return Stack(
key: _backdropKey,
children: [
widget.backLayer,
// TODO:添加 PositionedTransition(104)
// TODO:在 _FrontLayer 中包裹 front layer(104)
_FrontLayer(child: widget.frontLayer),
],
);
}

重载。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们给 Shrine 的主表面定制了一个形状。由于表面具有高度,用户可以看到白色前层后面有东西。让我们添加一个动作,以便用户可以看到背景的背景层。

6. 添加动作(Motion)

动作是一种可以让你的应用变得更真实的方式。它可以是大且夸张的、小且微妙的,亦或是介于两者之间的。但需要注意的是动作的形式一定要适合使用场景。多次重复的有规律的动作要精细小巧,才不会分散用户的注意力或占用太多时间。适当的情况,如用户第一次打开应用时,长时的动作可能会更引人注目,一些动画也可以帮助用户了解如何使用您的应用程序。

为菜单按钮添加显示动作

backdrop.dart 的顶部,其他类函数外,添加一个常量来表示我们需要的动画执行的速度:

// TODO:添加速度常数(104)
const double _kFlingVelocity = 2.0;

_BackdropState 中添加 AnimationController 部件,在 initState() 函数中实例化它,并将其部署在 state 的 dispose() 函数中:

// TODO:添加 AnimationController 部件(104)
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}

// TODO:重写 didUpdateWidget(104)

@override
void dispose() {
_controller.dispose();
super.dispose();
}

// TODO:添加函数以确定并改变 front layer 可见性(104)

部件生命周期

仅在部件成为其渲染树的一部分之前会调用一次 initState() 方法。只有在部件从树中移除时才会调用一次 dispose() 方法。

AnimationController 用来配合 Animation,并提供播放、反向和停止动画的 API。现在我们需要使用某个方法来移动它。

添加函数以确定并改变 front layer 的可见性:

// TODO:添加函数以确定并改变 front layer 的可见性(104)
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}

void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}

将 backLayer 包裹在 ExcludeSemantics 部件中。当 back layer 不可见时,此部件将从语义树中剔除 backLayer 的菜单项。

return Stack(
key: _backdropKey,
children: [
// TODO:将 backLayer 包裹在 ExcludeSemantics 部件中(104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),

修改 _buildStack() 方法使其持有一个 BuildContext 和 BoxConstraints。同时包含一个使用 RelativeRectTween 动画的 PositionedTransition:

// TODO:为 _buildStack 添加 BuildContext 和 BoxConstraints 参数(104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;

// TODO:创建一个 RelativeRectTween 动画(104)
Animation layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);

return Stack(
key: _backdropKey,
children: [
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO:添加一个 PositionedTransition(104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO:在 _BackdropState 上实现 onTap 属性(104)
child: widget.frontLayer,
),
),
],
);
}

最后,返回一个使用 _buildStack 作为其 builder 的 LayoutBuilder 部件,而不是为 Scaffold 的主体调用 _buildStack 函数:

return Scaffold(
appBar: appBar,
// TODO:返回一个 LayoutBuilder 部件(104)
body: LayoutBuilder(builder: _buildStack),
);

我们使用 LayoutBuilder 将 front/back 堆栈的构建延迟到布局阶段,以便我们可以合并背景的实际整体高度。LayoutBuilder 是一个特殊的部件,其构建器回调提供了大小约束。

LayoutBuilder

部件树通过遍历叶结点来组织布局。约束在树下传递,但是在叶结点根据约束返回其大小之前通常不会计算大小。叶子点无法知道它的父母的大小,因为它尚未计算。

当部件必须知道其父部件的大小以便自行布局(且父部件大小不依赖于子部件)时,LayoutBuilder 就派上用场了。它使用一个方法来返回部件。

了解有关更多信息,请查看 LayoutBuilder 类文档。

build() 方法中,将应用栏中的前导菜单图标转换为 IconButton,并在点击按钮时使用它来切换 front layer 的可见性。

// TODO:用 IconButton 替换 leading 菜单图标(104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),

在模拟器中重载并点击菜单按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

front layer 在向下移动(滑动)。但如果向下看,则会出现红色错误和溢出错误。这是因为 AsymmetricView 被这个动画挤压并变小,反过来使得 Column 的空间更小。最终,Column 不能用给定的空间自行排列并导致错误。如果我们用 ListView 替换 Column,则移动时列的尺寸仍然保持不变。

在 ListView 中包裹产品列项

supplemental/product_columns.dart 中,将 OneProductCardColumn 的 Column 替换成 ListView:

class OneProductCardColumn extends StatelessWidget {
OneProductCardColumn({this.product});

final Product product;

@override
Widget build(BuildContext context) {
// TODO:用 ListView 替换 Column(104)
return ListView(
reverse: true,
children: [
SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}

Column 包含 MainAxisAlignment.end。要使得从底部开始布局,使用 reverse: true。其孩子的顺序将翻转以弥补变化。

重载并点击菜单按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

OneProductCardColumn 上的灰色溢出警告消失了!现在让我们修复另一个问题。

supplemental/product_columns.dart 中修改 imageAspectRatio 的计算方式,并将 TwoProductCardColumn 中的 Column 替换成 ListView:

// TODO:修改 imageAspectRatio 的计算方式(104)
double imageAspectRatio =
(heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages)
? constraints.biggest.width / heightOfImages
: 33 / 49;

// TODO:用 ListView 替换 Column(104)
return ListView(
children: [
Padding(
padding: EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top,
)
: SizedBox(
height: heightOfCards,
),
),
SizedBox(height: spacerHeight),
Padding(
padding: EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
});

我们还为 imageAspectRatio 添加了一些安全性。

重载。然后点击菜单按钮。

现在已经没有溢出了。

7. 在 back layer 上添加菜单

菜单是由可点击文本项组成的列表,当发生点击事件时通知监听器。在此小节,你将添加一个类别过滤菜单。

添加菜单

在 front layer 添加菜单并在 back layer 添加互动按钮。

创建名为 lib/category_menu_page.dart 的新文件:

import ‘package:flutter/material.dart’;
import ‘package:meta/meta.dart’;

import ‘colors.dart’;
import ‘model/product.dart’;

class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged onCategoryTap;
final List _categories = Category.values;

const CategoryMenuPage({
Key key,
@required this.currentCategory,
@required this.onCategoryTap,
}) : assert(currentCategory != null),
assert(onCategoryTap != null);

Widget _buildCategory(Category category, BuildContext context) {
final categoryString =
category.toString().replaceAll(‘Category.’, ‘’).toUpperCase();
final ThemeData theme = Theme.of(context);

return GestureDetector(
onTap: () => onCategoryTap(category),
child: category == currentCategory
? Column(
children: [
SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.body2,
textAlign: TextAlign.center,
),
SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.body2.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}

@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}

它是一个 GestureDetector,它包含一个 Column,其孩子是类别名称。下划线用于指示所选的类别。

app.dart 中,将 ShrineApp 部件从 stateless 转换成 stateful。

  1. 高亮 ShrineApp.
  2. 按 alt(option)+ enter
  3. 选择 “Convert to StatefulWidget”。
  4. 将 ShrineAppState 类更改为 private(_ShrineAppState)。要从 IDE 主菜单执行此操作,请选择 Refactor > Rename。或者在代码中,您可以高亮显示类名 ShrineAppState,然后右键单击并选择 Refactor > Rename。输入 _ShrineAppState 以使该类成为私有。

app.dart 中,为选择的类别添加一个变量 _ShrineAppState,并在点击时添加一个回调:

// TODO:将 ShrineApp 转换成 stateful 部件(104)
class _ShrineAppState extends State {
Category _currentCategory = Category.all;

void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}

然后将 back layer 修改为 CategoryMenuPage。

app.dart 中引入 CategoryMenuPage:

import ‘backdrop.dart’;
import ‘colors.dart’;
import ‘home.dart’;
import ‘login.dart’;
import ‘category_menu_page.dart’;
import ‘model/product.dart’;
import ‘supplemental/cut_corners_border.dart’;

build() 方法,将 backlayer 字段修改成 CategoryMenuPage 并让 currentCategory 字段持有实例变量。

home: Backdrop(
// TODO:让 currentCategory 字段持有 _currentCategory(104)
currentCategory: _currentCategory,
// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(),
// TODO:将 backLayer 修改成 CategoryMenuPage(104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: Text(‘SHRINE’),
backTitle: Text(‘MENU’),
),

重载并点击菜单按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你点击了菜单选项,然而什么也没有发生…让我们修复它。

home.dart 中,为 Category 添加一个变量并将其传递给 AsymmetricView。

import ‘package:flutter/material.dart’;

import ‘model/products_repository.dart’;
import ‘model/product.dart’;
import ‘supplemental/asymmetric_view.dart’;

class HomePage extends StatelessWidget {
// TODO:为 Category 添加一个变量(104)
final Category category;

const HomePage({this.category: Category.all});

@override
Widget build(BuildContext context) {
// TODO:为 Category 添加一个变量并将其传递给 AsymmetricView(104)
return AsymmetricView(products: ProductsRepository.loadProducts(category));
}
}

app.dart 中为 frontLayer 传递 _currentCategory

// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(category: _currentCategory),

重载。点击模拟器中的菜单按钮并选择一个类别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击菜单图标以查看产品。他们被过滤了!

选择菜单项后关闭 front layer

backdrop.dart 中,为 BackdropState 重写 didUpdateWidget() 方法:

// TODO:为 didUpdateWidget() 添加重写方法(104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);

if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}

热重载,然后点击菜单图标并选择一个类别。菜单应该自动关闭,然后你将看到所选择类别的物品。现在同样地将这个功能添加到 front layer 。

切换 front layer

backdrop.dart 中,给 backdrop layer 添加一个 on-tap 回调:

class _FrontLayer extends StatelessWidget {
// TODO:添加 on-tap 回调(104)
const _FrontLayer({
Key key,
this.onTap, // 新增代码
this.child,
}) : super(key: key);

final VoidCallback onTap; // 新增代码
final Widget child;

然后将一个 GestureDetector 添加到 _FrontLayer 的孩子 Column 的子节点中:

child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// TODO:添加一个 GestureDetector(104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),

然后在 _buildStack() 方法的 _BackdropState 中实现新的 onTap 属性:

PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO:在 _BackdropState 中实现 onTap 属性(104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),

重载并点击 front layer 的顶部。每次你点击 front layer 顶部时都它应该打开或者关闭。

8. 添加品牌图标

品牌肖像也应该延伸到熟悉的图标。让我们自定义显示图标并将其与我们的标题合并,以获得独特的品牌外观。

修改菜单按钮图标

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

backdrop.dart 中,新建 _BackdropTitle 类。

// TODO:添加 _BackdropTitle 类(104)
class _BackdropTitle extends AnimatedWidget {
final Function onPress;
final Widget frontTitle;
final Widget backTitle;

const _BackdropTitle({
Key key,
Listenable listenable,
this.onPress,
@required this.frontTitle,
@required this.backTitle,
}) : assert(frontTitle != null),
assert(backTitle != null),
super(key: key, listenable: listenable);

@override
Widget build(BuildContext context) {
final Animation animation = this.listenable;

return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: [
// 品牌图标
SizedBox(
width: 72.0,
child: IconButton(
padding: EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: [
Opacity(
opacity: animation.value,
child: ImageIcon(AssetImage(‘assets/slanted_menu.png’)),
),
FractionalTranslation(
translation: Tween(
begin: Offset.zero,
end: Offset(1.0, 0.0),
).evaluate(animation),
child: ImageIcon(AssetImage(‘assets/diamond.png’)),
)]),
),
),
// 在这里,我们在 backTitle 和 frontTitle 之间是实现自定义的交叉淡入淡出效果
// 这使得两个文本之间能够平滑过渡。
Stack(
children: [
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),

如何成为Android高级架构师!

架构师必须具备抽象思维和分析的能力,这是你进行系统分析和系统分解的基本素质。只有具备这样的能力,架构师才能看清系统的整体,掌控全局,这也是架构师大局观的形成基础。 你如何具备这种能力呢?一是来自于经验,二是来自于学习。

架构师不仅要具备在问题领域上的经验,也需要具备在软件工程领域内的经验。也就是说,架构师必须能够准确得理解需求,然后用软件工程的思想,把需求转化和分解成可用计算机语言实现的程度。经验的积累是需要一个时间过程的,这个过程谁也帮不了你,是需要你去经历的。

但是,如果你有意识地去培养,不断吸取前人的经验的话,还是可以缩短这个周期的。这也是我整理架构师进阶此系列的始动力之一。


成为Android架构师必备知识技能

对应导图的学习笔记(由阿里P8大牛手写,我负责整理成PDF笔记)

部分内容展示

《设计思想解读开源框架》

  • 目录
  • 热修复设计
  • 插件化框架设计

    《360°全方面性能优化》
  • 设计思想与代码质量优化
  • 程序性能优化

    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
    工程的思想,把需求转化和分解成可用计算机语言实现的程度。经验的积累是需要一个时间过程的,这个过程谁也帮不了你,是需要你去经历的。

但是,如果你有意识地去培养,不断吸取前人的经验的话,还是可以缩短这个周期的。这也是我整理架构师进阶此系列的始动力之一。


成为Android架构师必备知识技能

[外链图片转存中…(img-XxuLaC9a-1715883640563)]

对应导图的学习笔记(由阿里P8大牛手写,我负责整理成PDF笔记)

[外链图片转存中…(img-ZvyZlnFS-1715883640564)]

部分内容展示

《设计思想解读开源框架》

  • 目录
    [外链图片转存中…(img-eHjjWr9d-1715883640564)]
  • 热修复设计
    [外链图片转存中…(img-yL86DmQ5-1715883640565)]
  • 插件化框架设计
    [外链图片转存中…(img-cgoy9Wc4-1715883640566)]
    《360°全方面性能优化》
    [外链图片转存中…(img-oKKD8qVz-1715883640567)]
  • 设计思想与代码质量优化
    [外链图片转存中…(img-oBMSfuh5-1715883640567)]
  • 程序性能优化
    [外链图片转存中…(img-Xi0CH4PR-1715883640568)]
    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值