1. מבוא
Material Design היא מערכת ליצירת מוצרים דיגיטליים נועזים ומרהיבים. שילוב של סגנון, מיתוג, אינטראקציה ותנועה בקבוצה עקבית של עקרונות ורכיבים מאפשר לצוותים של מוצרים לממש את מלוא הפוטנציאל שלהם בתכנון.
Material Components (MDC) עוזרים למפתחים להטמיע את Material Design. MDC נוצר על ידי צוות של מהנדסים ומעצבי חוויית משתמש ב-Google, והוא כולל עשרות רכיבי ממשק משתמש יפים ופונקציונליים. הוא זמין ל-Android, ל-iOS, לאינטרנט ול-Flutter.material.io/develop |
מהי מערכת התנועה של Material ל-Flutter?
מערכת התנועה Material של Flutter היא קבוצה של דפוסי מעבר בתוך חבילת האנימציות, שיכולות לעזור למשתמשים להבין אפליקציה ולנווט בה, כפי שמתואר בהנחיות של עיצוב Material Design.
ארבעת הדפוסים העיקריים של מעבר Material הם:
- טרנספורמציה של קונטיינר: מעברים בין רכיבי ממשק משתמש שמכילים קונטיינר. הדפוס הזה יוצר חיבור גלוי בין שני רכיבי ממשק משתמש נפרדים על ידי טרנספורמציה חלקה של רכיב אחד לרכיב אחר.
- ציר משותף: מעבר בין רכיבי ממשק משתמש שיש להם קשר מרחבי או ניווט. המערכת משתמשת בטרנספורמציה משותפת על ציר ה-x, ה-y או ה-z כדי לחזק את הקשר בין הרכיבים.
- עמעום הדרגתי: מעבר בין רכיבי ממשק משתמש שאין להם קשר חזק זה לזה. נעשה שימוש בתהליך עמעום הדרגתי ברצף עם קנה מידה של הרכיב הנכנס.
- התכהה: משמש לרכיבי ממשק משתמש שנכנסים ממסגרת גבולות המסך או נכנסים אליהם.
חבילת האנימציות מציעה ווידג'טים של מעבר לתבניות האלה, שבנויים על ספריית האנימציות של Flutter (flutter/animation.dart
) וספריית החומרים של Flutter (flutter/material.dart
):
ב-Codelab הזה תשתמשו במעברי Material המבוססים על המסגרת של Flutter ו-Material, משמעות הדבר היא שעוסקים בווידג'טים. :)
מה תפַתחו
בקודלאב הזה תלמדו איך ליצור כמה מעברים באפליקציית אימייל לדוגמה ב-Flutter שנקראת Reply, באמצעות Dart. כך תוכלו להבין איך להשתמש במעברים מחבילת האנימציות כדי להתאים אישית את המראה והתחושה של האפליקציה.
קוד ההתחלה לאפליקציית ה-Reply יסופק, וישלבו את המעברים הבאים ב-Material באפליקציה. את המעברים הבאים ניתן לראות בקובץ ה-GIF המלא של Codelab:
- מעבר של Container Transform מרשימת כתובות האימייל לדף הפרטים של האימייל
- מעבר של Container Transform מ-FAB לכתיבת דף אימייל
- מעבר ציר ה-Z המשותף מסמל החיפוש לדף הצפייה בחיפוש
- מעבר עמעום בין דפי תיבות דואר
- מעבר Fade Through בין הלחצן הגדול ליצירת הודעה לבין הלחצן הגדול להשבתה
- מעבר מסוג Fade Through בין כותרת של תיבת דואר שנעלמת
- מעבר מסוג Fade Through בין הפעולות בסרגל האפליקציות התחתון
מה צריך להכין
- ידע בסיסי בפיתוח Flutter וב-Dart
- עורך קוד
- מכשיר או אמולטור של Android/iOS
- קוד לדוגמה (ראו שלב הבא)
איזה דירוג מגיע לדעתך לרמת הניסיון שלך בפיתוח אפליקציות Flutter?
מה היית רוצה ללמוד מ-Codelab הזה?
2. הגדרת סביבת הפיתוח של Flutter
כדי להשלים את שיעור ה-Lab הזה אתם צריכים שתי תוכנות: Flutter SDK וכלי עריכה.
אפשר להריץ את Codelab באמצעות כל אחד מהמכשירים הבאים:
- מכשיר Android או iOS פיזי שמחובר למחשב ומוגדר למצב פיתוח.
- סימולטור iOS (נדרשת התקנה של כלי Xcode).
- Android Emulator (נדרשת הגדרה ב-Android Studio).
- דפדפן (Chrome נדרש לניפוי באגים).
- כאפליקציית מחשב ל-Windows, ל-Linux או ל-macOS. צריך לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, כדי לפתח אפליקציה למחשב של Windows, צריך לפתח את האפליקציה ב-Windows כדי לגשת לשרשרת ה-build המתאימה. יש דרישות ספציפיות למערכת ההפעלה שמפורטות בהרחבה בכתובת docs.flutter.dev/desktop.
3. הורדת האפליקציה למתחילים ב-Codelab
אפשרות 1: יצירת עותקים (cloning) של אפליקציית ה-codelab למתחילים מ-GitHub
כדי להעתיק (clone) את codelab הזה מ-GitHub, מריצים את הפקודות הבאות:
git clone https://github.com/material-components/material-components-flutter-motion-codelab.git cd material-components-flutter-motion-codelab
אפשרות 2: מורידים את קובץ ה-zip של אפליקציית codelab למתחילים
אפליקציית ההתחלה נמצאת בספרייה material-components-flutter-motion-codelab-starter
.
אימות יחסי התלות של פרויקטים
הפרויקט תלוי בחבילת האנימציות. בpubspec.yaml
, חשוב לשים לב שהקטע dependencies
כולל את הדברים הבאים:
animations: ^2.0.0
פתיחת הפרויקט והרצת האפליקציה
- פותחים את הפרויקט בכלי עריכה לבחירתכם.
- פועלים לפי ההוראות ל'הפעלת האפליקציה' בקטע תחילת העבודה: נסיעת מבחן בעורך שבחרתם.
הצלחת! קוד ההתחלה של דף הבית של Reply אמור לפעול במכשיר או במהדמ. אמורה להופיע תיבת הדואר הנכנס עם רשימה של הודעות אימייל.
אופציונלי: האטת האנימציות במכשיר
מאחר שה-Codelab הזה כולל מעברים מהירים אבל מלוטשים, כדאי להאט את האנימציות במכשיר כדי לראות פרטים מדויקים יותר לגבי המעברים במהלך ההטמעה. אפשר לעשות זאת באמצעות הגדרה באפליקציה, שאפשר לגשת אליה בהקשה על סמל ההגדרות כשהמסנן התחתון פתוח. אל דאגה, השיטה הזו להאטת האנימציות במכשיר לא תשפיע על האנימציות במכשיר מחוץ לאפליקציית Reply.
אופציונלי: מצב כהה
אם העיצוב הבהיר של Reply כואב לכם בעיניים, זה הפתרון בשבילכם. יש הגדרה מובנית באפליקציה שמאפשרת לשנות את העיצוב שלה למצב כהה, כדי שיהיה נוח יותר לעיניים. כדי לגשת להגדרה הזו, מקישים על סמל ההגדרות כשהמסנן התחתון פתוח.
4. מעיינים בקוד לדוגמה של האפליקציה
נסתכל על הקוד. סיפקנו אפליקציה שמשתמשת בחבילת האנימציות כדי לעבור בין מסכים שונים באפליקציה.
- HomePage: הצגת תיבת הדואר הנבחרת
- InboxPage: מציג רשימה של הודעות אימייל
- MailPreviewCard: הצגת תצוגה מקדימה של הודעת אימייל
- MailViewPage: מציג הודעת אימייל מלאה אחת.
- ComposePage: מאפשר לכתוב הודעת אימייל חדשה
- SearchPage: הצגת תצוגת חיפוש
router.dart
קודם כול, כדי להבין איך מוגדרת הניווט ברמה הבסיסית של האפליקציה, פותחים את הקובץ router.dart
בספרייה lib
:
class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
ReplyRouterDelegate({required this.replyState})
: navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
replyState.addListener(() {
notifyListeners();
});
}
@override
final GlobalKey<NavigatorState> navigatorKey;
RouterProvider replyState;
@override
void dispose() {
replyState.removeListener(notifyListeners);
super.dispose();
}
@override
ReplyRoutePath get currentConfiguration => replyState.routePath!;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<RouterProvider>.value(value: replyState),
],
child: Selector<RouterProvider, ReplyRoutePath?>(
selector: (context, routerProvider) => routerProvider.routePath,
builder: (context, routePath, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const CustomTransitionPage(
transitionKey: ValueKey('Home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const CustomTransitionPage(
transitionKey: ValueKey('Search'),
screen: SearchPage(),
),
],
);
},
),
);
}
bool _handlePopPage(Route<dynamic> route, dynamic result) {
// _handlePopPage should not be called on the home page because the
// PopNavigatorRouterDelegateMixin will bubble up the pop to the
// SystemNavigator if there is only one route in the navigator.
assert(route.willHandlePopInternally ||
replyState.routePath is ReplySearchPath);
final bool didPop = route.didPop(result);
if (didPop) replyState.routePath = const ReplyHomePath();
return didPop;
}
@override
Future<void> setNewRoutePath(ReplyRoutePath configuration) {
replyState.routePath = configuration;
return SynchronousFuture<void>(null);
}
}
זהו כלי הניווט הבסיסי שלנו, והוא מטפל במסכי האפליקציה שמציגים את כל התוכן של אזור העריכה, למשל HomePage
ו-SearchPage
. היא מאזינה למצב של האפליקציה כדי לבדוק אם הגדרנו את המסלול אל ReplySearchPath
. אם עשינו את זה, אנחנו בונים מחדש את הניווט עם SearchPage
בחלק העליון של הערימה. שימו לב שהמסכים שלנו תחומים בתוך CustomTransitionPage
ולא מוגדרים מעברים. זוהי דרך אחת לנווט בין מסכים ללא מעבר מותאם אישית.
home.dart
הגדרנו את המסלול אל ReplySearchPath
במצב האפליקציה שלנו על ידי ביצוע הפעולות הבאות בתוך _BottomAppBarActionItems
בhome.dart
:
Align(
alignment: AlignmentDirectional.bottomEnd,
child: IconButton(
icon: const Icon(Icons.search),
color: ReplyColors.white50,
onPressed: () {
Provider.of<RouterProvider>(
context,
listen: false,
).routePath = const ReplySearchPath();
},
),
);
בפרמטר onPressed
, אנחנו ניגשים ל-RouterProvider
שלנו ומגדירים את ה-routePath
שלו ל-ReplySearchPath
. RouterProvider
עוקב אחרי המצב של הניווט ברמה הבסיסית.
mail_view_router.dart
עכשיו נראה איך מוגדר הניווט הפנימי של האפליקציה, ואפשר לפתוח את mail_view_router.dart
בספרייה lib
. תראו ניווט דומה לזה שלמעלה:
class MailViewRouterDelegate extends RouterDelegate<void>
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
MailViewRouterDelegate({required this.drawerController});
final AnimationController drawerController;
@override
Widget build(BuildContext context) {
bool _handlePopPage(Route<dynamic> route, dynamic result) {
return false;
}
return Selector<EmailStore, String>(
selector: (context, emailStore) => emailStore.currentlySelectedInbox,
builder: (context, currentlySelectedInbox, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Fade through transition between mailbox pages (Motion)
CustomTransitionPage(
transitionKey: ValueKey(currentlySelectedInbox),
screen: InboxPage(
destination: currentlySelectedInbox,
),
)
],
);
},
);
}
...
}
זהו כלי הניווט הפנימי שלנו. הוא מטפל במסכים הפנימיים של האפליקציה שלנו שצורכים רק את גוף הקנבס, למשל InboxPage
. ב-InboxPage
מוצגת רשימת הודעות אימייל בהתאם לתיבת הדואר הנוכחית במצב של האפליקציה. כלי הניווט נוצר מחדש עם ערך InboxPage
הנכון מעל המקבץ, בכל פעם שמתרחש שינוי בנכס currentlySelectedInbox
במצב של האפליקציה שלנו.
home.dart
כדי להגדיר את תיבת הדואר הנוכחית במצב של האפליקציה, מבצעים את הפעולות הבאות ב-_HomePageState
ב-home.dart
:
void _onDestinationSelected(String destination) {
var emailStore = Provider.of<EmailStore>(
context,
listen: false,
);
if (emailStore.onMailView) {
emailStore.currentlySelectedEmailId = -1;
}
if (emailStore.currentlySelectedInbox != destination) {
emailStore.currentlySelectedInbox = destination;
}
setState(() {});
}
בפונקציה _onDestinationSelected
, אנחנו ניגשים ל-EmailStore
ומגדירים את currentlySelectedInbox
שלו ליעד שנבחר. EmailStore
שלנו עוקב אחרי מצב המנווטים הפנימיים שלנו.
home.dart
לסיום, כדי לראות דוגמה לשימוש בניתוב ניווט, פותחים את הקובץ home.dart
בספרייה lib
. מאתרים את הכיתה _ReplyFabState
בתוך המאפיין onTap
של הווידג'ט InkWell
. הקוד אמור להיראות כך:
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const ComposePage();
},
),
);
},
כך אפשר לנווט לדף של כתיבת האימייל, בלי מעבר מותאם אישית. במהלך סדנת הקוד הזו, נצלול לקוד של Reply כדי להגדיר מעברים של Material שעובדים בשילוב עם פעולות הניווט השונות באפליקציה.
עכשיו, אחרי שקראתם את הקוד לתחילת העבודה, ניישם את המעבר הראשון.
5. הוספת מעבר של Container Transform מרשימת כתובות האימייל לדף הפרטים של האימייל
כדי להתחיל, צריך להוסיף מעבר כשלוחצים על הודעת אימייל. לשינוי הניווט הזה, דפוס הטרנספורמציה של הקונטיינר מתאים במיוחד, כי הוא מיועד למעברים בין רכיבי ממשק משתמש שמכילים קונטיינר. הדפוס הזה יוצר חיבור גלוי בין שני רכיבים בממשק המשתמש.
לפני שמוסיפים קוד, כדאי לנסות להפעיל את אפליקציית 'תשובה' וללחוץ על הודעת אימייל. היא אמורה לבצע דילוג פשוט, כך שהמסך יוחלף ללא מעבר:
לפני
מתחילים בהוספת ייבוא לחבילת האנימציות בחלק העליון של mail_card_preview.dart
, כפי שמוצג בקטע הקוד הבא:
mail_card_preview.dart
import 'package:animations/animations.dart';
עכשיו, אחרי שסיימתם לייבא את חבילת האנימציות, אפשר להתחיל להוסיף מעברים יפים לאפליקציה. נתחיל ביצירת הכיתה StatelessWidget
שתכלול את הווידג'ט OpenContainer
.
ב-mail_card_preview.dart
, מוסיפים את קטע הקוד הבא אחרי הגדרת הכיתה של MailPreviewCard
:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
required this.id,
required this.email,
required this.closedChild,
});
final int id;
final Email email;
final Widget closedChild;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OpenContainer(
openBuilder: (context, closedContainer) {
return MailViewPage(id: id, email: email);
},
openColor: theme.cardColor,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0)),
),
closedElevation: 0,
closedColor: theme.cardColor,
closedBuilder: (context, openContainer) {
return InkWell(
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).currentlySelectedEmailId = id;
openContainer();
},
child: closedChild,
);
},
);
}
}
עכשיו נשתמש ב-wrapper החדש. בתוך הגדרת הכיתה MailPreviewCard
, נארוז את הווידג'ט Material
מהפונקציה build()
באמצעות _OpenContainerWrapper
החדש שלנו:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Material(
...
ל-_OpenContainerWrapper
שלנו יש ווידג'ט InkWell
, ומאפייני הצבע של ה-OpenContainer
מגדירים את הצבע של המארז שהוא מכיל. לכן, אנחנו יכולים להסיר את הווידג'טים Material ו-Inkwell. הקוד שייווצר ייראה כך:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Dismissible(
key: ObjectKey(email),
dismissThresholds: const {
DismissDirection.startToEnd: 0.8,
DismissDirection.endToStart: 0.4,
},
onDismissed: (direction) {
switch (direction) {
case DismissDirection.endToStart:
if (onStarredInbox) {
onStar();
}
break;
case DismissDirection.startToEnd:
onDelete();
break;
default:
}
},
background: _DismissibleContainer(
icon: 'twotone_delete',
backgroundColor: colorScheme.primary,
iconColor: ReplyColors.blue50,
alignment: Alignment.centerLeft,
padding: const EdgeInsetsDirectional.only(start: 20),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
if (onStarredInbox) {
return true;
}
onStar();
return false;
} else {
return true;
}
},
secondaryBackground: _DismissibleContainer(
icon: 'twotone_star',
backgroundColor: currentEmailStarred
? colorScheme.secondary
: theme.scaffoldBackgroundColor,
iconColor: currentEmailStarred
? colorScheme.onSecondary
: colorScheme.onBackground,
alignment: Alignment.centerRight,
padding: const EdgeInsetsDirectional.only(end: 20),
),
child: mailPreview,
),
);
בשלב הזה, צריכה להיות לכם טרנספורמציה של קונטיינר שפועל באופן מלא. לחיצה על הודעת אימייל מרחיבה את הפריט ברשימה למסך פרטים, בזמן שרשימה של הודעות האימייל מתכווצת. הקשה על מקש 'הקודם' מכווצת את מסך פרטי האימייל בחזרה לפריט ברשימה, תוך הגדלת קנה המידה ברשימת האימיילים.
אחרי
6. הוספת מעבר של Container Transform מ-FAB לכתיבת דף אימייל
נמשיך עם טרנספורמציית הקונטיינר ונוסיף מעבר מלחצן הפעולה הצף אל ComposePage
, שמרחיב את לחצן הפעולה הצף לכתובת אימייל חדשה שהמשתמש צריך לכתוב. קודם כול, מריצים מחדש את האפליקציה ולוחצים על FAB כדי לראות שאין מעבר כשמפעילים את מסך כתיבת האימייל.
לפני
האופן שבו נגדיר את המעבר הזה יהיה דומה מאוד לאופן שבו עשינו זאת בשלב האחרון, כי אנחנו משתמשים באותו סוג ווידג'ט, OpenContainer
.
ב-home.dart
, נייבא את package:animations/animations.dart
שבחלק העליון של הקובץ, ונשנה את השיטה _ReplyFabState
build()
. שנוסיף את הווידג'ט Material
שהוחזר עם הווידג'ט OpenContainer
:
home.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
openBuilder: (context, closedContainer) {
return const ComposePage();
},
openColor: theme.cardColor,
onClosed: (success) {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = false;
},
closedShape: circleFabBorder,
closedColor: theme.colorScheme.secondary,
closedElevation: 6,
closedBuilder: (context, openContainer) {
return Material(
color: theme.colorScheme.secondary,
...
בנוסף לפרמטרים ששימשו להגדרת הווידג'ט הקודם של OpenContainer
, עכשיו מוגדר גם onClosed
. onClosed
הוא ClosedCallback
, שמתבצעת אליו קריאה לאחר שהמסלול OpenContainer
קופץ או חוזר למצב סגור. הערך המוחזר של העסקה מועבר לפונקציה הזו כארגומנט. אנחנו משתמשים בCallback
הזה כדי להודיע לספק האפליקציה שלנו שיצאנו מהמסלול ComposePage
, כדי שהוא יוכל להודיע לכל המאזינים.
בדומה לשלב האחרון, נצטרך להסיר את הווידג'ט Material
מהווידג'ט שלנו, כי הווידג'ט OpenContainer
מטפל בצבע של הווידג'ט שה-closedBuilder
מחזיר באמצעות closedColor
. בנוסף, נסיר את הקריאה Navigator.push()
בתוך onTap
של הווידג'ט InkWell, ונחליף אותה ב-openContainer() Callback
שמתקבל מ-closedBuilder
של הווידג'ט OpenContainer
, כי עכשיו הווידג'ט OpenContainer
מטפל בחיבור שלו.
הקוד שתתקבל תיראה כך:
home.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
openBuilder: (context, closedContainer) {
return const ComposePage();
},
openColor: theme.cardColor,
onClosed: (success) {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = false;
},
closedShape: circleFabBorder,
closedColor: theme.colorScheme.secondary,
closedElevation: 6,
closedBuilder: (context, openContainer) {
return Tooltip(
message: tooltip,
child: InkWell(
customBorder: circleFabBorder,
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
openContainer();
},
child: SizedBox(
height: _mobileFabDimension,
width: _mobileFabDimension,
child: Center(
child: fabSwitcher,
),
),
),
);
},
);
עכשיו ננקה קוד ישן. מכיוון שווידג'ט OpenContainer
מטפל עכשיו בעדכון של ספק האפליקציה שלנו על כך שאנחנו לא נמצאים יותר ב-ComposePage
דרך onClosed ClosedCallback
, אנחנו יכולים להסיר את ההטמעה הקודמת שלנו ב-mail_view_router.dart
:
mail_view_router.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);
זהו זה השלב הזה! אמור להיות מעבר מ-FAB למסך הכתיבה שנראה כך:
אחרי
7. הוספת מעבר של ציר ה-Z המשותף מסמל החיפוש לדף תצוגת החיפוש
בשלב הזה, נוסיף מעבר מסמל החיפוש לתצוגת החיפוש במסך מלא. מכיוון שאין מאגר קבוע שמעורב בשינוי הניווט הזה, אנחנו יכולים להשתמש במעבר בציר Z משותף כדי לחזק את הקשר המרחבי בין שני המסכים ולציין מעבר לרמה אחת למעלה בהיררכיה של האפליקציה.
לפני שמוסיפים קוד נוסף, כדאי לנסות להריץ את האפליקציה ולהקיש על סמל החיפוש בפינה השמאלית התחתונה של המסך. הפעולה הזו אמורה להציג את מסך תצוגת החיפוש ללא מעבר.
לפני
כדי להתחיל, נעבור לקובץ router.dart
. אחרי הגדרת הכיתה ReplySearchPath
, מוסיפים את קטע הקוד הבא:
router.dart
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
class SharedAxisTransitionPageWrapper extends Page {
const SharedAxisTransitionPageWrapper(
{required this.screen, required this.transitionKey})
: super(key: transitionKey);
final Widget screen;
final ValueKey transitionKey;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
fillColor: Theme.of(context).cardColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.scaled,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return screen;
});
}
}
עכשיו נשתמש ב-SharedAxisTransitionPageWrapper
החדש כדי להשיג את המעבר הרצוי. בהגדרת הכיתה ReplyRouterDelegate
, מתחת לנכס pages
, נארוז את מסך החיפוש ב-SharedAxisTransitionPageWrapper
במקום ב-CustomTransitionPage
:
router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const CustomTransitionPage(
transitionKey: ValueKey('Home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('Search'),
screen: SearchPage(),
),
],
);
עכשיו מנסים להפעיל מחדש את האפליקציה.
הכול מתחיל להיראות מצוין! כשלוחצים על סמל החיפוש בסרגל התחתון של האפליקציה, מוצגים על ידי ציר משותף הגדלה של דף החיפוש. עם זאת, שימו לב איך דף הבית לא גדל בהתאם לעומס, ובמקום זאת נשאר סטטי בזמן שדף החיפוש מותאם לעומס. בנוסף, כשמקישים על לחצן החזרה, דף הבית לא משתנה כדי להיכנס לתצוגה. במקום זאת, הוא נשאר סטטי בזמן שדף החיפוש משתנה כדי לצאת מהתצוגה. כך שעדיין לא סיימנו.
כדי לפתור את שתי הבעיות, צריך גם לשלב את HomePage
עם SharedAxisTransitionWrapper
במקום בCustomTransitionPage
:
router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('search'),
screen: SearchPage(),
),
],
);
זהו! עכשיו אפשר לנסות להפעיל מחדש את האפליקציה ולהקיש על סמל החיפוש. מסך תצוגת הבית ומסך החיפוש אמורים להתעמעם ולהתקדם לאורך ציר ה-Z באופן עומק, וכך ליצור אפקט חלק בין שני המסכים.
אחרי
8. הוספת מעבר של 'העלמה' בין דפי תיבת הדואר
בשלב הזה נוסיף מעבר בין תיבות דואר שונות. מכיוון שאנחנו לא רוצים להדגיש קשר מרחבי או היררכי, נשתמש בהדגשה כדי לבצע החלפה פשוטה בין רשימות של כתובות אימייל.
לפני שמוסיפים קוד נוסף, כדאי לנסות להפעיל את האפליקציה, להקיש על הלוגו של 'תשובה' בסרגל האפליקציות התחתון ולעבור בין תיבות דואר. רשימת כתובות האימייל אמורה להשתנות ללא מעבר.
לפני
כדי להתחיל, ניגשים לקובץ mail_view_router.dart
שלנו. אחרי הגדרת הכיתה MailViewRouterDelegate
, מוסיפים את קטע הקוד הבא:
mail_view_router.dart
// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
const FadeThroughTransitionPageWrapper({
required this.mailbox,
required this.transitionKey,
}) : super(key: transitionKey);
final Widget mailbox;
final ValueKey transitionKey;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return mailbox;
});
}
}
בדומה לשלב האחרון, נשתמש ב-FadeThroughTransitionPageWrapper
החדש כדי להשיג את המעבר הרצוי. בתוך ההגדרה של הכיתה MailViewRouterDelegate
, בנכס pages
, במקום לעטוף את מסך תיבת הדואר שלנו באמצעות CustomTransitionPage
, צריך להשתמש בשדה FadeThroughTransitionPageWrapper
במקום זאת:
mail_view_router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Fade through transition between mailbox pages (Motion)
FadeThroughTransitionPageWrapper(
mailbox: InboxPage(destination: currentlySelectedInbox),
transitionKey: ValueKey(currentlySelectedInbox),
),
],
);
להפעיל מחדש את האפליקציה. כשפותחים את חלונית ההזזה לניווט התחתונה ומחליפים תיבות דואר, רשימת האימיילים הנוכחית אמורה להתעמעם ולהקטין את התצוגה בזמן שהרשימה החדשה נעלמת. איזה יופי!
אחרי
9. הוספת מעבר של עמעום הדרגתי בין 'אימייל חדש' ל'תשובה'
בשלב הזה, נוסיף מעבר בין סמלי FAB שונים. אנחנו לא רוצים להדגיש קשר מרחבי או היררכי, לכן נשתמש בהעברה בהדרגה כדי לבצע 'החלפה' פשוטה בין הסמלים בלחצן ההפעלה המהיר.
לפני שמוסיפים קוד כלשהו, כדאי לנסות להפעיל את האפליקציה, להקיש על הודעת אימייל ולפתוח את תצוגת האימייל. סמל ה-FAB צריך להשתנות ללא מעבר.
לפני
נעבוד ב-home.dart
במהלך שאר הקודלאב, כך שאין צורך להוסיף את הייבוא של חבילת האנימציות כי כבר עשינו זאת עבור home.dart
בשלב 2.
האופן שבו נגדיר את כמה מהמעברות הבאות יהיה דומה מאוד, כי כולם ישתמשו בכיתה לשימוש חוזר, _FadeThroughTransitionSwitcher
.
ב-home.dart
, מוסיפים את קטע הקוד הבא בקטע _ReplyFabState
:
home.dart
// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
const _FadeThroughTransitionSwitcher({
required this.fillColor,
required this.child,
});
final Widget child;
final Color fillColor;
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
fillColor: fillColor,
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
);
},
child: child,
);
}
}
עכשיו, ב-_ReplyFabState
, מחפשים את הווידג'ט fabSwitcher
. הסמל fabSwitcher
מחזיר סמל אחר, בהתאם לסטטוס של תצוגת האימייל. נסיים ב_FadeThroughTransitionSwitcher
:
home.dart
// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final circleFabBorder = const CircleBorder();
return Selector<EmailStore, bool>(
selector: (context, emailStore) => emailStore.onMailView,
builder: (context, onMailView, child) {
// TODO: Add Fade through transition between compose and reply FAB (Motion)
final fabSwitcher = _FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: onMailView
? Icon(
Icons.reply_all,
key: fabKey,
color: Colors.black,
)
: const Icon(
Icons.create,
color: Colors.black,
),
);
...
אנחנו נותנים ל-_FadeThroughTransitionSwitcher
fillColor
שקוף, כך שלא יהיה רקע בין הרכיבים במהלך המעבר. אנחנו גם יוצרים UniqueKey
ומקצים אותו לאחד מהסמלים.
בשלב הזה, אמורה להופיע לחצן FAB מותאם הקשר עם אנימציה מלאה. מעבר לתצוגת אימייל גורמת לסמל FAB הישן להתעמעם ולהקטין אותו, בזמן שהסמל החדש מתעמעם ומתפתח.
אחרי
10. הוספת מעבר של דהייה בין שם תיבת הדואר שנעלם
בשלב הזה, נוסיף מעבר של דהייה כדי שהשם של תיבת הדואר יעבור בין מצב גלוי למצב מוסתר כשנמצאים בתצוגת אימייל. מכיוון שאנחנו לא רוצים להדגיש קשר מרחבי או היררכי, נשתמש בהעברה בהדרגה כדי לבצע 'החלפה' פשוטה בין הווידג'ט Text
שמקיף את שם תיבת הדואר לבין SizedBox
ריק.
לפני שמוסיפים קוד כלשהו, כדאי לנסות להפעיל את האפליקציה, להקיש על הודעת אימייל ולפתוח את תצוגת האימייל. הכותרת של תיבת הדואר אמורה להיעלם ללא מעבר.
לפני
שאר הקודלאב הזה יהיה קצר, כי כבר ביצענו את רוב העבודה ב-_FadeThroughTransitionSwitcher
בשלב האחרון.
עכשיו נעבור לכיתה _AnimatedBottomAppBar
ב-home.dart
כדי להוסיף את המעבר. נשתמש שוב בכתובת _FadeThroughTransitionSwitcher
מהשלב האחרון שלנו, ונעטפים את התנאי של onMailView
המותנה, שתחזיר SizedBox
ריק, או כותרת של תיבת דואר שמתעמעמת בסנכרון עם חלונית ההזזה התחתונה:
home.dart
...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: onMailView
? const SizedBox(width: 48)
: FadeTransition(
opacity: fadeOut,
child: Selector<EmailStore, String>(
selector: (context, emailStore) =>
emailStore.currentlySelectedInbox,
builder: (
context,
currentlySelectedInbox,
child,
) {
return Text(
currentlySelectedInbox,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ReplyColors.white50,
),
);
},
),
),
),
זהו, סיימנו את השלב הזה.
מפעילים מחדש את האפליקציה. כשפותחים הודעת אימייל ומגיעים לתצוגת האימייל, כותרת תיבת הדואר בסרגל האפליקציות התחתון אמורה להתפוגג ולהתרחק. מדהים!
אחרי
11. הוספת מעבר של עמעום הדרגתי בין פעולות בסרגל האפליקציה התחתון
בשלב הזה, נוסיף עמעום באמצעות מעבר, כדי להפוך עמעום דרך הפעולות בסרגל התחתון של האפליקציה בהתאם להקשר של האפליקציות. מכיוון שאנחנו לא רוצים להדגיש קשר מרחבי או היררכי, נשתמש ב'דעיכה' כדי לבצע 'החלפה' פשוטה בין הפעולות בסרגל התחתון של האפליקציה כשהאפליקציה נמצאת בדף הבית, כשחלונית ההזזה התחתונה גלויה וכשאנחנו נמצאים בתצוגת האימייל.
לפני שמוסיפים קוד נוסף, כדאי לנסות להפעיל את האפליקציה, להקיש על הודעת אימייל ולפתוח את תצוגת האימייל. אפשר גם לנסות להקיש על הלוגו של 'תשובה'. הפעולות בסרגל האפליקציה התחתון אמורות להשתנות ללא מעבר.
לפני
בדומה לשלב האחרון, נשתמש שוב ב-_FadeThroughTransitionSwitcher
. כדי להשיג את המעבר הרצוי, עוברים להגדרת המחלקה _BottomAppBarActionItems
ועוטפים את הווידג'ט החוזר של הפונקציה build()
באמצעות _FadeThroughTransitionSwitcher
:
home.dart
// TODO: Add Fade through transition between bottom app bar actions (Motion)
return _FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: drawerVisible
? Align(
key: UniqueKey(),
alignment: AlignmentDirectional.bottomEnd,
child: IconButton(
icon: const Icon(Icons.settings),
color: ReplyColors.white50,
onPressed: () async {
drawerController.reverse();
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: modalBorder,
),
builder: (context) => const SettingsBottomSheet(),
);
},
),
)
: onMailView
...
רוצה לנסות? כשפותחים אימייל ומגיעים לתצוגת האימייל, הפעולות הישנות בסרגל האפליקציות התחתון אמורות להתפוגג ולהקטין, בזמן שהפעולות החדשות אמורות להתפוגג ולהגדיל. כל הכבוד!
אחרי
12. מעולה!
בעזרת חבילת האנימציות, אפשר ליצור מעברים יפים באפליקציה קיימת באמצעות פחות מ-100 שורות קוד ב-Dart, כך שהיא תתאים להנחיות של Material Design ותהיה עקבית במראה ובהתנהגות בכל המכשירים.
השלבים הבאים
למידע נוסף על מערכת התנועה של Material, כדאי לעיין בהנחיות ובמסמכי התיעוד המלאים למפתחים, ולנסות להוסיף כמה מעברים של Material לאפליקציה.
תודה שניסית את התכונה 'תנועה ב-Material'. אנחנו מקווים שנהניתם מה-Codelab הזה!
הצלחתי להשלים את הקודלהב הזה בזמן ובמאמץ סבירים
אני רוצה להמשיך להשתמש בעתיד במערכת התנועה Material
לגלריה של Flutter
להדגמות נוספות על אופן השימוש בווידג'טים של ספריית Material Flutter ושל Flutter, הקפידו לבקר בFlutter Gallery. |