מדוע גלילה גורמת לגמגום במסכים עם קצב רענון גבוה?
יש לך עיצוב יפהפה עם כותרת מתקפלת – תמונה גדולה, כותרת בולטת. אבל כשאתה בודק במכשיר 120Hz, האנימציה מגמגמת. אתה מנסה layoutIfNeeded() בתוך Animator, אבל זה מחמיר את המצב. סרגל הכלים קופץ, הכותרת דוהה בצורה לא יציבה, ואפקט הפרלקס מרגיש איטי. נתקלנו בבעיה הזו עשרות פעמים ביותר מ-50 פרויקטים ופיתחנו פתרון אמין. אם אתה צריך ליישם אנימציה כזו, צור איתנו קשר לייעוץ פרויקט.
מלכודות טכניות נפוצות
-
אילוץ פריסה בתוך delegate של גלילה: קריאה ל-
CADisplayLinkבתוךChoreographerאוCollapsingToolbarLayoutגורמת למעברי פריסה רקורסיביים ומפילה פריימים. - מעברים קשים בין מצבים: שינוי גובה הכותרת מ-250dp ל-56dp ללא אנימציית החלקה יוצר קפיצה נראית לעין.
- גלילת יתר ללא שיכוך: כשהמשתמש גולל מעבר לראש, הכותרת צריכה להימתח ולחזור בצורה חלקה, לא לקפוץ.
-
התעלמות מפרימיטיביות אנימציה של הפלטפורמה: באנדרואיד,
AppBarLayoutמבצע אנימציה לשינויים אוטומטית; דריסתם ידנית לעיתים קרובות שוברת את הפיזיקה המובנית.
סנכרון נכון נותן שיפור של 30% בחלקות בהשוואה לפתרונות טיפוסיים מ-Stack Overflow. הגישה שלנו משתמשת ב-<CoordinatorLayout> <AppBarLayout android:id="@+id/appBar" android:layout_height="250dp"> <CollapsingToolbarLayout app:layout_scrollFlags="scroll|exitUntilCollapsed" app:contentScrim="?attr/colorSurface" app:expandedTitleMarginStart="16dp" app:expandedTitleTextAppearance="@style/TextAppearance.App.HeadlineMedium" app:collapsedTitleTextAppearance="@style/TextAppearance.App.TitleMedium"> <ImageView android:layout_height="match_parent" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.5" /> <Toolbar android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" /> </CollapsingToolbarLayout> </AppBarLayout> <RecyclerView app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </CoordinatorLayout> (iOS) וב-app:layout_collapseMode="parallax" / "pin" (אנדרואיד) כדי לנעול את האנימציה לקצב רענון המסך, ומבטיחה 120 FPS.
איך אנו מיישמים אנימציות מתקפלות ללא גמגום
אנדרואיד: CollapsingToolbarLayout (הצהרתי)
<CoordinatorLayout>
<AppBarLayout
android:id="@+id/appBar"
android:layout_height="250dp">
<CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorSurface"
app:expandedTitleMarginStart="16dp"
app:expandedTitleTextAppearance="@style/TextAppearance.App.HeadlineMedium"
app:collapsedTitleTextAppearance="@style/TextAppearance.App.TitleMedium">
<ImageView
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.5" />
<Toolbar
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</CollapsingToolbarLayout>
</AppBarLayout>
<RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</CoordinatorLayout> -
app:layout_scrollFlags="scroll|exitUntilCollapsed"על ImageView יוצר אפקט פרלקס. -
contentScrimעל Toolbar שומר אותו קבוע כשמקופל. -
AppBarLayout.OnOffsetChangedListenerגרדיאנט מתעמעם מעל התמונה כשהמשתמש גולל.
הוסף אנימציות מותאמות אישית דרך appBarLayout.addOnOffsetChangedListener { appBar, offset -> val progress = (-offset).toFloat() / appBar.totalScrollRange.toFloat() // progress: 0f = развёрнуто, 1f = свёрнуто avatarView.alpha = 1f - (progress * 2).coerceIn(0f, 1f) subtitleView.scaleX = 1f - progress * 0.3f subtitleView.scaleY = subtitleView.scaleX } :
appBarLayout.addOnOffsetChangedListener { appBar, offset ->
val progress = (-offset).toFloat() / appBar.totalScrollRange.toFloat() // progress: 0f = развёрнуто, 1f = свёрнуто
avatarView.alpha = 1f - (progress * 2).coerceIn(0f, 1f)
subtitleView.scaleX = 1f - progress * 0.3f
subtitleView.scaleY = subtitleView.scaleX
}שימוש ב-LargeTopAppBar מקצר את זמן הפיתוח בחצי בהשוואה ל-TopAppBarDefaults.exitUntilCollapsedScrollBehavior() מותאם אישית.
Jetpack Compose: TopAppBarScrollBehavior
לכותרים סטנדרטיים של Material, השתמש ב-val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { LargeTopAppBar( title = { Text("Заголовок") }, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface, ) ) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { padding -> LazyColumn(contentPadding = padding) { ... } } עם התנהגות הגלילה המובנית:
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
topBar = {
LargeTopAppBar(
title = { Text("Заголовок") },
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface,
)
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { padding ->
LazyColumn(contentPadding = padding) {
// ...
}
}לכותרת מתקפלת מותאמת אישית עם תמונה (LargeTopAppBar לא תומך בתמונות), בנה משלך באמצעות NestedScrollConnection:
val toolbarHeightExpanded = 250.dp
val toolbarHeightCollapsed = 56.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeightExpanded.toPx() }
val toolbarOffset = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffset.value + delta
toolbarOffset.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
val progress = (-toolbarOffset.value / toolbarHeightPx).coerceIn(0f, 1f)
val currentHeight = lerp(toolbarHeightExpanded, toolbarHeightCollapsed, progress)val toolbarHeightExpanded = 250.dp val toolbarHeightCollapsed = 56.dp val toolbarHeightPx = with(LocalDensity.current) { toolbarHeightExpanded.toPx() } val toolbarOffset = remember { mutableStateOf(0f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.y val newOffset = toolbarOffset.value + delta toolbarOffset.value = newOffset.coerceIn(-toolbarHeightPx, 0f) return Offset.Zero } } } val progress = (-toolbarOffset.value / toolbarHeightPx).coerceIn(0f, 1f) val currentHeight = lerp(toolbarHeightExpanded, toolbarHeightCollapsed, progress) הוא המדד המרכזי: השתמש בו כדי לשלוט באלפא, בקנה מידה ובנראות של כל האלמנטים המונפשים.
iOS: UIScrollViewDelegate + Auto Layout
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
let maxOffset: CGFloat = 200 // высота развёрнутого header
if offset < 0 { // Overscroll вниз — растягиваем изображение
headerHeightConstraint.constant = 250 - offset
headerImageView.transform = .identity
} else {
let progress = min(offset / maxOffset, 1.0)
headerHeightConstraint.constant = max(250 - offset, 56)
// Fade out изображения
headerImageView.alpha = 1 - progress
// Title появляется в nav bar
navigationItem.title = progress > 0.9 ? screenTitle : ""
}
// Без layoutIfNeeded в animate — прямое изменение constraint, следующий layout pass подхватит
}ב-SwiftUI, השתמש ב-progress וב-scrollViewDidScroll כדי לעקוב אחר מיקום הגלילה ולהתאים את גובה הכותרת באופן דומה.
מקרה בוחן: השפעה בעולם האמיתי
בפרויקט עדכני לאפליקציה חברתית עם תעבורה גבוהה, היישום המקורי הפיל 15% מהפריימים במכשירי 120Hz. עיצבנו מחדש את אנימציית הכותרת באמצעות הטכניקות שלעיל, והשגנו 120 FPS יציב עם אפס פריימים שנפלו. האפליקציה הרגישה חלקה יותר באופן ניכר בבדיקות משתמשים.
הבטחת חלקות במסכי 120Hz
אנו מתחברים למחזור הרענון של המסך באמצעות func scrollViewDidScroll(_ scrollView: UIScrollView) { let offset = scrollView.contentOffset.y let maxOffset: CGFloat = 200 // высота развёрнутого header if offset < 0 { // Overscroll вниз — растягиваем изображение headerHeightConstraint.constant = 250 - offset headerImageView.transform = .identity } else { let progress = min(offset / maxOffset, 1.0) headerHeightConstraint.constant = max(250 - offset, 56) // Fade out изображения headerImageView.alpha = 1 - progress // Title появляется в nav bar navigationItem.title = progress > 0.9 ? screenTitle : "" } // Без layoutIfNeeded в animate — прямое изменение constraint, следующий layout pass подхватит } (iOS) ו-scrollViewDidScroll/CADisplayLink (אנדרואיד). זה מבטיח שכל פריים של אנימציה יוצג בדיוק בגבול ה-vsync. אנו גם מבצעים אינטרפולציות GPU קלות כדי להימנע מציור מחדש יקר של שכבות.
השוואת פלטפורמות
| פרמטר | iOS (UIKit) | אנדרואיד (CoordinatorLayout) | אנדרואיד (Compose) | SwiftUI |
|---|---|---|---|---|
| כלי עיקרי | scrollViewDidScroll + Auto Layout | CollapsingToolbarLayout + AppBarLayout | NestedScrollConnection | GeometryReader + @State |
| מורכבות | בינונית | נמוכה (הצהרתי) | גבוהה (מותאם אישית) | בינונית |
| שליטה באנימציה | מלאה | מוגבלת | מלאה | מלאה |
| ביצועים | גבוהים | גבוהים | גבוהים | גבוהים |
לפי הנחיות Material Design, CollapsingToolbarLayout היא הגישה המועדפת לתרחישים סטנדרטיים. עבור iOS, Apple HIG ממליצים להשתמש בכותרים גדולים או ביישומים מותאמים אישית דרך UIScrollView.
התהליך שלנו
- ניתוח – אנו בוחנים את ה-UI הנוכחי שלך ואת דרישות האנימציה. אנו מזהים מכשירי יעד, גרסאות מערכת הפעלה, וכל אילוצים מהנחיות בדיקת ה-App Store (סעיף 4.2/5.1) או מדיניות Play Store.
- עיצוב – אנו בוחרים את הגישה הטובה ביותר (רכיב סטנדרטי או מותאם אישית), מחשבים גיאומטריה, מקדם פרלקס והתנהגות גלילת יתר. נוצר אב טיפוס עם נתונים אמיתיים.
- יישום – אנו כותבים קוד ספציפי לפלטפורמה, ומטפלים בחתימת קוד, פרופילי provisioning, ProGuard, התראות push וקישורים עמוקים לפי הצורך. עבור רכישות בתוך האפליקציה, אנו משתמשים ב-StoreKit 2 וב-Billing 6.
- בדיקות – אנו בודקים על מכשירי 120Hz פיזיים, אמולטורים וסימולטורים. בדיקות רגרסיה אוטומטיות כלולות.
- פריסה – אנו מספקים הוראות אינטגרציה ל-CI/CD שלך ומכשירים את הצוות שלך.
מה כלול
- רכיב כותרת מתקפלת מוכן לשימוש עם אנימציה.
- קוד מקור עם הערות מובנות (Swift/Kotlin).
- תיעוד התאמה אישית (גבהים, צבעים, גופנים).
- דוגמת אינטגרציה עם ניווט, קישורים עמוקים ורכישות בתוך האפליקציה (אם נדרש).
- תמיכה וייעוץ לאחר ההשקה.
לוחות זמנים ותמחור
יישום סטנדרטי (באמצעות CollapsingToolbarLayout או LargeTopAppBar): כיום אחד. פתרון מותאם אישית עם פרלקס, מספר אלמנטים מונפשים ותמיכה חוצת פלטפורמות: 2–3 ימים. התמחור נקבע לאחר ניתוח הפרויקט הספציפי שלך. צור איתנו קשר להערכה והצעה.
למה לבחור בנו?
יש לנו ניסיון רב בפיתוח מובייל ואנימציות מותאמות אישית, עם יותר מ-50 פרויקטים מוצלחים. המהנדסים שלנו מחזיקים בהסמכות (Apple Certified iOS Developer, Google Associate Android Developer). אנו מבטיחים אנימציה חלקה בכל מכשירי היעד, כולל מסכי 120Hz. קבל ייעוץ לפרויקט שלך.







