MDC-104 Flutter: Material Advanced Components

۱. مقدمه

logo_components_color_2x_web_96dp.png

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

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

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

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

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

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

  • شکل
  • حرکت
  • ویجت‌های فلاتر (که در آزمایشگاه‌های کد قبلی استفاده کرده‌اید)

اندروید

آی‌او‌اس

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

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

فهرست منو ۴ دسته

فهرست منو ۴ دسته

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

  • شکل

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

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

۲. محیط توسعه فلاتر خود را تنظیم کنید

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

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

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

۳. اپلیکیشن شروع کدلب را دانلود کنید

ادامه از MDC-103؟

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

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

برنامه‌ی آغازین در دایرکتوری material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series قرار دارد.

... یا آن را از گیت‌هاب کلون کنید

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

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 قبلی را روی دستگاه خود ببینید.

اندروید

آی‌او‌اس

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

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

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

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

نوار برنامه خانه را حذف کنید

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

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

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

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

یک ویجت به نام Backdrop ایجاد کنید که شامل frontLayer و backLayer باشد.

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

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

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 با یک نوار برنامه درست مانند HomePage قبلی برمی‌گرداند. اما بدنه Scaffold یک Stack است. فرزندان یک Stack می‌توانند همپوشانی داشته باشند. اندازه و مکان هر فرزند نسبت به والد 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'),
),

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

اندروید

آی‌او‌اس

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

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

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

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

92ed338a15a074bd.png

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

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

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

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

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

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

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

اندروید

آی‌او‌اس

صفحه محصول حرم با شکل سفارشی

صفحه محصول حرم با شکل سفارشی

ما به سطح اصلی 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,
      ),
    ),
  );
}

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

اندروید

آی‌او‌اس

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

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

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

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

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

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

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

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

یک ویجت AnimationController به _BackdropState اضافه کنید، آن را در تابع initState() نمونه‌سازی کنید و آن را در تابع dispose() مربوط به state دور بریزید:

  // 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 قرار دهید. این ویجت وقتی لایه پشتی قابل مشاهده نیست، آیتم‌های منوی لایه پشتی را از درخت معنایی حذف می‌کند.

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

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

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

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

اندروید

آی‌او‌اس

منوی Shrine خالی با دو خطا

منوی Shrine خالی با دو خطا

لایه جلویی به سمت پایین حرکت می‌کند (می‌لغزد). اما اگر به پایین نگاه کنید، یک خطای قرمز و یک خطای سرریز وجود دارد. دلیل این امر این است که 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 را علامت بزنید. ترتیب فرزندان برای جبران تغییر معکوس می‌شود.

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

اندروید

آی‌او‌اس

منوی Shrine را با یک خطا خالی کنید

منوی Shrine را با یک خطا خالی کنید

هشدار خاکستری سرریز در 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 اضافه کردیم.

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

اندروید

آی‌او‌اس

منوی خالی معبد

منوی خالی معبد

دیگر خبری از سرریز شدن نیست.

۷. یک منو به لایه پشتی اضافه کنید

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

منو را اضافه کنید

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

یک فایل جدید به نام 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 را از حالت بدون وضعیت (stateless) به حالت با وضعیت (stateful) تبدیل کنید.

  1. ShrineApp.
  2. بر اساس IDE خود، اقدامات کد را نشان دهید:
  3. اندروید استودیو: دکمه‌های ⌥Enter (در مک) یا alt + enter را فشار دهید
  4. VS Code: دکمه‌های ⌘. (macOS) یا Ctrl+ را فشار دهید.
  5. «تبدیل به ویجت باوضعیت» را انتخاب کنید.
  6. کلاس ShrineAppState را به private (_ShrineAppState) تغییر دهید. روی ShrineAppState کلیک راست کنید و
  7. اندروید استودیو: Refactor > Rename را انتخاب کنید
  8. کد VS: انتخاب تغییر نام نماد
  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'),
            ),

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

اندروید

آی‌او‌اس

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

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

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

در 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),

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

اندروید

آی‌او‌اس

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

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

فیلتر شده‌اند!

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

در backdrop.dart ، یک override برای تابع 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 reload) انجام شود. روی آیکون منو ضربه بزنید و یک دسته بندی انتخاب کنید. منو باید به طور خودکار بسته شود و باید دسته بندی موارد انتخاب شده را ببینید. اکنون این قابلیت را به لایه جلویی نیز اضافه خواهید کرد.

لایه جلویی را تغییر دهید

در 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,
            ),
          ),

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

۸. یک آیکون برند اضافه کنید

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

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

اندروید

آی‌او‌اس

صفحه محصول 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(
                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 handler برای آیکون برند شده به _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 قرار گرفته تا فضای کافی برای حرکت افقی آیکون فراهم شود.

معماری «همه چیز یک ویجت است» در فلاتر، امکان تغییر طرح‌بندی پیش‌فرض 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';

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

۹. تبریک می‌گویم!

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

مراحل بعدی

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

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

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

من توانستم این آزمایشگاه کد را با مقدار قابل توجهی از زمان و تلاش تکمیل کنم.

کاملاً موافقم موافق خنثی مخالف کاملاً مخالفم

من دوست دارم در آینده به استفاده از کامپوننت‌های متریال ادامه دهم.

کاملاً موافقم موافق خنثی مخالف کاملاً مخالفم