MDC-104 Flutter: Material Advanced Components

1. مقدمه

logo_components_color_2x_web_96dp.png

Material Components (MDC) به توسعه دهندگان کمک می کند طراحی مواد را پیاده سازی کنند. MDC که توسط تیمی از مهندسان و طراحان UX در Google ایجاد شده است، دارای ده‌ها مؤلفه رابط کاربری زیبا و کاربردی است و برای Android، iOS، وب و Flutter.material.io/develop در دسترس است.

در Codelab MDC-103 ، رنگ، ارتفاع، تایپوگرافی، و شکل اجزای مواد (MDC) را برای استایل دادن به برنامه خود سفارشی کردید.

یک جزء در سیستم طراحی متریال مجموعه ای از وظایف از پیش تعریف شده را انجام می دهد و ویژگی های خاصی مانند یک دکمه دارد. با این حال، یک دکمه بیشتر از یک روش ساده برای کاربر برای انجام یک عمل است، بلکه بیانی بصری از شکل، اندازه و رنگ است که به کاربر اجازه می‌دهد بفهمد که تعاملی است و با لمس یا کلیک چیزی اتفاق می‌افتد.

دستورالعمل‌های طراحی متریال اجزاء را از دیدگاه طراح توصیف می‌کنند. آنها طیف گسترده ای از عملکردهای اساسی موجود در پلتفرم ها و عناصر آناتومیکی که هر جزء را تشکیل می دهند را توصیف می کنند. به عنوان مثال، یک پس‌زمینه حاوی یک لایه پشتی و محتوای آن، لایه جلویی و محتوای آن، قوانین حرکت و گزینه‌های نمایش است. هر یک از این مؤلفه ها را می توان برای نیازها، موارد استفاده و محتوای هر برنامه سفارشی کرد.

چیزی که خواهی ساخت

در این کد لبه، رابط کاربری برنامه Shrine را به یک ارائه دو سطحی به نام «پس‌زمینه» تغییر می‌دهید. پس زمینه شامل منویی است که دسته بندی های قابل انتخابی را فهرست می کند که برای فیلتر کردن محصولات نشان داده شده در شبکه نامتقارن استفاده می شوند. در این کد لبه از موارد زیر استفاده خواهید کرد:

  • شکل
  • حرکت
  • ویجت های فلاتر (که در کدهای قبلی استفاده کرده اید)

اندروید

iOS

برنامه تجارت الکترونیک با مضمون صورتی و قهوه‌ای با نوار برنامه بالا و شبکه‌ای نامتقارن و قابل پیمایش افقی پر از محصولات

برنامه تجارت الکترونیک با مضمون صورتی و قهوه‌ای با نوار برنامه بالا و شبکه‌ای نامتقارن و قابل پیمایش افقی پر از محصولات

فهرست فهرست 4 دسته

فهرست فهرست 4 دسته

اجزاء و زیرسیستم های Flutter مواد در این آزمایشگاه کد

  • شکل

سطح تجربه خود را با توسعه فلاتر چگونه ارزیابی می کنید؟

تازه کار متوسط مسلط

2. محیط توسعه Flutter خود را تنظیم کنید

برای تکمیل این آزمایشگاه به دو نرم افزار نیاز دارید - Flutter SDK و یک ویرایشگر .

شما می توانید کدلب را با استفاده از هر یک از این دستگاه ها اجرا کنید:

  • یک دستگاه فیزیکی Android یا iOS که به رایانه شما متصل شده و روی حالت Developer تنظیم شده است.
  • شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد).
  • شبیه ساز اندروید (نیاز به نصب در Android Studio دارد).
  • یک مرورگر (Chrome برای اشکال زدایی لازم است).
  • به عنوان یک برنامه دسکتاپ Windows ، Linux ، یا macOS . شما باید روی پلتفرمی که قصد استقرار در آن را دارید توسعه دهید. بنابراین، اگر می خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید در ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به طور مفصل در docs.flutter.dev/desktop پوشش داده شده است.

3. برنامه استارتر Codelab را دانلود کنید

از MDC-103 ادامه می دهید؟

اگر MDC-103 را تکمیل کرده اید، کد شما باید برای این کد لبه آماده باشد. رفتن به مرحله: منوی پس زمینه را اضافه کنید .

از صفر شروع کنم؟

برنامه شروع در فهرست راهنمای 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. دستورالعمل‌های «اجرای برنامه» را در Get Started: Test Drive برای ویرایشگر انتخابی خود دنبال کنید.

موفقیت! شما باید صفحه ورود به حرم را از کد لبه های قبلی دستگاه خود ببینید.

