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 framework ו-Material Library, כלומר אתם משתמשים בווידג'טים. :)
מה תפַתחו
ה-Codelab הזה ינחה אותך בתהליך הבנייה של כמה מעברים לאפליקציית אימייל לדוגמה של Flutter בשם Reply, בעזרת Drt, במטרה להדגים איך אפשר להשתמש במעברים מחבילת האנימציות כדי להתאים אישית את המראה והסגנון של האפליקציה.
הקוד ההתחלתי של אפליקציית 'תשובה' יסופק, ואתם תשלבו את המעברים הבאים ב-Material באפליקציה. את המעברים הבאים אפשר לראות בקובץ ה-GIF שהושלם על ידי Codelab:
- מעבר Container Transform מרשימת כתובות האימייל לדף הפרטים של האימייל
- מעבר של Container Transform מ-FAB לכתיבת דף אימייל
- מעבר ציר ה-Z המשותף מסמל החיפוש לדף של תצוגת החיפוש
- מעבר עמעום בין דפי תיבות דואר
- מעבר Fade Through בין הכתיבה לבין אישור FAB לתשובה.
- מעבר עמעום בין כותרת תיבת הדואר שנעלמת
- מעבר עמעום בין פעולות בסרגל התחתון של האפליקציה
מה צריך להכין
- ידע בסיסי בהתפתחות של Flutter וב-Dart
- עורך קוד
- אמולטור או מכשיר של Android/iOS
- הקוד לדוגמה (מידע נוסף מופיע בשלב הבא)
איזה דירוג מגיע לדעתך לרמת הניסיון שלך בפיתוח אפליקציות Flutter?
מה היית רוצה ללמוד מ-Codelab הזה?
2. הגדרת סביבת הפיתוח של Flutter
כדי להשלים את שיעור ה-Lab הזה אתם צריכים שתי תוכנות: Flutter SDK וכלי עריכה.
אפשר להריץ את Codelab באמצעות כל אחד מהמכשירים הבאים:
- מכשיר פיזי שמשמש ל-Android או ל-iOS שמחובר למחשב ומוגדר ל'מצב פיתוח'.
- הסימולטור של iOS (צריך להתקין כלים של Xcode).
- האמולטור של Android (נדרשת הגדרה ב-Android Studio).
- דפדפן (Chrome נדרש לניפוי באגים).
- בתור אפליקציית Windows , Linux או macOS למחשב. צריך לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, כדי לפתח אפליקציה למחשב של Windows, צריך לפתח את האפליקציה ב-Windows כדי לגשת לשרשרת ה-build המתאימה. יש דרישות ספציפיות למערכת ההפעלה שמפורטות בהרחבה בכתובת docs.flutter.dev/desktop.
3. הורדת האפליקציה לתחילת העבודה של Codelab
אפשרות 1: שכפול אפליקציית Codelab למתחילים מ-GitHub
כדי לשכפל את 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
פותחים את הפרויקט ומפעילים את האפליקציה
- פותחים את הפרויקט בכלי עריכה לבחירתכם.
- פועלים לפי ההוראות ל'הפעלת האפליקציה'. בקטע שנתחיל?: נסיעת מבחן בכלי העריכה שבחרתם.
הצלחת! קוד הסימן לתחילת הפעולה בדף הבית של התשובה אמור לפעול במכשיר או באמולטור שלכם. תיבת הדואר הנכנס אמורה להכיל רשימה של כתובות אימייל.
אופציונלי: האטה של האנימציות במכשיר
מאחר שה-Codelab הזה כולל מעברים מהירים אבל מלוטשים, כדאי להאט את האנימציות במכשיר כדי לראות פרטים מדויקים יותר לגבי המעברים במהלך ההטמעה. ניתן לעשות זאת באמצעות הגדרה בתוך האפליקציה. ניתן לגשת אליה בהקשה על סמל ההגדרות כשחלונית ההזזה התחתונה פתוחה. אל דאגה, השיטה הזו של האטת האנימציות במכשיר לא תשפיע על האנימציות במכשיר מחוץ לאפליקציית 'תשובה'.
אופציונלי: מצב כהה
אם הנושא הבהיר של 'תשובה' פוגע לעיניים, אל תסתכלו עוד. באפליקציה יש הגדרה שמאפשרת לשנות את עיצוב האפליקציה למצב כהה כדי שיתאימו לעיניים שלך. כדי לגשת להגדרה הזו, מקישים על סמל ההגדרות כשחלונית ההזזה התחתונה פתוחה.
4. היכרות עם קוד האפליקציה לדוגמה
בואו נסתכל על הקוד. אנחנו מספקים אפליקציה שמשתמשת בחבילת האנימציות כדי לעבור בין מסכים שונים באפליקציה.
- דף הבית: מציג את תיבת הדואר שנבחרה
- InboxPage: מציג רשימה של כתובות אימייל
- MailPreviewCard: הצגת תצוגה מקדימה של הודעת אימייל
- MailViewPage: מציג הודעת אימייל מלאה אחת.
- ComposePage: מאפשר לכתוב הודעת אימייל חדשה
- דף החיפוש: מציג תצוגה של חיפוש
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();
},
),
);
},
כך אפשר לנווט לדף של כתיבת האימייל, בלי מעבר מותאם אישית. במהלך ה-Codelab הזה, תתנסו בקוד התשובה כדי להגדיר מעברי 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 לאימייל חדש שהמשתמש ייכתב. קודם כול, מריצים מחדש את האפליקציה ולוחצים על 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.
לפני שמוסיפים קוד כלשהו, כדאי לנסות להפעיל את האפליקציה, להקיש על הודעת אימייל ולפתוח את תצוגת האימייל. סמל ה-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,
),
);
...
אנחנו מעניקים fillColor
שקוף ב_FadeThroughTransitionSwitcher
שלנו, כך שאין רקע בין הרכיבים במהלך המעבר. אנחנו גם יוצרים UniqueKey
ומקצים אותו לאחד מהסמלים.
בשלב הזה, צריכה להיות לכם לחצן FAB מונפש לפי הקשר. מעבר לתצוגת אימייל גורמת לסמל FAB הישן להתעמעם ולהקטין אותו, בזמן שהסמל החדש עמעום ומתפתח.
אחרי
10. הוספת מעבר עמעום בין כותרת תיבת הדואר שנעלמת
בשלב הזה, נוסיף עמעום באמצעות מעבר, כדי לעמעם הכותרת של תיבת הדואר בין מצב גלוי ובלתי נראה בתצוגת אימייל. מכיוון שאנחנו לא רוצים להדגיש קשר מרחבי או היררכי, נשתמש בהדגשה כדי לבצע פעולת החלפה פשוטה. בין הווידג'ט Text
שכולל את הכותרת של תיבת הדואר, לבין SizedBox
ריק.
לפני שמוסיפים קוד כלשהו, כדאי לנסות להפעיל את האפליקציה, להקיש על הודעת אימייל ולפתוח את תצוגת האימייל. הכותרת של תיבת הדואר אמורה להיעלם ללא מעבר.
לפני
שאר העבודה של ה-Codelab הזה תהיה מהירה, כי כבר עשינו את רוב העבודה ב_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 שורות של קוד Drt, ועוזרת לכם ליצור מעברים יפים באפליקציה קיימת שעומדת בהנחיות של Material Design, וגם מראה ומתנהג באופן עקבי בכל המכשירים.
השלבים הבאים
לקבלת מידע נוסף על מערכת התנועה של Material, כדאי לעיין בהנחיות ובתיעוד המלא למפתחים, ולנסות להוסיף מעברי Material לאפליקציה!
תודה שניסית תנועה בעיצוב חדשני. אנחנו מקווים שנהניתם מה-Codelab הזה!
הצלחתי להשלים את ה-Codelab הזה תוך השקעה של זמן ומאמץ סבירים
אני רוצה להמשיך להשתמש בעתיד במערכת התנועה מסוג Material
לעיון ב-Flutter Gallery
להדגמות נוספות על אופן השימוש בווידג'טים של ספריית Material Flutter ושל Flutter, הקפידו לבקר בFlutter Gallery. |