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

ใน Codelab MDC-103 คุณได้ปรับแต่งสี ระดับความสูง การพิมพ์ และรูปร่างของ Material Components (MDC) เพื่อจัดรูปแบบแอป

คอมโพเนนต์หนึ่งในระบบดีไซน์ Material ทำงานชุดหนึ่งที่กำหนดไว้ล่วงหน้าและมีลักษณะเฉพาะบางอย่าง เช่น ปุ่ม อย่างไรก็ตาม ปุ่มเป็นมากกว่าวิธีสำหรับให้ผู้ใช้ทำงาน แต่ยังเป็นการแสดงออกทางภาพของรูปร่าง ขนาด และสีที่ช่วยให้ผู้ใช้ทราบว่าเป็นการโต้ตอบ และจะมีบางอย่างเกิดขึ้นเมื่อมีการแตะหรือคลิก

หลักเกณฑ์ดีไซน์ 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

หากต้องการโคลน Codelab นี้จาก 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 เลเยอร์ ได้แก่ เลเยอร์หลัง (ที่แสดงการทำงานและตัวกรอง) และเลเยอร์หน้า (ที่แสดงเนื้อหา) คุณสามารถใช้ฉากหลังเพื่อแสดงข้อมูลและการดำเนินการแบบอินเทอร์แอกทีฟ เช่น การนำทางหรือตัวกรองเนื้อหา

นำแถบแอป Home ออก

วิดเจ็ต HomePage จะเป็นเนื้อหาของเลเยอร์หน้าสุดของเรา ตอนนี้มีแถบแอปอยู่ เราจะย้ายแถบแอปไปที่เลเยอร์หลังและหน้าแรกจะรวมเฉพาะ AsymmetricView เท่านั้น

ใน home.dart ให้เปลี่ยนฟังก์ชัน build() เพื่อแสดงผล AsymmetricView เท่านั้น:

// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));

เพิ่มวิดเจ็ตฉากหลัง

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

ตอนนี้ให้เพิ่มอินสแตนซ์ฉากหลังไปยัง 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

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

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

หน้าผลิตภัณฑ์ของศาลเจ้าที่มีรูปร่างที่กำหนดเอง

หน้าผลิตภัณฑ์ของศาลเจ้าที่มีรูปร่างที่กำหนดเอง

โดยได้เปลี่ยนพื้นผิวหลักของศาลเจ้าเป็นรูปร่างที่กำหนดเอง อย่างไรก็ตาม เราต้องการให้ไอคอนนี้เชื่อมต่อภาพกับแถบแอป

เปลี่ยนสีแถบแอป

ใน 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,
          ),
        ),
      ],
    );
  }

สุดท้าย แทนที่จะเรียกใช้ฟังก์ชัน _buildStack สำหรับเนื้อหาของ Scaffold ให้แสดงผลวิดเจ็ต LayoutBuilder ที่ใช้ _buildStack เป็นเครื่องมือสร้าง:

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

เราได้เลื่อนการสร้างสแต็กเลเยอร์ด้านหน้า/ด้านหลังจนถึงเวลาของเลย์เอาต์โดยใช้ LayoutBuilder เพื่อให้เรารวมความสูงโดยรวมตามจริงของฉากหลังได้ LayoutBuilder เป็นวิดเจ็ตพิเศษที่ Callback ของเครื่องมือสร้างมีข้อจำกัดด้านขนาด

ในฟังก์ชัน build() ให้เปลี่ยนไอคอนเมนูนำหน้าในแถบแอปเป็น "ปุ่มไอคอน" และใช้เพื่อสลับการมองเห็นเลเยอร์หน้าเมื่อแตะปุ่ม

      // 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 โค้ด: กด ⌘ (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);
    }
  }

บันทึกโปรเจ็กต์เพื่อทริกเกอร์การโหลดซ้ำแบบ Hot แตะไอคอนเมนูและเลือกหมวดหมู่ เมนูควรปิดโดยอัตโนมัติและคุณจะเห็นหมวดหมู่ของรายการที่เลือก ถึงตอนนี้คุณจะต้องเพิ่มฟังก์ชันดังกล่าวไว้ในเลเยอร์หน้าด้วย

สลับเลเยอร์หน้า

ใน backdrop.dart ให้เพิ่ม Callback เมื่อมีการแตะลงในเลเยอร์ฉากหลังโดยทำดังนี้

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 Components

สำหรับเป้าหมายแบบขยาย ให้ลองแทนที่ไอคอนที่มีแบรนด์ด้วย AnimatedIcon ที่เคลื่อนไหวระหว่าง 2 ไอคอนเมื่อทำให้ฉากหลังปรากฏขึ้น

มี Codelab ของ Flutter มากมายให้ลองใช้โดยอิงตามความสนใจของคุณ เรามี Codelab เฉพาะเกี่ยวกับ Material อีกรายการหนึ่งที่คุณอาจสนใจ ซึ่งได้แก่ การสร้างการเปลี่ยนภาพที่สวยงามด้วยการเคลื่อนไหวของ Material สำหรับ Flutter

ฉันทำ Codelab นี้เสร็จได้ โดยใช้เวลาและลงแรงพอสมควร

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