اندروید

iOS

صفحه ورود به حرم

صفحه ورود به حرم

4. منوی پس زمینه را اضافه کنید

یک پس زمینه در پشت همه محتوا و اجزای دیگر ظاهر می شود. این از دو لایه تشکیل شده است: یک لایه پشتی (که اعمال و فیلترها را نمایش می دهد) و یک لایه جلو (که محتوا را نمایش می دهد). می توانید از یک پس زمینه برای نمایش اطلاعات و اقدامات تعاملی مانند پیمایش یا فیلترهای محتوا استفاده کنید.

نوار برنامه home را بردارید

ویجت HomePage محتوای لایه جلویی ما خواهد بود. در حال حاضر یک نوار برنامه دارد. نوار برنامه را به لایه پشتی منتقل می کنیم و صفحه اصلی فقط شامل AsymmetricView می شود.

در home.dart ، تابع build() را تغییر دهید تا فقط AsymmetricView را برگرداند:

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

ویجت Backdrop را اضافه کنید

ویجتی به نام 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 باشند و بنابراین نباید فراموش شوند.

در تعریف کلاس Backdrop، کلاس _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 را با یک نوار برنامه برمی گرداند، درست مانند صفحه اصلی. اما بدن Scaffold یک پشته است. فرزندان یک پشته می توانند همپوشانی داشته باشند. اندازه و مکان هر کودک نسبت به والدین پشته مشخص شده است.

اکنون یک نمونه Backdrop را به 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'),
),

پروژه خود را ذخیره کنید، باید ببینید که صفحه اصلی ما و همچنین نوار برنامه نمایش داده می شود:

اندروید

iOS

صفحه محصول حرم با زمینه صورتی

صفحه محصول حرم با زمینه صورتی

BackLayer ناحیه صورتی را در یک لایه جدید در پشت صفحه اصلی frontLayer نشان می دهد.

می‌توانید از Flutter Inspector برای تأیید اینکه Stack واقعاً یک کانتینر در پشت صفحه اصلی دارد استفاده کنید. باید شبیه این باشد:

92ed338a15a074bd.png

اکنون می توانید طراحی و محتوای لایه ها را تنظیم کنید.

5. یک شکل اضافه کنید

در این مرحله، به لایه جلویی استایل می دهید تا برشی در گوشه بالا سمت چپ اضافه کند.

طراحی متریال به این نوع سفارشی سازی به عنوان یک شکل اشاره می کند. سطوح مواد می توانند اشکال دلخواه داشته باشند. شکل ها به سطوح تاکید و سبک می بخشند و می توانند برای بیان برند استفاده شوند. اشکال مستطیلی معمولی را می توان با گوشه ها و لبه های منحنی یا زاویه دار و هر تعداد ضلع سفارشی کرد. آنها می توانند متقارن یا نامنظم باشند.

یک شکل به لایه جلویی اضافه کنید

لوگوی زاویه دار 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),
      ],
    );
  }

بارگذاری مجدد

اندروید

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

راه اندازی مجدد داغ. اکنون باید نوار برنامه رنگی جدید ظاهر شود.

اندروید

iOS

صفحه محصول حرم با نوار برنامه رنگی

صفحه محصول حرم با نوار برنامه رنگی

به دلیل این تغییر، کاربران می توانند ببینند که چیزی در پشت لایه سفید جلویی وجود دارد. بیایید حرکت را اضافه کنیم تا کاربران بتوانند لایه پشتی پس زمینه را ببینند.

6. حرکت را اضافه کنید

حرکت راهی برای زنده کردن اپلیکیشن شماست. می تواند بزرگ و دراماتیک، ظریف و مینیمال، یا هر جایی در این بین باشد. اما به یاد داشته باشید که نوع حرکتی که استفاده می کنید باید متناسب با موقعیت باشد. حرکتی که برای اعمال تکراری و منظم اعمال می‌شود، باید کوچک و ظریف باشد، به طوری که این اقدامات حواس کاربر را پرت نکنند یا زمان زیادی را به طور منظم نگیرند. اما موقعیت‌های مناسبی وجود دارد، مانند اولین باری که کاربر یک برنامه را باز می‌کند، که می‌تواند چشم نوازتر باشد، و برخی انیمیشن‌ها می‌توانند به آموزش کاربر در مورد نحوه استفاده از برنامه شما کمک کنند.

حرکت آشکار را به دکمه منو اضافه کنید

در بالای backdrop.dart ، خارج از محدوده هر کلاس یا تابع، یک ثابت اضافه کنید تا سرعتی را که می‌خواهیم انیمیشن ما داشته باشد را نشان دهد:

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

