1. บทนำ
Material Components (MDC) ช่วยให้นักพัฒนานำดีไซน์ Material มาใช้ MDC สร้างขึ้นโดยทีมวิศวกรและนักออกแบบ UX จาก Google โดยมีคอมโพเนนต์ UI ที่สวยงามและใช้งานได้จริงหลายสิบรายการ และพร้อมใช้งานสำหรับ Android, iOS, เว็บ และ Flutter ที่ material.io/develop |
ในโค้ดแล็บ MDC-103 คุณได้ปรับแต่งสี ระดับความสูง การจัดรูปแบบตัวอักษร และรูปร่างของคอมโพเนนต์ Material (MDC) เพื่อจัดสไตล์แอป
คอมโพเนนต์ในระบบ Material Design จะทํางานชุดหนึ่งๆ ที่กําหนดไว้ล่วงหน้าและมีลักษณะบางอย่าง เช่น ปุ่ม อย่างไรก็ตาม ปุ่มไม่ได้มีไว้เพียงเพื่อให้ผู้ใช้ดำเนินการเท่านั้น แต่ยังเป็นการแสดงรูปร่าง ขนาด และสีที่ช่วยให้ผู้ใช้ทราบว่าปุ่มเป็นแบบอินเทอร์แอกทีฟ และจะมีบางอย่างเกิดขึ้นเมื่อแตะหรือคลิก
หลักเกณฑ์ดีไซน์ Material จะอธิบายส่วนประกอบต่างๆ จากมุมมองของนักออกแบบ ซึ่งอธิบายฟังก์ชันพื้นฐานที่หลากหลายซึ่งมีอยู่ในแพลตฟอร์มต่างๆ และองค์ประกอบทางกายภาพที่ประกอบกันเป็นคอมโพเนนต์แต่ละรายการ ตัวอย่างเช่น ฉากหลังประกอบด้วยเลเยอร์ด้านหลังและเนื้อหาของเลเยอร์ เลเยอร์ด้านหน้าและเนื้อหาของเลเยอร์ กฎการเคลื่อนไหว และตัวเลือกการแสดงผล องค์ประกอบแต่ละอย่างเหล่านี้สามารถปรับแต่งตามความต้องการ กรณีการใช้งาน และเนื้อหาของแต่ละแอปได้
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ คุณจะเปลี่ยน UI ในแอป Shrine เป็นการนำเสนอแบบ 2 ระดับที่เรียกว่า "ฉากหลัง" พื้นหลังมีเมนูที่แสดงรายการหมวดหมู่ที่เลือกได้เพื่อใช้กรองผลิตภัณฑ์ที่แสดงในตารางกริดแบบไม่สมมาตร ใน Codelab นี้ คุณจะใช้สิ่งต่อไปนี้
- รูปร่าง
- การเคลื่อนไหว
- วิดเจ็ต Flutter (ที่คุณเคยใช้ใน Codelab ก่อนหน้านี้)
Android | iOS |
คอมโพเนนต์และระบบย่อยของ Material Flutter ใน Codelab นี้
- รูปร่าง
คุณจะให้คะแนนระดับประสบการณ์ในการพัฒนาด้วย Flutter เท่าใด
2. ตั้งค่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ Flutter
คุณต้องใช้ซอฟต์แวร์ 2 อย่างในการฝึกนี้ ได้แก่ Flutter SDK และเครื่องมือแก้ไข
คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้
- อุปกรณ์ Android หรือ iOS ที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
- เครื่องมือจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
- โปรแกรมจำลอง Android (ต้องตั้งค่าใน Android Studio)
- เบราว์เซอร์ (ต้องใช้ Chrome สำหรับการแก้ไขข้อบกพร่อง)
- เป็นแอปพลิเคชัน Windows, Linux หรือ macOS บนเดสก์ท็อป คุณต้องพัฒนาในแพลตฟอร์มที่คุณวางแผนจะใช้งาน ดังนั้น หากต้องการพัฒนาแอปเดสก์ท็อป Windows คุณต้องพัฒนาใน Windows เพื่อเข้าถึงเชนการบิลด์ที่เหมาะสม มีข้อกำหนดเฉพาะระบบปฏิบัติการที่ครอบคลุมรายละเอียดใน docs.flutter.dev/desktop
3. ดาวน์โหลดแอปเริ่มต้นของ Codelab
หากต้องการดําเนินการต่อจาก MDC-103
หากคุณดำเนินการ MDC-103 เสร็จสมบูรณ์แล้ว โค้ดของคุณก็จะพร้อมใช้งานสำหรับ Codelab นี้ ข้ามไปยังขั้นตอน: เพิ่มเมนูฉากหลัง
หากเพิ่งเริ่มต้น
แอปเริ่มต้นอยู่ในไดเรกทอรี material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series
...หรือจะโคลนจาก GitHub ก็ได้
หากต้องการโคลนโค้ดแล็บนี้จาก GitHub ให้เรียกใช้คำสั่งต่อไปนี้
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 104-starter_and_103-complete
เปิดโปรเจ็กต์และเรียกใช้แอป
- เปิดโปรเจ็กต์ในเครื่องมือแก้ไขที่ต้องการ
- ทำตามวิธีการ "เรียกใช้แอป" ในเริ่มต้นใช้งาน: ลองใช้สำหรับโปรแกรมแก้ไขที่คุณเลือก
สำเร็จ! คุณควรเห็นหน้าเข้าสู่ระบบของ Shrine จาก Codelab ก่อนหน้าในอุปกรณ์
Android | iOS |
4. เพิ่มเมนูฉากหลัง
ฉากหลังจะปรากฏขึ้นด้านหลังเนื้อหาและคอมโพเนนต์อื่นๆ ทั้งหมด ซึ่งประกอบด้วย 2 เลเยอร์ ได้แก่ เลเยอร์หลัง (ที่แสดงการทำงานและตัวกรอง) และเลเยอร์หน้า (ที่แสดงเนื้อหา) คุณสามารถใช้พื้นหลังเพื่อแสดงข้อมูลและการดําเนินการแบบอินเทอร์แอกทีฟ เช่น การนําทางหรือตัวกรองเนื้อหา
นำแถบแอปหน้าแรกออก
วิดเจ็ต HomePage จะเป็นเนื้อหาของเลเยอร์หน้าสุดของเรา โดยตอนนี้จะมีแถบแอป เราจะย้ายแถบแอปไปยังเลเยอร์ด้านหลังและหน้าแรกจะมีเฉพาะ AsymmetricView เท่านั้น
ใน home.dart
ให้เปลี่ยนฟังก์ชัน build()
ให้แสดงผลเฉพาะ AsymmetricView ดังนี้
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
เพิ่มวิดเจ็ตพื้นหลัง
สร้างวิดเจ็ตชื่อ Backdrop ที่มี frontLayer
และ backLayer
backLayer
มีเมนูที่อนุญาตให้คุณเลือกหมวดหมู่เพื่อกรองรายการ (currentCategory
) เนื่องจากเราต้องการคงการเลือกเมนูไว้ เราจึงทำให้ฉากหลังเป็นวิดเจ็ตเก็บสถานะ
วิธีเพิ่มไฟล์ใหม่ลงใน /lib
ชื่อ backdrop.dart
import 'package:flutter/material.dart';
import 'model/product.dart';
// TODO: Add velocity constant (104)
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,
Key? key,
}) : super(key: key);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
โปรดทราบว่าเราทำเครื่องหมายพร็อพเพอร์ตี้บางรายการเป็น required
วิธีนี้เป็นแนวทางปฏิบัติแนะนำสำหรับพร็อพเพอร์ตี้ในเครื่องมือสร้างที่ไม่มีค่าเริ่มต้น และเป็น null
ไม่ได้ ดังนั้นจึงไม่ควรลืม
เพิ่มคลาส _BackdropState ในส่วนการกําหนดคลาสของพื้นหลัง
// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO: Add AnimationController widget (104)
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
elevation: 0.0,
titleSpacing: 0.0,
// TODO: Replace leading menu icon with IconButton (104)
// TODO: Remove leading property (104)
// TODO: Create title with _BackdropTitle parameter (104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO: Add open login (104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO: Add open login (104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: _buildStack(),
);
}
}
ฟังก์ชัน build()
จะแสดงผล Scaffold ที่มีแถบแอปเหมือนกับที่ HomePage เคยแสดง แต่เนื้อหาของ Scaffold คือ Stack องค์ประกอบย่อยของชุดกลุ่มสามารถทับซ้อนกันได้ ขนาดและตำแหน่งของแต่ละลูกจะถูกระบุโดยสัมพันธ์กับรายการหลักของกลุ่ม
ตอนนี้ให้เพิ่มอินสแตนซ์ฉากหลังไปยัง ShrineApp
ใน app.dart
ให้นําเข้า backdrop.dart
และ model/product.dart
โดยทําดังนี้
import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';
ใน app.dart,
ให้แก้ไขเส้นทาง /
โดยแสดงผล Backdrop
ที่มี HomePage
เป็น frontLayer
ดังนี้
// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: Category.all,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
บันทึกโปรเจ็กต์ของคุณ คุณจะเห็นว่าหน้าแรกปรากฏขึ้น เช่นเดียวกับแถบแอป
Android | iOS |
backLayer แสดงพื้นที่สีชมพูในเลเยอร์ใหม่ที่อยู่เบื้องหลังหน้าแรกของ frontLayer
คุณสามารถใช้ Flutter Inspector เพื่อยืนยันว่าสแต็กมีคอนเทนเนอร์อยู่หลังหน้าแรกจริงๆ ซึ่งควรมีลักษณะคล้ายกับตัวอย่างต่อไปนี้
ตอนนี้คุณปรับได้ทั้งการออกแบบและเนื้อหาของเลเยอร์
5. เพิ่มรูปร่าง
ในขั้นตอนนี้ คุณจะต้องจัดสไตล์เลเยอร์ด้านหน้าเพื่อเพิ่มการตัดที่มุมซ้ายบน
ดีไซน์ Material เรียกการปรับแต่งประเภทนี้ว่ารูปร่าง พื้นผิววัสดุมีรูปร่างที่กำหนดเองได้ รูปร่างช่วยเพิ่มความโดดเด่นและสไตล์ให้กับพื้นผิว รวมถึงใช้เพื่อแสดงแบรนด์ได้ รูปสี่เหลี่ยมผืนผ้าธรรมดาสามารถปรับแต่งให้มีมุมและขอบโค้งหรือเป็นมุม และปรับให้มีจำนวนด้านเท่าใดก็ได้ โดยอาจเป็นแบบสมมาตรหรือไม่สมมาตรก็ได้
เพิ่มรูปร่างลงในเลเยอร์ด้านหน้า
โลโก้ Shrine แบบเอียงเป็นแรงบันดาลใจให้กับเรื่องราวรูปร่างของแอป Shrine เรื่องราวรูปร่างเป็นการนำรูปทรงต่างๆ ที่นำไปใช้ในแอปทั่วไปไปใช้ เช่น รูปร่างโลโก้จะสะท้อนอยู่ในองค์ประกอบของหน้าเข้าสู่ระบบที่มีการใช้รูปร่าง ในขั้นตอนนี้ คุณจะต้องจัดรูปแบบเลเยอร์หน้าด้วยการตัดมุมที่มุมบนซ้าย
ใน backdrop.dart
ให้เพิ่มคลาสใหม่ _FrontLayer
// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
Expanded(
child: child,
),
],
),
);
}
}
จากนั้นในฟังก์ชัน _buildStack()
ของ _BackdropState ให้รวมเลเยอร์ด้านหน้าไว้ใน _FrontLayer ดังนี้
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
โหลดซ้ำ
Android | iOS |
เราได้กำหนดรูปร่างที่กำหนดเองให้กับพื้นผิวหลักของ Shrine อย่างไรก็ตาม เราต้องการให้ไอคอนนี้เชื่อมต่อภาพกับแถบแอป
เปลี่ยนสีแถบแอป
ใน app.dart
ให้เปลี่ยนฟังก์ชัน _buildShrineTheme()
เป็นดังนี้
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePink100,
onPrimary: kShrineBrown900,
secondary: kShrineBrown900,
error: kShrineErrorRed,
),
textTheme: _buildShrineTextTheme(base.textTheme),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePink100,
),
appBarTheme: const AppBarTheme(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
),
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
);
}
รีสตาร์ทขณะทำงาน แถบแอปสีใหม่จะปรากฏขึ้น
Android | iOS |
การเปลี่ยนแปลงนี้ทำให้ผู้ใช้เห็นว่ามีบางสิ่งอยู่หลังเลเยอร์สีขาวด้านหน้า มาเพิ่มการเคลื่อนไหวเพื่อให้ผู้ใช้เห็นเลเยอร์ด้านหลังของฉากหลัง
6. เพิ่มการเคลื่อนไหว
ภาพเคลื่อนไหวคือวิธีที่ทำให้แอปมีชีวิตชีวา อาจเป็นลายที่โดดเด่นและสะดุดตา ลายที่ละเอียดและมินิมอล หรือลายที่อยู่ระหว่างกลางก็ได้ แต่อย่าลืมว่าประเภทการเคลื่อนไหวที่คุณใช้ควรเหมาะกับสถานการณ์ ภาพเคลื่อนไหวที่ใช้กับการดำเนินการซ้ำๆ ตามปกติควรมีขนาดเล็กและสังเกตได้ยาก เพื่อไม่ให้การดำเนินการดังกล่าวรบกวนผู้ใช้หรือใช้เวลามากเกินไปเป็นประจำ แต่ก็มีบางสถานการณ์ที่เหมาะสม เช่น เมื่อผู้ใช้เปิดแอปเป็นครั้งแรก ซึ่งอาจดึงดูดสายตาได้มากกว่า และภาพเคลื่อนไหวบางรายการอาจช่วยสอนผู้ใช้เกี่ยวกับวิธีใช้แอป
เพิ่มการเคลื่อนไหวแบบเผยให้เห็นในปุ่มเมนู
ที่ด้านบนของ backdrop.dart
นอกเหนือจากขอบเขตของคลาสหรือฟังก์ชัน ให้เพิ่มค่าคงที่เพื่อแสดงความเร็วที่ต้องการให้ภาพเคลื่อนไหวมี
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
เพิ่มวิดเจ็ต AnimationController ไปยัง _BackdropState สร้างอินสแตนซ์ในฟังก์ชัน initState()
และกำจัดในฟังก์ชัน dispose()
ของรัฐ ดังนี้
// TODO: Add AnimationController widget (104)
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO: Add override for didUpdateWidget (104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO: Add functions to get and change front layer visibility (104)
AnimationController ประสานงานภาพเคลื่อนไหว และให้ API แก่คุณเพื่อเล่น ย้อนกลับ และหยุดภาพเคลื่อนไหว ตอนนี้เราต้องการฟังก์ชันที่ทำให้รูปภาพเคลื่อนไหว
เพิ่มฟังก์ชันที่กำหนดและเปลี่ยนการเปิดเผยของเลเยอร์หน้า:
// TODO: Add functions to get and change front layer visibility (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 วิดเจ็ตนี้จะยกเว้นรายการเมนูของ BackLayer จากแผนผังอรรถศาสตร์เมื่อมองไม่เห็นเลเยอร์หลัง
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
เปลี่ยนฟังก์ชัน _buildStack() เพื่อใช้ BuildContext และ BoxConstraints นอกจากนี้ยังรวมการเปลี่ยนตำแหน่งที่ใช้ภาพเคลื่อนไหวแบบ RelativeRectTween ด้วย ดังนี้
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO: Create a RelativeRectTween Animation (104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO: Add a PositionedTransition (104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
child: widget.frontLayer,
),
),
],
);
}
สุดท้าย ให้แสดงผลวิดเจ็ต LayoutBuilder ที่ใช้ _buildStack เป็น Builder แทนการเรียกใช้ฟังก์ชัน _buildStack สำหรับเนื้อหาของ Scaffold ดังนี้
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
เราได้เลื่อนการสร้างกองเลเยอร์ด้านหน้า/ด้านหลังไว้จนกว่าจะถึงเวลาจัดเลย์เอาต์โดยใช้ LayoutBuilder เพื่อให้รวมความสูงโดยรวมจริงของฉากหลังได้ LayoutBuilder เป็นวิดเจ็ตพิเศษที่ Callback ของเครื่องมือสร้างจะระบุข้อจำกัดด้านขนาด
ในฟังก์ชัน build()
ให้เปลี่ยนไอคอนเมนูนำหน้าในแถบแอปเป็น IconButton และใช้เพื่อสลับการมองเห็นเลเยอร์ด้านหน้าเมื่อแตะปุ่ม
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
โหลดซ้ำแล้วแตะปุ่มเมนูในเครื่องจำลอง
Android | iOS |
เลเยอร์ด้านหน้าเคลื่อนไหว (เลื่อน) ลง แต่หากมองลงไปจะเห็นข้อผิดพลาดสีแดงและข้อผิดพลาดเพิ่มเติมแสดง เนื่องจาก AsymmetricView ถูกบีบและเล็กลงจากภาพเคลื่อนไหวนี้ ซึ่งทำให้คอลัมน์มีที่ว่างน้อยลง ในที่สุด คอลัมน์จะจัดวางตัวเองไม่ได้ด้วยพื้นที่ว่างที่มีให้และทำให้เกิดข้อผิดพลาด หากเราแทนที่คอลัมน์ด้วย ListView ขนาดคอลัมน์ควรยังคงเหมือนเดิมขณะแสดงภาพเคลื่อนไหว
ตัดคอลัมน์ผลิตภัณฑ์ใน ListView
ใน supplemental/product_columns.dart
ให้แทนที่คอลัมน์ใน OneProductCardColumn
ด้วย ListView ดังนี้
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({required this.product, Key? key}) : super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 550,
),
child: ProductCard(
product: product,
),
),
const SizedBox(
height: 40.0,
),
],
);
}
}
คอลัมน์ดังกล่าวมี MainAxisAlignment.end
เริ่มเลย์เอาต์จากด้านล่าง ทําเครื่องหมาย reverse: true
ระบบจะยกเลิกคำสั่งซื้อของบุตรหลานเพื่อชดเชยการเปลี่ยนแปลง
โหลดซ้ำแล้วแตะปุ่มเมนู
Android | iOS |
คำเตือนรายการเพิ่มเติมสีเทาใน OneProductCardColumn หายไปแล้ว มาแก้ปัญหาอีกรายการกัน
ใน supplemental/product_columns.dart
ให้เปลี่ยนวิธีคํานวณ imageAspectRatio
และแทนที่คอลัมน์ใน TwoProductCardColumn
ด้วย ListView ดังนี้
// TODO: Change imageAspectRatio calculation (104)
double imageAspectRatio = heightOfImages >= 0.0
? constraints.biggest.width / heightOfImages
: 49.0 / 33.0;
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top!,
)
: SizedBox(
height: heightOfCards,
),
),
const SizedBox(height: spacerHeight),
Padding(
padding: const EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
นอกจากนี้ เรายังเพิ่มความปลอดภัยให้กับ imageAspectRatio
ด้วย
โหลดซ้ำ จากนั้นแตะปุ่มเมนู
Android | iOS |
ไม่ต้องกังวลว่าจะเกินขีดจำกัดอีกต่อไป
7. เพิ่มเมนูในเลเยอร์ด้านหลัง
เมนูคือรายการรายการข้อความที่แตะได้ซึ่งจะแจ้งให้โปรแกรมฟังเมื่อมีการแตะรายการข้อความ ในขั้นตอนนี้ คุณจะต้องเพิ่มเมนูการกรองหมวดหมู่
เพิ่มเมนู
เพิ่มเมนูลงในเลเยอร์ด้านหน้าและปุ่มแบบอินเทอร์แอกทีฟลงในเลเยอร์ด้านหลัง
สร้างไฟล์ใหม่ชื่อ lib/category_menu_page.dart
โดยทำดังนี้
import 'package:flutter/material.dart';
import 'colors.dart';
import 'model/product.dart';
class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged<Category> onCategoryTap;
final List<Category> _categories = Category.values;
const CategoryMenuPage({
Key? key,
required this.currentCategory,
required this.onCategoryTap,
}) : super(key: key);
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: <Widget>[
const SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.bodyLarge!.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
นั่นคือ GestureDetector ที่รวมคอลัมน์ที่มีรายการย่อยเป็นชื่อหมวดหมู่ ระบบจะใช้ขีดล่างเพื่อระบุหมวดหมู่ที่เลือก
ใน app.dart
ให้แปลงวิดเจ็ต ShrineApp จากไม่เก็บสถานะเป็นเก็บสถานะ
- ไฮไลต์
ShrineApp.
- แสดงการดำเนินการโค้ดตาม IDE ของคุณ
- Android Studio: กด ⌥Enter (macOS) หรือ alt + enter
- VS Code: กด ⌘. (macOS) หรือ Ctrl+.
- เลือก "แปลงเป็น StatefulWidget"
- เปลี่ยนคลาส ShrineAppState เป็นแบบส่วนตัว (_ShrineAppState) คลิกขวาที่ ShrineAppState และ
- Android Studio: เลือก "ปรับโครงสร้าง" > "เปลี่ยนชื่อ"
- VS Code: เลือก "เปลี่ยนชื่อสัญลักษณ์"
- ป้อน _ShrineAppState เพื่อทำให้ชั้นเรียนเป็นแบบส่วนตัว
ใน app.dart
ให้เพิ่มตัวแปรลงใน _ShrineAppState สําหรับหมวดหมู่ที่เลือกและ Callback เมื่อมีการแตะดังนี้
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
จากนั้นเปลี่ยนเลเยอร์ด้านหลังเป็น CategoryMenuPage
ใน app.dart
ให้นำเข้า CategoryMenuPage:
import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
ในฟังก์ชัน build()
ให้เปลี่ยนช่อง backLayer เป็น CategoryMenuPage และช่อง currentCategory เพื่อรับตัวแปรอินสแตนซ์
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: _currentCategory,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: const Text('SHRINE'),
backTitle: const Text('MENU'),
),
โหลดซ้ำแล้วแตะปุ่มเมนู
Android | iOS |
หากคุณแตะตัวเลือกเมนู ก็ยังไม่มีอะไรเกิดขึ้น เรามาแก้ปัญหานั้นกันดีกว่า
ใน home.dart
ให้เพิ่มตัวแปรสำหรับหมวดหมู่แล้วส่งไปยัง AsymmetricView
import 'package:flutter/material.dart';
import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO: Add a variable for Category (104)
final Category category;
const HomePage({this.category = Category.all, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO: Pass Category variable to AsymmetricView (104)
return AsymmetricView(
products: ProductsRepository.loadProducts(category),
);
}
}
ใน app.dart
ให้ส่ง _currentCategory
สำหรับ frontLayer
ดังนี้
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
โหลดซ้ำ แตะปุ่มเมนูในเครื่องมือจำลอง และเลือกหมวดหมู่
Android | iOS |
กรองแล้ว
ปิดเลเยอร์ด้านหน้าหลังจากเลือกเมนู
ใน backdrop.dart
ให้เพิ่มการลบล้างสำหรับฟังก์ชัน didUpdateWidget()
(เรียกว่าเมื่อใดก็ตามที่มีการเปลี่ยนแปลงการกำหนดค่าวิดเจ็ต) ใน _BackdropState:
// TODO: Add override for didUpdateWidget() (104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
บันทึกโปรเจ็กต์เพื่อทริกเกอร์การโหลดซ้ำขณะทำงาน แตะไอคอนเมนูและเลือกหมวดหมู่ เมนูควรปิดโดยอัตโนมัติและคุณควรเห็นหมวดหมู่ของรายการที่เลือก ถึงตอนนี้คุณจะต้องเพิ่มฟังก์ชันดังกล่าวไว้ในเลเยอร์หน้าด้วย
สลับเลเยอร์หน้า
ใน backdrop.dart
ให้เพิ่มการเรียกกลับเมื่อแตะลงในเลเยอร์พื้นหลัง โดยทำดังนี้
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
this.onTap, // New code
required this.child,
}) : super(key: key);
final VoidCallback? onTap; // New code
final Widget child;
จากนั้นเพิ่ม GestureDetector ลงในองค์ประกอบย่อยของ _FrontLayer: องค์ประกอบย่อยของคอลัมน์:
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
จากนั้นติดตั้งใช้งานพร็อพเพอร์ตี้ onTap
ใหม่บน _BackdropState ในฟังก์ชัน _buildStack()
ดังนี้
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
โหลดซ้ำแล้วแตะด้านบนของเลเยอร์ด้านหน้า เลเยอร์ควรเปิดและปิดทุกครั้งที่คุณแตะด้านบนของเลเยอร์ด้านหน้า
8. เพิ่มไอคอนแบรนด์
ระบบการตีความสัญลักษณ์ของแบรนด์ยังครอบคลุมถึงไอคอนที่คุ้นเคยด้วยเช่นกัน มาสร้างไอคอนการเปิดเผยที่กำหนดเองและผสานเข้ากับชื่อเพื่อให้มีรูปลักษณ์ที่ไม่เหมือนใครและสื่อถึงแบรนด์กัน
เปลี่ยนไอคอนปุ่มเมนู
Android | iOS |
ใน backdrop.dart
ให้สร้างคลาสใหม่ชื่อ _BackdropTitle
// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
final void Function() onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key? key,
required Animation<double> listenable,
required this.onPress,
required this.frontTitle,
required this.backTitle,
}) : _listenable = listenable,
super(key: key, listenable: listenable);
final Animation<double> _listenable;
@override
Widget build(BuildContext context) {
final Animation<double> animation = _listenable;
return DefaultTextStyle(
style: Theme.of(context).textTheme.titleLarge!,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: const EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.0, 0.0),
).evaluate(animation),
child: const ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
_BackdropTitle
คือวิดเจ็ตที่กําหนดเองซึ่งจะมาแทนที่วิดเจ็ต Text
ธรรมดาสําหรับพารามิเตอร์ title
ของวิดเจ็ต AppBar
โฆษณาจะมีไอคอนเมนูแบบเคลื่อนไหวและการเปลี่ยนแบบภาพเคลื่อนไหวระหว่างชื่อหน้ากับชื่อหลัง ไอคอนเมนูแบบเคลื่อนไหวจะใช้เนื้อหาใหม่ คุณต้องเพิ่มการอ้างอิง slanted_menu.png
ใหม่ลงใน pubspec.yaml
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
นําพร็อพเพอร์ตี้ leading
ออกจากเครื่องมือสร้าง AppBar
คุณต้องนำไอคอนดังกล่าวออกเพื่อให้ระบบแสดงผลไอคอนแบรนด์ที่กำหนดเองแทนวิดเจ็ต leading
เดิม ระบบจะส่งภาพเคลื่อนไหว listenable
และตัวแฮนเดิล onPress
สำหรับไอคอนที่มีแบรนด์ไปยัง _BackdropTitle
ระบบส่งผ่าน frontTitle
และ backTitle
ด้วยเพื่อให้แสดงผลภายในชื่อฉากหลังได้ พารามิเตอร์ title
ของ AppBar
ควรมีลักษณะดังนี้
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
ไอคอนที่มีแบรนด์สร้างขึ้นใน _BackdropTitle.
โดยมีไอคอนเคลื่อนไหว Stack
รายการ ได้แก่ เมนูที่ลาดเอียงและเพชร ซึ่งอยู่ใน IconButton
เพื่อให้กดได้ จากนั้น IconButton
จะรวมไว้ใน SizedBox
เพื่อให้มีพื้นที่สำหรับการเคลื่อนไหวไอคอนแนวนอน
สถาปัตยกรรม "ทุกอย่างคือวิดเจ็ต" ของ Flutter ช่วยให้คุณสามารถปรับเปลี่ยนเลย์เอาต์ของ AppBar
เริ่มต้นได้โดยไม่ต้องสร้างวิดเจ็ต AppBar
ที่กําหนดเองใหม่ทั้งหมด พารามิเตอร์ title
ซึ่งเดิมเป็นวิดเจ็ต Text
สามารถแทนที่ด้วย _BackdropTitle
ที่ซับซ้อนมากขึ้นได้ เนื่องจาก _BackdropTitle
มีไอคอนที่กำหนดเองด้วย จึงใช้แทนพร็อพเพอร์ตี้ leading
ซึ่งตอนนี้สามารถละเว้นได้ การแทนที่วิดเจ็ตง่ายๆ นี้สามารถทำได้โดยไม่ต้องเปลี่ยนแปลงพารามิเตอร์อื่นๆ เช่น ไอคอนการทำงาน ซึ่งยังคงทำงานได้ด้วยตัวเองต่อไป
เพิ่มทางลัดกลับไปยังหน้าจอการเข้าสู่ระบบ
ใน backdrop.dart,
เพิ่มทางลัดกลับไปยังหน้าจอเข้าสู่ระบบจากไอคอน 2 ไอคอนที่อยู่ท้ายแถบแอป: เปลี่ยนป้ายกำกับเชิงความหมายของไอคอนให้สอดคล้องกับวัตถุประสงค์ใหม่
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
คุณจะได้รับข้อผิดพลาดหากลองโหลดซ้ำ โปรดนำเข้า login.dart
เพื่อแก้ไขข้อผิดพลาด
import 'login.dart';
โหลดแอปซ้ำแล้วแตะปุ่มค้นหาหรือปรับแต่งเพื่อกลับไปยังหน้าจอการเข้าสู่ระบบ
9. ยินดีด้วย
ในบทเรียนจาก Codelab ทั้ง 4 ครั้งนี้ คุณได้เรียนรู้วิธีใช้คอมโพเนนต์ของ Material เพื่อสร้างประสบการณ์การใช้งานที่สง่างามไม่เหมือนใคร ซึ่งสะท้อนถึงบุคลิกภาพและสไตล์ของแบรนด์
ขั้นตอนถัดไป
Codelab นี้ MDC-104 เป็น Codelab ลำดับสุดท้ายในชุดนี้ คุณสำรวจคอมโพเนนต์อื่นๆ เพิ่มเติมใน Material Flutter ได้โดยไปที่แคตตาล็อกวิดเจ็ตคอมโพเนนต์ Material
สำหรับเป้าหมายเพิ่มเติม ให้ลองแทนที่ไอคอนที่มีแบรนด์ด้วย AnimatedIcon ที่แสดงภาพเคลื่อนไหวระหว่าง 2 ไอคอนเมื่อแสดงพื้นหลัง
ยังมีCodelab ของ Flutter อีกมากมายให้คุณลองทำตามความสนใจ เรามี Codelab เฉพาะของ Material อีกรายการหนึ่งซึ่งคุณอาจสนใจ นั่นคือ การสร้างการเปลี่ยนภาพที่สวยงามด้วยการเคลื่อนไหวของวัสดุสำหรับ Flutter