MDC-104 Flutter: คอมโพเนนต์ขั้นสูงของวัสดุ

MDC-104 Flutter:
คอมโพเนนต์ขั้นสูงของวัสดุ

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ มิ.ย. 6, 2023
account_circleเขียนโดย Material Flutter Team

1 บทนำ

logo_components_color_2x_web_96dp.png

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

แอปอีคอมเมิร์ซธีมสีชมพูและน้ำตาลที่มีแถบแอปด้านบนและตารางกริดแบบอสมมาตรที่เลื่อนได้ในแนวนอนและเต็มไปด้วยผลิตภัณฑ์

แอปอีคอมเมิร์ซธีมสีชมพูและน้ำตาลที่มีแถบแอปด้านบนและตารางกริดแบบเลื่อนได้แนวนอนแบบไม่สมมาตรซึ่งเต็มไปด้วยผลิตภัณฑ์

ข้อมูลเมนู 4 หมวดหมู่

ข้อมูลเมนู 4 หมวดหมู่

คอมโพเนนต์และระบบย่อยของ 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

เปิดโปรเจ็กต์และเรียกใช้แอป

  1. เปิดโปรเจ็กต์ในเครื่องมือแก้ไขที่ต้องการ
  2. ทำตามวิธีการ "เรียกใช้แอป" ในเริ่มต้นใช้งาน: ลองใช้สำหรับโปรแกรมแก้ไขที่คุณเลือก

สำเร็จ! คุณควรเห็นหน้าเข้าสู่ระบบของ 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 เพื่อยืนยันว่าสแต็กมีคอนเทนเนอร์อยู่หลังหน้าแรกจริงๆ ซึ่งควรมีลักษณะคล้ายกับตัวอย่างต่อไปนี้

92ed338a15a074bd.png

ตอนนี้คุณปรับได้ทั้งการออกแบบและเนื้อหาของเลเยอร์

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

เมนูศาลเจ้าว่างเปล่าพร้อมข้อผิดพลาด 2 ข้อ

เมนูศาลเจ้าว่างเปล่าพร้อมข้อผิดพลาด 2 ข้อ

เลเยอร์ด้านหน้าเคลื่อนไหว (เลื่อน) ลง แต่หากมองลงไปจะเห็นข้อผิดพลาดสีแดงและข้อผิดพลาดเพิ่มเติมแสดง เนื่องจาก 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

เมนูศาลเจ้าว่างเปล่าและมีข้อผิดพลาด 1 ข้อ

เมนูศาลเจ้าว่างเปล่าและมีข้อผิดพลาด 1 ข้อ

คำเตือนรายการเพิ่มเติมสีเทาใน 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 จากไม่เก็บสถานะเป็นเก็บสถานะ

  1. ไฮไลต์ShrineApp.
  2. แสดงการดำเนินการโค้ดตาม IDE ของคุณ
  3. Android Studio: กด ⌥Enter (macOS) หรือ alt + enter
  4. VS Code: กด ⌘. (macOS) หรือ Ctrl+.
  5. เลือก "แปลงเป็น StatefulWidget"
  6. เปลี่ยนคลาส ShrineAppState เป็นแบบส่วนตัว (_ShrineAppState) คลิกขวาที่ ShrineAppState และ
  7. Android Studio: เลือก "ปรับโครงสร้าง" > "เปลี่ยนชื่อ"
  8. VS Code: เลือก "เปลี่ยนชื่อสัญลักษณ์"
  9. ป้อน _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

เมนูศาลเจ้าที่มี 4 หมวดหมู่

เมนูศาลเจ้าที่มี 4 หมวดหมู่

หากคุณแตะตัวเลือกเมนู ก็ยังไม่มีอะไรเกิดขึ้น เรามาแก้ปัญหานั้นกันดีกว่า

ใน 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

หน้าผลิตภัณฑ์ของ Shrine ที่มีไอคอนแบรนด์

หน้าผลิตภัณฑ์ของศาลเจ้าที่มีไอคอนแบรนด์

ใน 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

ฉันทำ Codelab นี้เสร็จภายในระยะเวลาและความพยายามที่เหมาะสม

ฉันต้องการใช้คอมโพเนนต์เนื้อหาต่อไปในอนาคต