入門介紹完,今天我們,先來分析幾個官方提供的示例。
以下程式碼來源於 flutter_gallery中的pesto_demo示例
1. PESTO菜譜
0.需求分析
分析layout
- 有頂部的
appBar
和floatingActionButton
。 - 下面的列表是由
CardView
組成的listView
分析動畫
- 頭部的
Toolbar
是可以伸縮的頭部,並且帶有動畫(重點和難點) - 轉場動畫
分析事件
- 點選搜尋和
floatingActionButton
彈出SnackBar
- 儲存選單的喜歡的狀態
1.動手
初始化
- 資料結構和假資料
//0.定義好資料結構
//從圖中可以看到,列表頁得需要得是下面幾個欄位
class Recipe {
const Recipe(
{this.name,
this.author,
this.ingredientsImagePath,
this.description,
this.imagePath,
this.ingredients,
this.steps});
final String name;
final String author;
final String description;
final String ingredientsImagePath;
final String imagePath;
//這兩個欄位是詳情頁需要得
final List<RecipeIngredient> ingredients;
final List<RecipeStep> steps;
}
//詳情頁需要得
class RecipeIngredient {
const RecipeIngredient({this.amount, this.description});
final String amount;
final String description;
}
class RecipeStep {
const RecipeStep({this.duration, this.description});
final String duration;
final String description;
}
複製程式碼
- Theme 整體的主題風格是亮色系,顏色是綠色,accentColor是紅色
//0.寫好主題
final PestoHomeThemeData = ThemeData(
brightness: Brightness.light,
primaryColor: Colors.teal,
accentColor: Colors.redAccent);
複製程式碼
- 快取喜歡的結果
//還需要一個儲存是否喜歡得欄位
final Set<Recipe> _favoriteRecipes = new Set<Recipe>();
複製程式碼
- Scaffold
- 因為頂部的
appBar
和floatingActionButton
,所以要最完成需要使用Scaffold
。而且 而它的body
和appBar
,一個是需要填充資料的列表,一個是需要動畫的appBar
。所以是一個StatefulWidget
。 而整體是一個頁面,因為要不斷傳遞我們的_favoriteRecipes
,所以又封裝了一層StatelessWidget
.
靜態部分
我們暫時先不管滾動的部分。
- 將原來的Theme的platform保留
class PestoHome extends StatelessWidget {
@override
Widget build(BuildContext context) =>
//傳遞_favoriteRecipes給它
RecipeGridPage(recipes: _favoriteRecipes.toList());
}
class RecipeGridPage extends StatefulWidget {
final List<Recipe> recipes;
RecipeGridPage({Key key, @required this.recipes}) : super(key: key);
@override
State<StatefulWidget> createState() => _RecipeGridPageState();
}
class _RecipeGridPageState extends State<RecipeGridPage> {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
//因為需要floatingActionButton,所以需要Scaffold
return Theme(
//將context中的platform資訊保留
data: _pTheme.copyWith(platform: Theme.of(context).platform),
child: Scaffold(
key: scaffoldKey,
floatingActionButton: FloatingActionButton(
onPressed: null),
body: null,
));
}
}
複製程式碼
FloatingActionButton
先把FloatingActionButton 完成。就是簡單的彈出SnackBar的功能。
彈出SnackBar,需要Scaffold的BuildContext。通過之前的學習,我們知道有3個可以得到的方式(Scaffold
的GlobalKey
,builder
方法得到正確的BuildContext
,或者直接寫成子元件)。
- 這裡採用的是
GlobalKey
的方式 這種方式最簡單了。使用GlobalKey
的方式,其他要彈的,都可以快速拿到state
。
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
//直接使用scaffoldKey.currentState彈出
scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('Not supported.')));
}),
複製程式碼
Appbar(暫時)
- 程式碼 先新增一個暫時的AppBar,滑動動畫的部分,我們會後面處理
class _RecipeGridPageState extends State<RecipeGridPage> {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
List<Recipe> items = kPestoRecipes;
@override
Widget build(BuildContext context) {
print('items.length=${items.length}');
//因為需要floatingActionButton,所以需要Scaffold
return Theme(
//將context中的platform資訊保留
data: _pTheme.copyWith(platform: Theme.of(context).platform),
child: Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: Text('靜態頁面'),
actions: <Widget>[
GestureDetector(
onTap: () {
scaffoldKey.currentState.showSnackBar(
SnackBar(content: Text('Not supported.')));
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.search),
),
)
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
//直接使用scaffoldKey.currentState彈出
scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('Not supported.')));
}),
body: null)
);
}
複製程式碼
- 結果
圖中分別的操作是,第一次點選右上角。第二次點選右下角按鈕。都彈出了
SnackBar
。和預期一樣。
body部分
- recipe card
寫好字型的樣式
class PestoStyle extends TextStyle {
const PestoStyle({
double fontSize: 12.0,
FontWeight fontWeight,
Color color: Colors.black87,
double letterSpacing,
double height,
}) : super(
inherit: false,
color: color,
fontFamily: 'Raleway',
fontSize: fontSize,
fontWeight: fontWeight,
textBaseline: TextBaseline.alphabetic,
letterSpacing: letterSpacing,
height: height,
);
}
final TextStyle titleStyle = const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
final TextStyle authorStyle = const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);
複製程式碼
確定好整體的佈局
- 如上圖分析,大體的佈局就是這樣。
- 因為是MD中
Card
的樣式,所以需要在最外層包裹一層Card
- 同時,圖中未標註的是,
padding
的部分。在Flutter
中,要實現padding,只要在它包裹在外面一層佈局下就可以了。
封裝成Card元件
- 封裝元件
class RecipeCard extends StatelessWidget {
final TextStyle titleStyle =
const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
final TextStyle authorStyle =
const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);
RecipeCard({Key key, @required this.recipe}) : super(key: key);
final Recipe recipe;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset(
recipe.imagePath,
fit: BoxFit.contain,
),
Row(
children: <Widget>[
new Padding(
padding: const EdgeInsets.all(16.0),
child: new Image.asset(
recipe.ingredientsImagePath,
width: 48.0,
height: 48.0,
),
),
new Expanded(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(recipe.name,
style: titleStyle,
softWrap: false,
overflow: TextOverflow.ellipsis),
new Text(recipe.author, style: authorStyle),
],
),
),
],
)
],
),
);
}
}
複製程式碼
然後我們先修改程式碼,先預覽一下這個RecipeCard
是否滿足我們的需求。
//修改_RecipeGridPageState build方法
class _RecipeGridPageState extends State<RecipeGridPage> {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
List<Recipe> items = kPestoRecipes;
@override
Widget build(BuildContext context) {
print('items.length=${items.length}');
//因為需要floatingActionButton,所以需要Scaffold
return Theme(
//省去不需要修改的部分...
//ListView相當於Android中的RecycleView
body: ListView.builder(
//顯示的數量,就是item的數量
itemCount: items.length,
itemBuilder: (context, index) {
//將我們封裝好的提供出去
return RecipeCard(recipe: items[index]);
})));
}
}
複製程式碼
-
效果預覽
-
新增
onTap
事件監聽 確實達到了我們的效果。 我們還預期點選item,跳轉到詳情頁。那我們給RecipeCard新增手勢,並將點選事件傳入。
//省略不修改的程式碼
class RecipeCard extends StatelessWidget {
//新增點選事件的回撥
RecipeCard({Key key, @required this.recipe,this.onTap}) : super(key: key);
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
//使用GestureDetector來包裹,獲取事件
return GestureDetector(
onTap: onTap,
child: Card(//省略重複程式碼
),
);
}
}
class _RecipeGridPageState extends State<RecipeGridPage> {
@override
Widget build(BuildContext context) {
return Theme(
//將context中的platform資訊保留
data: _pTheme.copyWith(platform: Theme.of(context).platform),
child: Scaffold(
key: scaffoldKey,
floatingActionButton: FloatingActionButton(
//省略...
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return RecipeCard(
recipe: items[index],
//傳入點選事件
onTap: () {
showRecipePage(context, items[index]);
},
);
})));
}
//需要顯示我們的商品詳情頁
void showRecipePage(BuildContext context, Recipe item) {
}
}
複製程式碼
商品詳情頁
同樣,我們也只先實現下面的部分。- 分析
- 我們發現,介面是由兩個重疊的元素形成的。
一個是下面的列表,另一個是疊在上面的
floattingButton
。 RecipeSheet
離頂部有一般floattingButton
高度的距離。floattingButton
距離右邊有一定距離。
這樣我們就使用Stack
和Position
來完成我們的定位。
- 程式碼
class _RecipePageState extends State<RecipePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
return new Scaffold(
key: _scaffoldKey,
body: new Stack(
children: <Widget>[
new ListView(
children: <Widget>[
new Stack(
children: <Widget>[
new Container(
padding: const EdgeInsets.only(top: _kFabHalfSize),
child: new RecipeSheet(recipe: widget.recipe),
),
new Positioned(
right: 16.0,
child: new FloatingActionButton(
child: new Icon(
isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: _toggleFavorite,
),
),
],
)
],
)
],
),
);
}
void _toggleFavorite() {
setState(() {
if (_favoriteRecipes.contains(widget.recipe))
_favoriteRecipes.remove(widget.recipe);
else
_favoriteRecipes.add(widget.recipe);
});
}
}
class RecipeSheet extends StatelessWidget {
final TextStyle titleStyle = const PestoStyle(fontSize: 34.0);
final TextStyle descriptionStyle = const PestoStyle(
fontSize: 15.0, color: Colors.black54, height: 24.0 / 15.0);
final TextStyle itemStyle =
const PestoStyle(fontSize: 15.0, height: 24.0 / 15.0);
final TextStyle itemAmountStyle = new PestoStyle(
fontSize: 15.0, color: _pTheme.primaryColor, height: 24.0 / 15.0);
final TextStyle headingStyle = const PestoStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0 / 15.0);
RecipeSheet({Key key, this.recipe}) : super(key: key);
final Recipe recipe;
@override
Widget build(BuildContext context) {
return new Material(
child: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
child: new Table(
columnWidths: const <int, TableColumnWidth>{
0: const FixedColumnWidth(64.0)
},
children: <TableRow>[
new TableRow(children: <Widget>[
new TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: new Image.asset(recipe.ingredientsImagePath,
width: 32.0,
height: 32.0,
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown)),
new TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: new Text(recipe.name, style: titleStyle)),
]),
new TableRow(children: <Widget>[
const SizedBox(),
new Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
child: new Text(recipe.description, style: descriptionStyle)),
]),
new TableRow(children: <Widget>[
const SizedBox(),
new Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: new Text('Ingredients', style: headingStyle)),
]),
]
..addAll(recipe.ingredients.map((RecipeIngredient ingredient) {
return _buildItemRow(ingredient.amount, ingredient.description);
}))
..add(new TableRow(children: <Widget>[
const SizedBox(),
new Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: new Text('Steps', style: headingStyle)),
]))
..addAll(recipe.steps.map((RecipeStep step) {
return _buildItemRow(step.duration ?? '', step.description);
})),
),
),
);
}
TableRow _buildItemRow(String left, String right) {
return new TableRow(
children: <Widget>[
new Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: new Text(left, style: itemAmountStyle),
),
new Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: new Text(right, style: itemStyle),
),
],
);
}
}
複製程式碼
注意:
- 這裡需要注意的是
..
這種語法。這是dart
的語法。相當於呼叫後面的方法,然後返回本身
這樣的操作。 Table
TableRow
和TableCell
都是Flutter
中提供的表格控制元件。
- 效果圖
然後修改跳轉的程式碼
void showRecipePage(BuildContext context, Recipe item) {
Navigator.push(context, new MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/recipe'),
builder: (BuildContext context) {
return new Theme(
data: _pTheme.copyWith(platform: Theme.of(context).platform),
child: new RecipePage(recipe: item),
);
},
));
}
複製程式碼
動態部分
理論認識
因為我們需要appBar進行滑動。所以需要使用CustomScrollView
。結合SliverAppBar
和SliverGrid
來進行整體的繪製。
CustomScrollView
- 使用它,可以結合
Sliver
來創造自定義的滾動效果。 比如說 做一個MD中常用的app bar 擴充套件的效果,就可以使用SliverAppBar
,SliverList
和SliverGrid
來完成。 - 會創造
RenderSliver
物件。 - 還可以通過
NotificationListener
來監聽滾動事件,或者通過ScrollController
來監聽和控制滾動事件。
很多經典的MD
的appBar部分動畫,都可以得到相應的實現。
觀察動畫
商品詳情頁
我們發現,商品詳情頁的動畫效果,有點像是MD內,appbar放一張圖片,然後完全滾動遮蓋的效果。 所以,我們先用這個效果來嘗試以下,實現效果
- 程式碼
class _RecipePageState extends State<RecipePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
double _getAppBarHeight(BuildContext context) =>
MediaQuery.of(context).size.height * 0.3;
@override
Widget build(BuildContext context) {
final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
final double appBarHeight = _getAppBarHeight(context);
return new Scaffold(
key: _scaffoldKey,
//將body直接改為CustomScrollView
body: CustomScrollView(
slivers: <Widget>[
//分別返回`SliverAppBar`和`SliverToBoxAdapter`
SliverAppBar(
expandedHeight: appBarHeight - _kFabHalfSize,
backgroundColor: Colors.transparent,
//這個是決定appBar有多大的和裡面放東西的控制元件
flexibleSpace: FlexibleSpaceBar(
//建立一個stack
background: Stack(
fit: StackFit.expand,
children: <Widget>[
//先放一層圖片在下面
Image.asset(
widget.recipe.imagePath,
fit: BoxFit.cover,
height: appBarHeight - _kFabHalfSize,
),
//再蓋一層漸變色
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0x60000000), Color(0x00000000)],
begin: Alignment(0.0, -1.0),
end: Alignment(0.0, -0.2)),
))
],
)),
),
//因為child接受的是Sliver,我們可以將Box的控制元件,使用SliverToBoxAdapter來包括,簡單的就可以顯示了
SliverToBoxAdapter(
child: new Stack(
children: <Widget>[
//這裡和原來一樣。同樣是要疊兩層。因為floattingActionBar是突出半個
Container(
width: _kRecipePageMaxWidth,
padding: const EdgeInsets.only(top: _kFabHalfSize),
child: new RecipeSheet(recipe: widget.recipe),
),
Positioned(
right: 16.0,
child: new FloatingActionButton(
child: new Icon(isFavorite
? Icons.favorite
: Icons.favorite_border),
onPressed: _toggleFavorite,
),
),
],
),
)
],
));
}
//省略相同部分...
}
複製程式碼
Scaffold
的body
下直接使用CustomScrollView
。SliverAppBar
中的flexibleSpace
來存放appBar內顯示的其他控制元件- 預設的
SliverAppBar
的pined
為false
,故他會跟著滾上去。 - 因為
CustomScrollView
的slivers
接受的是sliver
,我們可以將Box
的控制元件,使用SliverToBoxAdapter
來包括,簡單的就可以顯示了
- 執行效果
仔細看,有兩點效果還是不滿足我們預期的效果。
FloatingActionButton
,需要壓住一點上面的圖片。- 滾動時,我們不需要圖片進行透明度的漸變。
再次修改
既然這樣,我們就不能用自帶的來完成效果了。再次觀察預期的效果,發現,關鍵點:背後的圖片是不動的。 所以我們想,讓圖片整個放在背後,appBar只是一個透明的遮罩!
- 程式碼
class _RecipePageState extends State<RecipePage> {
//省略重複部分....
@override
Widget build(BuildContext context) {
//省略重複部分....
return new Scaffold(
key: _scaffoldKey,
//0.將body替換成一個Stack
body: Stack(
children: <Widget>[
//將圖片跌在最下一層。並放在頂部
Positioned(
child: Image.asset(
widget.recipe.imagePath,
fit: BoxFit.cover,
height: appBarHeight+ _kFabHalfSize,
),
top: 0.0,
left: 0.0,
right: 0.0,
),![33.gif](https://upload-images.jianshu.io/upload_images/1877190-6544b5dccd3bd690.gif?imageMogr2/auto-orient/strip)
//然後再疊放我們的ScrollView
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: appBarHeight - _kFabHalfSize,
backgroundColor: Colors.transparent,
pinned: false,
//flexibleSpace 的background只是一個遮罩
flexibleSpace: FlexibleSpaceBar(
background: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0x60000000), Color(0x00000000)],
begin: Alignment(0.0, -1.0),
end: Alignment(0.0, -0.2)),
))),//Stack,FlexibleSpaceBar
), //SliverAppBar
SliverToBoxAdapter(//... ) //SliverToBoxAdatper
],//<Widget>[]
),//CustomScrollView
], //<WIdget>[]
)); //stack,Scaffold
}
}
複製程式碼
- 執行效果
確認過眼神,就是我們要的效果。商品詳情頁的動畫完成~
首頁
同樣的,我們發現預設的效果並不滿足我們。我們這裡需要根據滾動的量去改變FlexibleSpaceBar內我們建立的logo和圖示的大小。
- 第一步,先改成經典的MD樣式
class _RecipeGridPageState extends State<RecipeGridPage> {
//省略程式碼...
@override
Widget build(BuildContext context) {
//得到狀態列高度
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Theme(
data: _pTheme.copyWith(platform: Theme.of(context).platform),
child: Scaffold(
key: scaffoldKey,
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
//直接使用scaffoldKey.currentState彈出
scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('Not supported.')));
}),
//將body改為CustomScrollView
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
//pinned為true ,這樣就不會隨著繼續往上滑動
pinned: true,
expandedHeight: _kAppBarHeight,
backgroundColor: Colors.teal,
//將原來放在appbar的action放在這裡
actions: <Widget>[
GestureDetector(
onTap: () {
scaffoldKey.currentState.showSnackBar(
SnackBar(content: Text('Not supported.')));
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.search),
),
)
],
//這裡新增。繪製出我們的圖示
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: new EdgeInsets.only(
top: statusBarHeight + 0.5 * 10.0,
bottom: 10.0,
),
child: Center(
//固定寬度的居中處理
child: SizedBox(
width: kLogoWidth,
//使用stack展示上下佈局。為什麼不用column?
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned.fromRect(
rect: Rect.fromLTWH(
0.0, 0.0, kLogoWidth, kLogoHeight/5*2),
child: new Image.asset(
'flutter_gallery_assets/pesto/logo_small.png',
fit: BoxFit.contain,
),
),
Positioned.fromRect(
rect: Rect.fromLTWH(
0.0, kLogoHeight/5*2, kLogoWidth, kTextHeight),
child: Center(
child: new Text('PESTO',
style: logoTitleStyle,
textAlign: TextAlign.center),
),
),
],
),
),
),
),
),
),
//下面是一個list.把原來的listView.builder改成這樣就可以了
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return RecipeCard(
recipe: items[index],
onTap: () {
showRecipePage(context, items[index]);
},
);
},
childCount: items.length,
)),
],
)));
}
//省略程式碼...
}
複製程式碼
- 效果1
確實不符合我們的效果,接下來需要動畫控制整個效果。
- 正在的技術 我們這裡的效果是根據appBar的大小,進行圖示的縮放,最後保留圖示,停留在那。
我們可以使用LayoutBuilder
這個類,來傳遞變化的父元件的約束。
LayoutBuilder
還記得我們入門的第二遍文章介紹過的Builder
嗎(可以正確傳入當前子控制元件的父元件的BuildContext
)?與其類似的,還存在
LayoutBuilder。它可以傳入父元件的大小,讓我們的自元件跟著他進行變化。
將FlexibleSpaceBar
修改成LayoutBuilder
就可以得到變化的Contraints了。
題外話:FlexibleSpaceBar
的實現方式和這種方式不同。這個我們後面再研究
- 程式碼
flexibleSpace: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
print('constraints=' + constraints.toString());
return Padding(//...與原來相同的程式碼
);
}
複製程式碼
- 效果2
看到這時候,之前
FlexibleSpaceBar
自帶的漸變效果就消失了, 還可以可以看到這個constraints
的高度在變化
這樣,我們就根據這樣的數值,來完成我們的動畫效果
- 程式碼
flexibleSpace: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
//這是AppBar的總高度
double biggestHeight = constraints.biggest.height;
//當前的AppBar的真實高度,去掉了狀態列
final double appBarHeight = biggestHeight-statusBarHeight;
//appBarHeight - kToolbarHeight 代表的是當前的擴充套件量,_kAppBarHeight - kToolbarHeight表示最大的擴充套件量
//t就是,變化的Scale
final double t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight);
// begin + (end - begin) * t; lerp函式可以快速取到根據當前的比例中間值
final double extraPadding = new Tween<double>(begin: 10.0, end: 24.0).lerp(t);
final double logoHeight = appBarHeight - 1.5 * extraPadding;
//字型的樣式沒有發生變化。
final TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0);
//字型所佔用的rect空間
final RectTween _textRectTween = new RectTween(
begin: new Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
end: new Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight)
);
//透明度變化的曲線。這裡是easeInOut
final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
//圖片所佔用的rect空間
final RectTween _imageRectTween = new RectTween(
begin: new Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
end: new Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
);
return Padding(
padding: new EdgeInsets.only(
//這個padding就直接設定變化
top: statusBarHeight + 0.5 * extraPadding,
bottom:extraPadding,
),
child: Center(
child: Transform(
//因為整體需要一個Scale的變化,所以就用transform.可以理解成css一樣的transfrom動畫。
//這裡是使用單位矩陣*scale來計算.scale等於當前logo的高度佔總共的高度
transform: new Matrix4.identity()..scale(logoHeight / kLogoHeight),
//佈置在上中
alignment: Alignment.topCenter,
child: SizedBox(
width: kLogoWidth,
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned.fromRect(
//這裡傳遞的佔用位置也是不斷變化的,這裡說明其實我們外層其實也可以用SizedBox來實現?
rect: _imageRectTween.lerp(t),
child: new Image.asset(
'flutter_gallery_assets/pesto/logo_small.png',
fit: BoxFit.contain,
),
),
Positioned.fromRect(
rect: _textRectTween.lerp(t),
child: Center(
//建立一個透明度來包裹
child: Opacity(
//找到這個曲線上t百分比佔的位置
opacity: _textOpacity.transform(t),
child: new Text('PESTO',
style: titleStyle,
textAlign: TextAlign.center),
),
),
),
],
),
),
),
),
);
})
複製程式碼
- 最終效果 可以觀察到,需要的實現效果有三個
- 上下的
padding
發生改變 通過直接改變包裹的padding
值來改變。
new EdgeInsets.only(
//這個padding就直接設定變化
top: statusBarHeight + 0.5 * extraPadding,
bottom:extraPadding,
),
複製程式碼
- 整體變小
通過在包裹一層,
Transfrom
元件,改變其中的矩陣來完成。 還有一個就是SizedBox中定義的Rect
來控制佔用的控制元件。並不會Scale
控制元件
child: Transform(
//因為整體需要一個Scale的變化,所以就用transform.可以理解成css一樣的transfrom動畫。
//這裡是使用單位矩陣*scale來計算.scale等於當前logo的高度佔總共的高度
transform: new Matrix4.identity()
..scale(logoHeight / kLogoHeight),
//佈置在上中
alignment: Alignment.topCenter,
child: SizedBox(
width: kLogoWidth,
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned.fromRect(
//這裡傳遞的佔用位置也是不斷變化的,這裡說明其實我們外層其實也可以用SizedBox來實現?
rect: _imageRectTween.lerp(t),
child: new Image.asset(
'flutter_gallery_assets/pesto/logo_small.png',
fit: BoxFit.contain,
),
),
Positioned.fromRect(
rect: _textRectTween.lerp(t),
child: Center(
//建立一個透明度來包裹
child: Opacity(
//找到這個曲線上t百分比佔的位置
opacity: _textOpacity.transform(t),
child: new Text('PESTO',
style: titleStyle,
textAlign: TextAlign.center),
),
),
),
],
),
),
),
複製程式碼
- 下面的文字有一個透明度的改變 通過包裹一層透明度元件(Opacity),修改opacity的值,來完成。
Opacity(
//找到這個曲線上t百分比佔的位置
opacity: _textOpacity.transform(t),
child: new Text('PESTO',
style: titleStyle,
textAlign: TextAlign.center),
),
複製程式碼
#####新增轉場動畫效果
使用Hero
元件包裹Image
,並且同時帶有相同的tag
- RecipeCard
class RecipeCard extends StatelessWidget {
//省略無用程式碼
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//這裡進行包裹
Hero(
tag: "${recipe.imagePath}",
child: Image.asset(
recipe.imagePath,
fit: BoxFit.contain,
),
),
Row(
//省略重複程式碼
)
],
),
),
);
}
}
複製程式碼
- _RecipePageState
//...省略
@override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
body: Stack(
children: <Widget>[
Positioned(
//同樣在這裡包裹住圖片
child: Hero(
tag: "${widget.recipe.imagePath}",
child: Image.asset(
widget.recipe.imagePath,
fit: BoxFit.cover,
height: appBarHeight + _kFabHalfSize,
),
),
top: 0.0,
left: 0.0,
right: 0.0,
),
CustomScrollView(//...省略
),
],
));
}
複製程式碼
總結
最後總結一下。 看到這樣一個,不屬於自帶效果的動畫,我們剛剛開始確實無法入手。 遇到這樣的方法,最簡單的也是最耗時方式就是降維。就像本編文章一樣,花了大量的事件,先完成靜態簡單的熟悉的頁面。再完成動態的效果。
這邊文章我們熟悉的知識點,可以簡單做一下回顧
- 封裝一個簡單的Card元件
- 使用ListView.Builder來顯示一個列表
- 使用
GestureDetector
來監聽手勢事件 - Stack佈局的使用。(可以理解成FrameLayout)
- dart的..的級聯用法。(這個用法超級常見和方便)
- Table TableRow 和TableCell元件來顯示簡單的表單功能
- 使用CustomScrollView結合SliverAppBar和SliverList來實現經典的MD動畫效果
- 使用CustomScrollView等元件,結合LayoutBuilder來實現自定義的動畫效果。
LayoutBuilder會傳入父元件的約束。我們就可以通過傳入的約束,計算變化量。並利用一系列內建的動畫元件
Transfrom
Opacity
等,來進行變化。