- 原文地址:Flutter Challenge: YouTube (Picture-In-Picture)
- 原文作者:Deven Joshi
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:MeFelixWang
挑戰 Flutter 嘗試在 Flutter 中重新建立特定應用的 UI 或設計。
此挑戰將嘗試實現 YouTube 的主頁和視訊詳情頁(視訊實際播放的頁面),包括動畫。
這個挑戰將比我以前的挑戰稍微複雜一些,但結果卻更好。
開始
YouTube 應用包括:
a)主頁包括:
- AppBar 中有三個 action
- 使用者訂閱視訊
- 底部導航欄
b)視訊詳情頁包括:
- 可縮小的主播放器,能讓使用者檢視他們的訂閱資訊(PIP)
- 基於當前視訊的使用者推薦
建立專案
讓我們建立一個名為 youtube_flutter 的 Flutter 專案,並刪除所有預設程式碼,只留下一個帶有預設 appBar 的空白頁面。
import `package:flutter/material.dart`;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: `Flutter Demo`,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(""),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
],
),
),
);
}
}
複製程式碼
製作 AppBar
AppBar 左側有 YouTube 的 logo 和名稱,右側有三個 action,即記錄、搜尋和開啟配置檔案。
重新建立 AppBar:
appBar: new AppBar(
backgroundColor: Colors.white,
title: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(FontAwesomeIcons.youtube, color: Colors.red,),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text("YouTube", style: TextStyle(color: Colors.black, letterSpacing: -1.0, fontWeight: FontWeight.w700),),
),
],
),
actions: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Icon(Icons.videocam, color: Colors.black54,),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Icon(Icons.search, color: Colors.black54,),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Icon(Icons.account_circle, color: Colors.black54,),
),
],
),
複製程式碼
這就是重新建立的 AppBar 的樣子:
注意:對於 YouTube 的 logo,我使用了 Dart pub FontFlutterAwesome 圖示。
接著製作底部導航欄,
建立 BottomNavigationBar
底部導航欄有5項,在 Flutter 中重新建立非常簡單。我們使用 Scaffold 的 bottomNavigationBar 引數。
bottomNavigationBar: BottomNavigationBar(items: [
BottomNavigationBarItem(icon: Icon(Icons.home, color: Colors.black54,), title: Text("Home", style: TextStyle(color: Colors.black54),),),
BottomNavigationBarItem(icon: Icon(FontAwesomeIcons.fire, color: Colors.black54,), title: Text("Home", style: TextStyle(color: Colors.black54),),),
BottomNavigationBarItem(icon: Icon(Icons.subscriptions, color: Colors.black54,), title: Text("Home", style: TextStyle(color: Colors.black54),),),
BottomNavigationBarItem(icon: Icon(Icons.email, color: Colors.black54,), title: Text("Home", style: TextStyle(color: Colors.black54),),),
BottomNavigationBarItem(icon: Icon(Icons.folder, color: Colors.black54,), title: Text("Home", style: TextStyle(color: Colors.black54),),),
], type: BottomNavigationBarType.fixed,),
複製程式碼
注意:對於4個以上的專案我們需要指定一個固定的 BottomNavigationBarType,因為為了避免擁擠預設型別是 shifting。
結果是:
重新建立的 YouTube 底部導航欄
使用者訂閱視訊
使用者訂閱視訊是由推薦視訊組成的專案列表。我們來看看列表項:
列表項由一個帶有一張圖片的 Column 和一個有關視訊資訊的 Raw 組成。該 Row 由一張圖片,一個包含標題、釋出者和選單按鈕的 Column 組成。
要在 Flutter 中建立列表,我們可以使用 ListView.builder()。重新建立列表項,如下:
ListView.builder(
itemCount: 3,
itemBuilder: (context, position) {
return Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(child: Image.asset(videos[position].imagePath, fit: BoxFit.cover,)),
],
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: Icon(Icons.account_circle, size: 40.0,), flex: 2,),
Expanded(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(videos[position].title, style: TextStyle(fontSize: 18.0),),
),
Text(videos[position].publisher, style: TextStyle(color: Colors.black54),)
],
crossAxisAlignment: CrossAxisAlignment.start,
),
flex: 9,
),
Expanded(child: Icon(Icons.more_vert), flex: 1,),
],
),
)
],
);
},
),
複製程式碼
這裡的視訊只是包含由標題和釋出者等視訊詳情的列表。
這是重新建立的主頁的樣子:
我們重新建立的主頁
現在,我們將繼續討論稍微難一點的部分,視訊詳情頁。
建立視訊詳情頁
視訊詳情頁才是在 YouTube 中真正展示視訊的頁面。頁面的亮點是我們可以縮小視訊,並在螢幕的右下角繼續播放。對於本文,我們將專注於縮小動畫而不是實際播放視訊。
請注意,這並不是一個特別的頁面,而是在現有螢幕上疊加覆蓋層。因此,我們將使用 Stack 元件來覆蓋螢幕。
所以在背後,將有我們的主頁,而頂部將是我們的視訊頁面。
構建浮動視訊播放器(畫中畫)
為了構建可以擴大至填充整個螢幕的浮動視訊播放器,我們使用 LayoutBuilder 來完美地適配螢幕。
在繼續之前,我們先定義一些值,即縮小和擴大時視訊播放器的大小。我們不用為擴大的播放器設定寬度,而是從佈局構建器中獲取。
var currentAlignment = Alignment.topCenter;
var minVideoHeight = 100.0;
var minVideoWidth = 150.0;
var maxVideoHeight = 200.0;
// 這是一個任意的值,當構建佈局時會改變。
var maxVideoWidth = 250.0;
var currentVideoHeight = 200.0;
var currentVideoWidth = 200.0;
bool isInSmallMode = false;
複製程式碼
這裡, “small mode” 指視訊播放器縮小的時候。
構建視訊詳情頁的 LayoutBuilder 可以寫成:
LayoutBuilder(
builder: (context, constraints) {
maxVideoWidth = constraints.biggest.width;
if(!isInSmallMode) {
currentVideoWidth = maxVideoWidth;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Expanded(
child: Align(
child: Padding(
padding: EdgeInsets.all(isInSmallMode? 8.0 : 0.0),
child: GestureDetector(
child: Container(
width: currentVideoWidth,
height: currentVideoHeight,
child: Image.asset(
videos[videoIndexSelected].imagePath,
fit: BoxFit.cover,),
color: Colors.blue,
),
onVerticalDragEnd: (details) {
if(details.velocity.pixelsPerSecond.dy > 0) {
setState(() {
isInSmallMode = true;
});
}else if (details.velocity.pixelsPerSecond.dy < 0){
setState(() {
});
}
},
),
),
alignment: currentAlignment,
),
flex: 3,
),
currentAlignment == Alignment.topCenter ?
Expanded(
flex: 6,
child: Container(
child: Column(
children: <Widget>[
Row(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Video Recommendation"),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Video Recommendation"),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Video Recommendation"),
),
),
)
],
),
color: Colors.white,
),
)
:Container(),
Row(),
],
);
},
)
複製程式碼
注意我們是如何獲得最大螢幕寬度然後使用,而不是使用我們的第一個任意值來作為最大螢幕寬度。
我們附加了一個 GestureDetector 來檢測螢幕上的滑動,以便我們可以相應地縮小和擴大它。讓我們建立動畫。
為視訊詳情頁新增動畫
當我們製作動畫時,需要處理兩件事:
- 將視訊從右上角移動到右下角。
- 更改視訊的大小並使其變小。
對於這些東西,我們使用兩個 Tweens,一個 AlignmentTween 和一個 Tween,並構造兩個同時執行的獨立動畫。
AnimationController alignmentAnimationController;
Animation alignmentAnimation;
AnimationController videoViewController;
Animation videoViewAnimation;
var currentAlignment = Alignment.topCenter;
@override
void initState() {
super.initState();
alignmentAnimationController = AnimationController(vsync: this, duration: Duration(seconds: 1))
..addListener(() {
setState(() {
currentAlignment = alignmentAnimation.value;
});
});
alignmentAnimation = AlignmentTween(begin: Alignment.topCenter, end: Alignment.bottomRight).animate(CurvedAnimation(parent: alignmentAnimationController, curve: Curves.fastOutSlowIn));
videoViewController = AnimationController(vsync: this, duration: Duration(seconds: 1))
..addListener(() {
setState(() {
currentVideoWidth = (maxVideoWidth*videoViewAnimation.value) + (minVideoWidth*(1.0-videoViewAnimation.value));
currentVideoHeight = (maxVideoHeight*videoViewAnimation.value) + (minVideoHeight*(1.0-videoViewAnimation.value));
});
});
videoViewAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(videoViewController);
}
複製程式碼
當我們的視訊播放器向上或向下滑動時,會觸發這些動畫。
onVerticalDragEnd: (details) {
if(details.velocity.pixelsPerSecond.dy > 0) {
setState(() {
isInSmallMode = true;
alignmentAnimationController.forward();
videoViewController.forward();
});
}else if (details.velocity.pixelsPerSecond.dy < 0){
setState(() {
alignmentAnimationController.reverse();
videoViewController.reverse().then((value) {
setState(() {
isInSmallMode = false;
});
});
});
}
},
複製程式碼
這是程式碼的最終結果:
最終重新建立的 YouTube 應用
以下是該應用的視訊:
- YouTube 視訊連結:youtu.be/dTpZ1BtNy4w
最終應用的 iOS 視訊
這是該專案的 GitHub 連結:github.com/deven98/You…
感謝閱讀此 Flutter 挑戰。可以留言告訴我任何你想要在 Flutter 中重新建立的應用。喜歡請給個 star,下次見。
不要錯過:
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。