یک ویجت AnimationController را به _BackdropState اضافه کنید، آن را در تابع initState() نمونه سازی کنید و آن را در تابع state's 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);
  }

لایه پشتی را در یک ویجت 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 تغییر دهید. همچنین، یک PositionedTransition که یک انیمیشن 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() آیکون منوی اصلی در نوار برنامه را به یک IconButton تبدیل کنید و از آن برای تغییر نمای لایه جلویی هنگام ضربه زدن روی دکمه استفاده کنید.

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

دوباره بارگیری کنید سپس روی دکمه منو در شبیه ساز ضربه بزنید.

اندروید

iOS

منوی خالی حرم با دو خطا

منوی خالی حرم با دو خطا

لایه جلویی متحرک (اسلاید) به پایین است. اما اگر به پایین نگاه کنید، یک خطای قرمز و یک خطای سرریز وجود دارد. این به این دلیل است که AsymmetricView توسط این انیمیشن فشرده شده و کوچکتر می شود که به نوبه خود فضای کمتری به ستون ها می دهد. در نهایت، ستون ها نمی توانند خود را با فضای داده شده نشان دهند و منجر به خطا می شوند. اگر ستون ها را با ListViews جایگزین کنیم، اندازه ستون باید همانطور که متحرک هستند باقی بماند.

ستون های محصول را در 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 . ترتیب بچه ها برای جبران این تغییر معکوس می شود.

دوباره بارگیری کنید و روی دکمه منو ضربه بزنید.

اندروید

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 اضافه کردیم.

بارگذاری مجدد سپس روی دکمه منو ضربه بزنید.

اندروید

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: Refactor > Rename را انتخاب کنید
  8. VS Code: تغییر نام نماد را انتخاب کنید
  9. برای خصوصی کردن کلاس وارد _ShrineAppState شوید.

در app.dart ، یک متغیر به _ShrineAppState برای دسته انتخابی اضافه کنید و وقتی روی آن ضربه زدید، یک تماس پاسخ دهید:

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'),
            ),

دوباره بارگیری کنید و روی دکمه Menu ضربه بزنید.

اندروید

iOS

منوی حرم با 4 دسته

منوی حرم با 4 دسته

اگر روی یک گزینه منو ضربه بزنید، هیچ اتفاقی نمی افتد...هنوز. بیایید آن را درست کنیم.

در home.dart ، یک متغیر برای Category اضافه کنید و آن را به 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),

بارگذاری مجدد روی دکمه منو در شبیه ساز ضربه بزنید و یک دسته را انتخاب کنید.

اندروید

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. یک نماد مارک اضافه کنید

نماد نگاری مارک به نمادهای آشنا نیز گسترش می یابد. بیایید نماد آشکار را سفارشی کنیم و آن را با عنوان خود ادغام کنیم تا ظاهری منحصر به فرد و مارک دار داشته باشیم.

نماد دکمه منو را تغییر دهید

اندروید

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, یک میانبر از دو نماد انتهایی در نوار برنامه به صفحه ورود اضافه کنید: برچسب های معنایی نمادها را تغییر دهید تا هدف جدید آنها را منعکس کند.

        // 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. تبریک می گویم!

در طول این چهار آزمایشگاه، یاد گرفته‌اید که چگونه از Material Components برای ایجاد تجربیات کاربر منحصر به فرد و ظریفی استفاده کنید که شخصیت و سبک برند را بیان می‌کند.

مراحل بعدی

این کد لبه، MDC-104، این توالی از لبه های کد را تکمیل می کند. با مراجعه به کاتالوگ ابزارک های Material Components می توانید حتی اجزای بیشتری را در Material Flutter کاوش کنید.

برای یک هدف طولانی، سعی کنید نماد مارک دار را با یک AnimatedIcon جایگزین کنید که وقتی پس زمینه قابل مشاهده است بین دو نماد متحرک شود.

تعداد زیادی کدهای Flutter دیگر وجود دارد که می توانید بر اساس علایق خود امتحان کنید. ما یکی دیگر از کدهای مخصوص مواد داریم که ممکن است به آن علاقه مند باشید: ساختن انتقال های زیبا با حرکت مواد برای فلوتر .

من توانستم با صرف زمان و تلاش معقول این کد لبه را تکمیل کنم

کاملا موافقم موافقم خنثی مخالف به شدت مخالفم

من می‌خواهم در آینده از Material Component استفاده کنم

کاملا موافقم موافقم خنثی مخالف به شدت مخالفم