‫MDC-104 Flutter: عناصر پیشرفته Material

۱. مقدمه

logo_components_color_2x_web_96dp.png

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

در codelab‏ MDC-103، رنگ، ارتفاع، تایپوگرافی، و شکل «عناصر Material» ‏ (MDC) را برای سبک‌دهی به برنامه‌تان سفارشی‌سازی کردید.

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

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

آنچه خواهید ساخت

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

  • شکل‌ها
  • حرکت
  • ابزاره‌های Flutter (که در codelabs قبلی استفاده کرده‌اید)

Android

iOS

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

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

منوِ فهرست‌کننده ۴ دسته

منوِ فهرست‌کننده ۴ دسته

زیرسیستم‌ها و عناصر Material Flutter در این codelab

  • شکل‌ها

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

مبتدی متوسط ماهر

۲. راه‌اندازی محیط توسعه Flutter

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

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

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

۳. بارگیری برنامه آغازگر 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 را از codelabs قبلی در دستگاهتان ببینید.

Android

iOS

صفحه ورود به سیستم معبد

صفحه ورود به سیستم Shrine

۴. منو افزودن پس‌زمینه

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

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

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

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

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

افزودن ابزاره «پساویز»

ابزاره‌ای به‌نام پساویز ایجاد کنید که شامل frontLayer و backLayer باشد.

backLayer شامل منویی است که به شما امکان می‌دهد دسته‌ای را برای فیلتر کردن فهرست انتخاب کنید (currentCategory). ازآنجایی‌که می‌خواهیم انتخاب منو ماندگار باشد، Backdrop را به یک ابزارک حالت‌دار تبدیل می‌کنیم.

افزودن فایل جدید به /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 یک Stack است. فرزندان «پشته» می‌توانند هم‌پوشانی داشته باشند. اندازه و مکان هر فرزند نسبت‌به والد «پشته» مشخص می‌شود.

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

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

Android

iOS

صفحه محصول Shrine با پس‌زمینه صورتی

صفحه محصول زیارتگاه با پس‌زمینه صورتی

‫backLayer ناحیه صورتی را در لایه جدیدی در پشت صفحه اصلی frontLayer نشان می‌دهد.

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

92ed338a15a074bd.png

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

۵. افزودن شکل

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

«طراحی مواد» به این نوع سفارشی‌سازی به‌عنوان شکل اشاره می‌کند. سطوح 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 با شکل سفارشی

به سطح اصلی 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

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

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

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

‫۶. افزودن حرکت

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

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

در بالای 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 را بگیرد. همچنین، یک PositionedTransition اضافه کنید که یک RelativeRectTween Animation را می‌گیرد:

  // 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 ابزارک ویژه‌ای است که بازخوانی سازنده آن محدودیت‌های اندازه را ارائه می‌دهد.

در تابع build()، نماد منو اصلی را در نوار برنامه به IconButton تبدیل کنید و از آن برای تغییر رؤیت‌پذیری لایه جلو هنگام تک‌ضرب زدن روی دکمه استفاده کنید.

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

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

Android

iOS

منو «زیارتگاه» خالی با دو خطا

منو «زیارتگاه» خالی با دو خطا

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

ستون‌های محصول را در ListView بپیچید

در supplemental/product_columns.dart، «ستون» در OneProductCardColumn را با «فهرست‌واره» جایگزین کنید:

class OneProductCardColumn extends StatelessWidget {
  const OneProductCardColumn({required this.product, Key? key}) : super(key: key);

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        ConstrainedBox(
          constraints: const BoxConstraints(
            maxWidth: 550,
          ),
          child: ProductCard(
            product: product,
          ),
        ),
        const SizedBox(
          height: 40.0,
        ),

      ],
    );
  }
}

این ستون شامل MainAxisAlignment.end است. برای شروع چیدمان از پایین، reverse: true را علامت بزنید. ترتیب فرزندان برای جبران تغییر معکوس می‌شود.

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

Android

iOS

منوِ «معبد» خالی با یک خطا

منوِ «معبد» خالی با یک خطا

هشدار سرریز خاکستری در OneProductCardColumn برداشته شد! اکنون بیایید دیگری را درست کنیم.

در supplemental/product_columns.dart، نحوه محاسبه imageAspectRatio را تغییر دهید و «ستون» را در TwoProductCardColumn با «فهرست‌واره» جایگزین کنید:

      // 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

منو «معبد» خالی است

منو معبد خالی

سرریز دیگری وجود ندارد.

‫۷. افزودن منو در لایه پشت

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

افزودن منو

منو را به لایه جلو و دکمه‌های تعاملی را به لایه پشت اضافه کنید.

فایل جدیدی به‌نام lib/category_menu_page.dart ایجاد کنید:

import 'package:flutter/material.dart';

import 'colors.dart';
import 'model/product.dart';

class CategoryMenuPage extends StatelessWidget {
  final Category currentCatego<ry;
  fi>nal ValueChangedCategory onC<ategoryT>ap;
  final ListCategory _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);

    r>eturn 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: _ca>tegories
            .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»: «بازسازی > تغییر نام» را انتخاب کنید
  8. کد VS: «تغییر نام نماد» را انتخاب کنید
  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 co>ntext) = Backdrop(
              // TODO: Make currentCategory field take _currentCategory (104)
              currentCategory: _currentCategory,
              // TODO: Pass _currentCategory for frontLayer (104)
              frontLayer: HomePage(),
              // TODO: Change backLayer field value to CategoryMenuPage (104)
              backLayer: CategoryMenuPage(
                currentCategory: _currentCategory,
                onCategoryTap: _onCategoryTap,
              ),
              frontTitle: const Text('SHRINE'),
              backTitle: const Text('MENU'),
            ),

مجدد بار کنید و روی دکمه «منو» تک‌ضرب بزنید.

Android

iOS

منو زیارتگاه با ۴ دسته

منوِ زیارتگاه با ۴ دسته

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

در home.dart، متغیری برای «دسته» اضافه کنید و آن را به AsymmetricView ارسال کنید.

import 'package:flutter/material.dart';

import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';

class HomePage extends StatelessWidget {
  // TODO: Add a variable for Category (104)
  final Category category;

  const HomePage({this.category = Category.all, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: Pass Category variable to AsymmetricView (104)
    return AsymmetricView(
      products: ProductsRepository.loadProducts(category),
    );
  }
}

در app.dart، _currentCategory را برای frontLayer رد کنید:.

// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),

مجدد بار کنید. روی دکمه منو در شبیه‌ساز تک‌ضرب بزنید و «دسته» را انتخاب کنید.

Android

iOS

صفحه محصول فیلترشده Shrine

صفحه محصول فیلترشده معبد

فیلتر شدند!

بستن لایه جلویی پس‌از انتخاب منو

در 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;

سپس یک «ژست‌شناس» به فرزند _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,
            ),
          ),

دوباره بار کنید و روی بالای لایه جلو ضربه بزنید. هر بار که روی بالای لایه جلو ضربه می‌زنید، لایه باید باز و بسته شود.

‫۸. افزودن نماد نمانام‌دار

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

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

Android

iOS

صفحه محصول Shrine با نماد نمانام‌دار

صفحه محصول 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(
                translatio<n: Twe>enOffset(
                  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(
                tr<anslat>ion: TweenOffset(
                  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(
                tr<anslat>ion: TweenOffset(
                  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 co>ntext) = LoginPage()),
            );
          },
        ),
        IconButton(
          icon: const Icon(
            Icons.tune,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildCo>ntext context) = LoginPage()),
            );
          },
        ),

اگر دوباره بار کنید، خطا دریافت خواهید کرد. برای رفع خطا، login.dart را وارد کنید:

import 'login.dart';

برنامه را مجدداً بار کنید و روی دکمه‌های جستجو یا تنظیم ضربه بزنید تا به صفحه ورود به سیستم برگردید.

‫۹. تبریک!

طی این چهار codelab، یاد گرفتید چگونه از «عناصر Material» برای ساختن تجربه‌های کاربری منحصربه‌فرد و شیک که شخصیت و سبک نمانام را بیان می‌کنند استفاده کنید.

مراحل بعدی

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

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

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

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

کاملاً موافقم موافقم نه موافقم نه مخالف مخالفم کاملاً مخالفم

مایلم در آینده به استفاده از «عناصر Material» ادامه دهم

کاملاً موافق موافق نه موافق نه مخالف مخالف کاملاً مخالف