@nativescript-community/ui-appbar 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,825 @@
1
+ import { ActionBar, Application, Color, Page, Utils } from '@nativescript/core';
2
+ import { createNativeFormattedText } from '@nativescript-community/ui-core/formatted-text';
3
+ import { getNativeIcon } from '@nativescript-community/ui-menu';
4
+ import { AppBarBase, AppBarItem, AppPageOverlay, AppPageSearchContent, notifyBottomItemTap } from './index-common';
5
+ export { AppBarItem, AppPageOverlay, AppPageSearchContent };
6
+ const MATCH_PARENT = -1;
7
+ const WRAP_CONTENT = -2;
8
+ const COLLAPSE_MODE_PIN = 1;
9
+ function getActionBarSize(context) {
10
+ const value = new android.util.TypedValue();
11
+ if (context.getTheme().resolveAttribute(16843499 /* android.R.attr.actionBarSize */, value, true)) {
12
+ return android.util.TypedValue.complexToDimensionPixelSize(value.data, context.getResources().getDisplayMetrics());
13
+ }
14
+ return Math.round(56 * Utils.layout.getDisplayDensity());
15
+ }
16
+ function getMaterialAttr(name) {
17
+ return +java.lang.Class.forName('com.google.android.material.R$attr').getDeclaredField(name).get(null);
18
+ }
19
+ /** a color of the Material theme (colorSurface, colorOnSurface...) */
20
+ function getThemeColor(view, name, fallback) {
21
+ return com.google.android.material.color.MaterialColors.getColor(view, getMaterialAttr(name), fallback);
22
+ }
23
+ function toNativeColor(value) {
24
+ if (!value) {
25
+ return null;
26
+ }
27
+ return (value instanceof Color ? value : new Color(value)).android;
28
+ }
29
+ /**
30
+ * Badges (a number, a text, or a dot) on the actions of a toolbar. The previous ones are detached. The action views must exist: attached after the layout.
31
+ */
32
+ function attachBadges(toolbar, previous, wanted) {
33
+ const BadgeDrawable = com.google.android.material.badge.BadgeDrawable;
34
+ const BadgeUtils = com.google.android.material.badge.BadgeUtils;
35
+ previous.splice(0).forEach((badge) => BadgeUtils.detachBadgeDrawable(badge, toolbar, badge.__itemId));
36
+ if (!wanted.length) {
37
+ return;
38
+ }
39
+ toolbar.post(new java.lang.Runnable({
40
+ run: () => {
41
+ wanted.forEach(({ badge, id }) => {
42
+ const drawable = BadgeDrawable.create(toolbar.getContext());
43
+ if (typeof badge === 'number') {
44
+ drawable.setNumber(badge);
45
+ }
46
+ else if (typeof badge === 'string') {
47
+ drawable.setText(badge);
48
+ }
49
+ // `true`: the dot (no number, no text)
50
+ drawable.__itemId = id;
51
+ BadgeUtils.attachBadgeDrawable(drawable, toolbar, id);
52
+ previous.push(drawable);
53
+ });
54
+ }
55
+ }));
56
+ }
57
+ /**
58
+ * Android: a Material Toolbar. In an `AppPage` it lives in an `AppBarLayout`: `size` medium / large use a M3 `CollapsingToolbarLayout`,
59
+ * `scrollBehavior` the scroll flags / lift on scroll of the AppBarLayout.
60
+ */
61
+ export class AppBar extends AppBarBase {
62
+ constructor() {
63
+ super(...arguments);
64
+ this._badges = [];
65
+ }
66
+ // a Material 3 toolbar: tinted navigation icon, centered title...
67
+ createNativeView() {
68
+ return new com.google.android.material.appbar.MaterialToolbar(this._context);
69
+ }
70
+ // in an AppPage the surface of the bar is the AppBarLayout (lift, translucent, collapsing area): the toolbar is transparent
71
+ // @ts-ignore
72
+ _redrawNativeBackground(value) {
73
+ if (this.page instanceof AppPage) {
74
+ return;
75
+ }
76
+ // @ts-ignore
77
+ super._redrawNativeBackground(value);
78
+ }
79
+ /** the color of the bar: the css `background-color`, else the surface color of the theme */
80
+ getBarColor() {
81
+ const toolbar = this.nativeViewProtected;
82
+ return toNativeColor(this.backgroundColor) ?? getThemeColor(toolbar, 'colorSurface', android.graphics.Color.WHITE);
83
+ }
84
+ /** the color of the title and the icons: the css `color`, else the on surface color of the theme */
85
+ getOnBarColor() {
86
+ const toolbar = this.nativeViewProtected;
87
+ return toNativeColor(this.color) ?? getThemeColor(toolbar, 'colorOnSurface', android.graphics.Color.BLACK);
88
+ }
89
+ /** tints the navigation icon, the actions and the overflow icon with the color of the content of the bar */
90
+ applyTints() {
91
+ const toolbar = this.nativeViewProtected;
92
+ if (!toolbar) {
93
+ return;
94
+ }
95
+ const tint = this.getOnBarColor();
96
+ toolbar.setNavigationIconTint(tint);
97
+ const list = android.content.res.ColorStateList.valueOf(tint);
98
+ const menu = toolbar.getMenu();
99
+ for (let i = 0; i < menu.size(); i++) {
100
+ androidx.core.view.MenuItemCompat.setIconTintList(menu.getItem(i), list);
101
+ }
102
+ const overflow = toolbar.getOverflowIcon();
103
+ if (overflow) {
104
+ const wrapped = androidx.core.graphics.drawable.DrawableCompat.wrap(overflow.mutate());
105
+ androidx.core.graphics.drawable.DrawableCompat.setTint(wrapped, tint);
106
+ toolbar.setOverflowIcon(wrapped);
107
+ }
108
+ }
109
+ // the back arrow of the core is the old holo one: use the Material arrow
110
+ _updateNavigationButton() {
111
+ // @ts-ignore
112
+ super._updateNavigationButton();
113
+ const nav = this.navigationButton;
114
+ const toolbar = this.nativeViewProtected;
115
+ if (nav && nav.visibility === 'visible' && nav.android?.systemIcon === 'ic_menu_back') {
116
+ const context = this._context;
117
+ const id = context.getResources().getIdentifier('abc_ic_ab_back_material', 'drawable', context.getPackageName());
118
+ if (id) {
119
+ toolbar.setNavigationIcon(androidx.core.content.ContextCompat.getDrawable(context, id));
120
+ }
121
+ }
122
+ }
123
+ onAppBarPropertyChanged() {
124
+ super.onAppBarPropertyChanged();
125
+ const page = this.page;
126
+ if (page instanceof AppPage) {
127
+ page.syncAppBar();
128
+ }
129
+ }
130
+ update() {
131
+ super.update();
132
+ const page = this.page;
133
+ if (page instanceof AppPage) {
134
+ page.syncAppBar();
135
+ }
136
+ }
137
+ _addActionItems() {
138
+ // @ts-ignore
139
+ super._addActionItems();
140
+ this.syncBadges();
141
+ if (this.searchable && this.searchMode !== 'bar') {
142
+ this.addSearchItem();
143
+ }
144
+ this.applyTints();
145
+ }
146
+ /** the badges of the actions (a number, a text or a dot): attached once the action views exist */
147
+ syncBadges() {
148
+ const toolbar = this.nativeViewProtected;
149
+ const items = (this.actionItems?.getVisibleItems() ?? []);
150
+ const wanted = items.filter((item) => item instanceof AppBarItem && item.badge !== undefined && item.badge !== null && item.badge !== false && item.badge !== '');
151
+ attachBadges(toolbar, this._badges, wanted.map((item) => ({ id: item._getItemId(), badge: item.badge })));
152
+ }
153
+ /** a collapsible SearchView as the first action */
154
+ addSearchItem() {
155
+ const toolbar = this.nativeViewProtected;
156
+ const menu = toolbar.getMenu();
157
+ const item = menu.add(0, 0x7f0a5ea1, 0, this.searchHint || 'Search');
158
+ const context = toolbar.getContext();
159
+ const iconId = context.getResources().getIdentifier('abc_ic_search_api_material', 'drawable', context.getPackageName());
160
+ item.setIcon(iconId ? androidx.core.content.ContextCompat.getDrawable(context, iconId) : null);
161
+ const onBar = this.getOnBarColor();
162
+ item.setShowAsAction(android.view.MenuItem.SHOW_AS_ACTION_ALWAYS | android.view.MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
163
+ const searchView = new androidx.appcompat.widget.SearchView(context);
164
+ searchView.setQueryHint(this.searchHint ?? null);
165
+ searchView.setMaxWidth(java.lang.Integer.MAX_VALUE);
166
+ const owner = new WeakRef(this);
167
+ searchView.setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener({
168
+ onQueryTextChange: (text) => {
169
+ const bar = owner.get();
170
+ if (bar) {
171
+ bar.searchText = text;
172
+ bar.notify({ eventName: AppBarBase.searchChangeEvent, object: bar, text });
173
+ }
174
+ return true;
175
+ },
176
+ onQueryTextSubmit: (text) => {
177
+ const bar = owner.get();
178
+ bar?.notify({ eventName: AppBarBase.searchSubmitEvent, object: bar, text });
179
+ return false;
180
+ }
181
+ }));
182
+ item.setOnActionExpandListener(new android.view.MenuItem.OnActionExpandListener({
183
+ onMenuItemActionExpand: () => true,
184
+ onMenuItemActionCollapse: () => {
185
+ const bar = owner.get();
186
+ if (bar) {
187
+ bar.searchText = '';
188
+ bar.notify({ eventName: AppBarBase.searchCancelEvent, object: bar, text: '' });
189
+ }
190
+ return true;
191
+ }
192
+ }));
193
+ {
194
+ // the SearchView follows the colors of the bar
195
+ const R = androidx.appcompat.R.id;
196
+ const edit = searchView.findViewById(R.search_src_text);
197
+ edit?.setTextColor(onBar);
198
+ edit?.setHintTextColor(androidx.core.graphics.ColorUtils.setAlphaComponent(onBar, 150));
199
+ for (const id of [R.search_close_btn, R.search_mag_icon, R.search_button, R.search_go_btn, R.search_voice_btn]) {
200
+ const icon = searchView.findViewById(id);
201
+ icon?.setColorFilter(onBar);
202
+ }
203
+ }
204
+ item.setActionView(searchView);
205
+ }
206
+ _updateTitleAndTitleView() {
207
+ const page = this.page;
208
+ const collapsing = page instanceof AppPage ? page.collapsingLayout : null;
209
+ const toolbar = this.nativeViewProtected;
210
+ if (this.titleView || (!this.formattedTitle && !this.subtitle && !collapsing)) {
211
+ // @ts-ignore
212
+ super._updateTitleAndTitleView();
213
+ return;
214
+ }
215
+ const title = this.formattedTitle ? createNativeFormattedText(this.formattedTitle) : this.title;
216
+ if (collapsing) {
217
+ // the CollapsingToolbarLayout draws the title: the Toolbar one is empty
218
+ toolbar.setTitle(null);
219
+ collapsing.setTitle(title ?? null);
220
+ if (typeof collapsing.setSubtitle === 'function') {
221
+ collapsing.setSubtitle(this.subtitle ?? null);
222
+ }
223
+ toolbar.setSubtitle(null);
224
+ }
225
+ else {
226
+ toolbar.setTitle(title ?? null);
227
+ toolbar.setSubtitle(this.subtitle ?? null);
228
+ }
229
+ }
230
+ }
231
+ /**
232
+ * Android: a Page whose content scrolls with a Material `AppBarLayout` (lift, hide, collapse...).
233
+ *
234
+ * The native tree is built INSIDE the root of the regular Page, so any library that replaces the Page root
235
+ * (like gesturehandler `PageLayout`) keeps working:
236
+ *
237
+ * root = super.createNativeView() (GridLayout, or the gesturehandler PageLayout)
238
+ * `- CoordinatorLayout (row 1)
239
+ * |- AppBarLayout <- the ActionBar Toolbar (in a CollapsingToolbarLayout for medium / large)
240
+ * `- ContentLayout (ScrollingViewBehavior) <- the page content
241
+ */
242
+ export class AppPage extends Page {
243
+ constructor() {
244
+ super(...arguments);
245
+ this._scrollBehavior = 'none';
246
+ this._searchBarSetup = false;
247
+ this._bottomBadges = [];
248
+ }
249
+ /** the CollapsingToolbarLayout of a medium / large bar */
250
+ get collapsingLayout() {
251
+ return this._collapsing;
252
+ }
253
+ /** the behavior of a bar which is not an AppBar (a core ActionBar) */
254
+ get scrollBehavior() {
255
+ return this._scrollBehavior;
256
+ }
257
+ set scrollBehavior(value) {
258
+ this._scrollBehavior = value;
259
+ this.syncAppBar();
260
+ }
261
+ /** views anchored to the page (a FAB, a bottom bar...) instead of the scrolling content */
262
+ get overlay() {
263
+ return this._overlay;
264
+ }
265
+ set overlay(value) {
266
+ if (this._overlay === value) {
267
+ return;
268
+ }
269
+ if (this._overlay) {
270
+ this._removeView(this._overlay);
271
+ }
272
+ this._overlay = value;
273
+ if (value) {
274
+ this._addView(value);
275
+ }
276
+ }
277
+ _addChildFromBuilder(name, value) {
278
+ if (value instanceof AppPageOverlay) {
279
+ this.overlay = value;
280
+ }
281
+ else if (value instanceof AppPageSearchContent) {
282
+ this.searchContent = value;
283
+ }
284
+ else {
285
+ super._addChildFromBuilder(name, value);
286
+ }
287
+ }
288
+ /** `searchMode="bar"`: the content of the full screen search (suggestions, results) */
289
+ get searchContent() {
290
+ return this._searchContent;
291
+ }
292
+ set searchContent(value) {
293
+ if (this._searchContent === value) {
294
+ return;
295
+ }
296
+ if (this._searchContent) {
297
+ this._removeView(this._searchContent);
298
+ }
299
+ this._searchContent = value;
300
+ if (value) {
301
+ this._addView(value);
302
+ }
303
+ }
304
+ eachChildView(callback) {
305
+ super.eachChildView(callback);
306
+ if (this._overlay) {
307
+ callback(this._overlay);
308
+ }
309
+ if (this._searchContent) {
310
+ callback(this._searchContent);
311
+ }
312
+ }
313
+ createNativeView() {
314
+ // the root can be replaced by another library (gesturehandler): never create it ourselves
315
+ const root = super.createNativeView();
316
+ const context = this._context;
317
+ const CoordinatorLayout = androidx.coordinatorlayout.widget.CoordinatorLayout;
318
+ const AppBarLayout = com.google.android.material.appbar.AppBarLayout;
319
+ const coordinator = new CoordinatorLayout(context);
320
+ // The root of the page applies the insets (status bar aside, handled by the AppBarLayout): the NativeScript layouts inside would apply them again
321
+ // (the overlay would be shifted by the height of the keyboard). The coordinator consumes them.
322
+ androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener(coordinator, new androidx.core.view.OnApplyWindowInsetsListener({
323
+ onApplyWindowInsets: () => androidx.core.view.WindowInsetsCompat.CONSUMED
324
+ }));
325
+ const appBarLayout = new AppBarLayout(context);
326
+ const content = new org.nativescript.widgets.ContentLayout(context);
327
+ coordinator.addView(appBarLayout, new CoordinatorLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
328
+ const contentParams = new CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
329
+ contentParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
330
+ coordinator.addView(content, contentParams);
331
+ // above the content and the bar, no behavior: it does not move with them
332
+ const overlay = new org.nativescript.widgets.ContentLayout(context);
333
+ coordinator.addView(overlay, new CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
334
+ this._nativeOverlay = overlay;
335
+ // the status bar area: the bar scrolls away under it, what it draws there (a faded title) is covered
336
+ const scrim = new android.view.View(context);
337
+ coordinator.addView(scrim, new CoordinatorLayout.LayoutParams(MATCH_PARENT, 0));
338
+ // the AppBarLayout is elevated (lift on scroll): a sibling is drawn below it unless it is elevated more
339
+ scrim.setElevation(Math.round(12 * Utils.layout.getDisplayDensity()));
340
+ this._statusScrim = scrim;
341
+ // the full screen search of the M3 SearchBar: on top of everything, hidden until the bar is tapped
342
+ const searchView = new com.google.android.material.search.SearchView(context);
343
+ coordinator.addView(searchView, new CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
344
+ this._searchView = searchView;
345
+ const params = new org.nativescript.widgets.CommonLayoutParams();
346
+ params.width = MATCH_PARENT;
347
+ params.height = MATCH_PARENT;
348
+ params.row = 1;
349
+ root.addView(coordinator, params);
350
+ this._coordinator = coordinator;
351
+ this._appBarLayout = appBarLayout;
352
+ this._nativeContent = content;
353
+ return root;
354
+ }
355
+ onUnloaded() {
356
+ // the activity gets its soft input mode back
357
+ const activity = Application.android.foregroundActivity || Application.android.startActivity;
358
+ if (this._softInputMode !== undefined && activity) {
359
+ activity.getWindow().setSoftInputMode(this._softInputMode);
360
+ }
361
+ this._softInputMode = undefined;
362
+ super.onUnloaded();
363
+ }
364
+ onLoaded() {
365
+ super.onLoaded();
366
+ // The root of the page pads itself with the keyboard (edge to edge): when the window also resizes for the keyboard (adjustResize)
367
+ // the page area is reduced twice: the bar, the content and the overlay get squeezed. The root handles the keyboard alone.
368
+ const activity = Application.android.foregroundActivity || Application.android.startActivity;
369
+ if (activity && this._softInputMode === undefined) {
370
+ const window = activity.getWindow();
371
+ this._softInputMode = window.getAttributes().softInputMode;
372
+ window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING | (this._softInputMode & 0x0f));
373
+ }
374
+ // the bar draws under the status bar: the AppBarLayout pads itself with the inset
375
+ this.androidOverflowEdge = 'top';
376
+ // the root does not pad itself with the status bar inset anymore: the AppBarLayout (and its background) does
377
+ const statusBarHeight = this.getStatusBarHeight();
378
+ this._appBarLayout?.setPadding(0, statusBarHeight, 0, 0);
379
+ const scrimParams = this._statusScrim.getLayoutParams();
380
+ scrimParams.height = statusBarHeight;
381
+ this._statusScrim.setLayoutParams(scrimParams);
382
+ // the bar scrolls away under the status bar area: its content must not draw in the padding
383
+ this._appBarLayout?.setClipToPadding(true);
384
+ this.syncAppBar();
385
+ }
386
+ disposeNativeView() {
387
+ this._coordinator = null;
388
+ this._appBarLayout = null;
389
+ this._nativeContent = null;
390
+ this._nativeOverlay = null;
391
+ this._bottomBar = null;
392
+ this._statusScrim = null;
393
+ this._searchBar = null;
394
+ this._searchView = null;
395
+ this._collapsing = null;
396
+ super.disposeNativeView();
397
+ }
398
+ getStatusBarHeight() {
399
+ const root = this.nativeViewProtected;
400
+ const insets = androidx.core.view.ViewCompat.getRootWindowInsets(root);
401
+ if (insets) {
402
+ const types = androidx.core.view.WindowInsetsCompat.Type.statusBars() | androidx.core.view.WindowInsetsCompat.Type.displayCutout();
403
+ return insets.getInsets(types).top;
404
+ }
405
+ const resources = this._context.getResources();
406
+ const id = resources.getIdentifier('status_bar_height', 'dimen', 'android');
407
+ return id > 0 ? resources.getDimensionPixelSize(id) : 0;
408
+ }
409
+ /** moves the toolbar between the AppBarLayout and a CollapsingToolbarLayout and applies the scroll settings */
410
+ syncAppBar() {
411
+ const appBarLayout = this._appBarLayout;
412
+ if (!appBarLayout) {
413
+ return;
414
+ }
415
+ const bar = this.hasActionBar ? this.actionBar : null;
416
+ const toolbar = bar?.nativeViewProtected;
417
+ if (!toolbar) {
418
+ return;
419
+ }
420
+ const context = this._context;
421
+ const appBar = bar instanceof AppBar ? bar : null;
422
+ const size = appBar?.size ?? 'small';
423
+ const behavior = appBar && appBar.scrollBehavior !== 'none' ? appBar.scrollBehavior : this._scrollBehavior;
424
+ const translucent = !!appBar?.translucent;
425
+ const AppBarLayoutParams = com.google.android.material.appbar.AppBarLayout.LayoutParams;
426
+ const CollapsingParams = com.google.android.material.appbar.CollapsingToolbarLayout.LayoutParams;
427
+ // 1. the structure
428
+ if (size === 'small') {
429
+ if (this._collapsing) {
430
+ toolbar.getParent()?.removeView(toolbar);
431
+ appBarLayout.removeView(this._collapsing);
432
+ this._collapsing = null;
433
+ this._collapsingSize = null;
434
+ }
435
+ if (toolbar.getParent() !== appBarLayout) {
436
+ toolbar.getParent()?.removeView(toolbar);
437
+ appBarLayout.addView(toolbar, new AppBarLayoutParams(MATCH_PARENT, WRAP_CONTENT));
438
+ }
439
+ }
440
+ else if (this._collapsingSize !== size || !this._collapsing) {
441
+ toolbar.getParent()?.removeView(toolbar);
442
+ if (this._collapsing) {
443
+ appBarLayout.removeView(this._collapsing);
444
+ }
445
+ const resources = context.getResources();
446
+ const id = resources.getIdentifier(size === 'large' ? 'ns_appbar_collapsing_large' : 'ns_appbar_collapsing_medium', 'layout', context.getPackageName());
447
+ const collapsing = android.view.LayoutInflater.from(context).inflate(id, appBarLayout, false);
448
+ const toolbarParams = new CollapsingParams(MATCH_PARENT, getActionBarSize(context));
449
+ toolbarParams.setCollapseMode(COLLAPSE_MODE_PIN);
450
+ collapsing.addView(toolbar, toolbarParams);
451
+ appBarLayout.addView(collapsing);
452
+ this._collapsing = collapsing;
453
+ this._collapsingSize = size;
454
+ // the title now belongs to the collapsing layout
455
+ bar.update();
456
+ }
457
+ // 2. the scroll behavior: the flags are on the direct child of the AppBarLayout
458
+ const child = this._collapsing || toolbar;
459
+ const params = child.getLayoutParams();
460
+ // `hide` with a scroll target: the bar follows the scroll of the list itself (see syncHide), the AppBarLayout does not scroll
461
+ const scrollTargetNative = appBar?.scrollTarget?.nativeViewProtected;
462
+ const customHide = behavior === 'hide' && !!scrollTargetNative;
463
+ let flags = 0;
464
+ if (behavior === 'hide' && !customHide) {
465
+ // the whole bar leaves the screen (also the pinned toolbar of a medium / large bar) and comes back as soon as the content scrolls up.
466
+ // The AppBarLayout consumes the scroll first: the list starts to scroll when the bar is completely shown / hidden
467
+ flags = AppBarLayoutParams.SCROLL_FLAG_SCROLL | AppBarLayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayoutParams.SCROLL_FLAG_SNAP;
468
+ }
469
+ else if (behavior !== 'hide' && size !== 'small') {
470
+ // the title collapses into the pinned toolbar
471
+ flags = AppBarLayoutParams.SCROLL_FLAG_SCROLL | AppBarLayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED;
472
+ }
473
+ if (params.getScrollFlags() !== flags) {
474
+ params.setScrollFlags(flags);
475
+ child.setLayoutParams(params);
476
+ }
477
+ // 3. the surface of the bar: the AppBarLayout. The toolbar is transparent and flat, the collapsing area and the status bar area follow the same color
478
+ toolbar.setBackground(null);
479
+ toolbar.setElevation(0);
480
+ toolbar.setStateListAnimator(null);
481
+ const AlphaTranslucent = 160;
482
+ let barColor = appBar ? appBar.getBarColor() : (toNativeColor(bar.backgroundColor) ?? getThemeColor(toolbar, 'colorSurface', android.graphics.Color.WHITE));
483
+ const onBarColor = appBar ? appBar.getOnBarColor() : (toNativeColor(bar.color) ?? getThemeColor(toolbar, 'colorOnSurface', android.graphics.Color.BLACK));
484
+ if (translucent) {
485
+ // the content scrolls under the bar, which stays slightly see through
486
+ barColor = androidx.core.graphics.ColorUtils.setAlphaComponent(barColor, AlphaTranslucent);
487
+ }
488
+ // lifted (content scrolls under the bar): the color of the bar with a little of the content color, the M3 surface container idea
489
+ const liftedColor = androidx.core.graphics.ColorUtils.blendARGB(barColor, onBarColor, 0.1);
490
+ const MaterialShapeDrawable = com.google.android.material.shape.MaterialShapeDrawable;
491
+ let background = appBarLayout.getBackground();
492
+ if (!(background instanceof MaterialShapeDrawable)) {
493
+ // the lift animation of the AppBarLayout works on a MaterialShapeDrawable
494
+ background = new MaterialShapeDrawable();
495
+ appBarLayout.setBackground(background);
496
+ }
497
+ background.setFillColor(android.content.res.ColorStateList.valueOf(barColor));
498
+ appBarLayout.setLiftOnScrollColor(android.content.res.ColorStateList.valueOf(liftedColor));
499
+ appBarLayout.setLiftOnScroll(behavior === 'lift');
500
+ const scrollTarget = appBar?.scrollTarget?.nativeViewProtected;
501
+ if (behavior === 'lift' && scrollTarget) {
502
+ appBarLayout.setLiftOnScrollTargetView(scrollTarget);
503
+ }
504
+ this.setStatusScrimColor(appBarLayout.isLifted() && behavior === 'lift' ? liftedColor : barColor);
505
+ if (!this._liftListener) {
506
+ // the status bar area follows the color of the bar while it lifts
507
+ this._liftListener = new com.google.android.material.appbar.AppBarLayout.LiftOnScrollListener({
508
+ onUpdate: (elevation, color) => this.setStatusScrimColor(color)
509
+ });
510
+ appBarLayout.addLiftOnScrollListener(this._liftListener);
511
+ }
512
+ if (this._collapsing) {
513
+ // the scrim drawn once the bar is collapsed
514
+ this._collapsing.setContentScrimColor(behavior === 'lift' ? liftedColor : barColor);
515
+ this._collapsing.setStatusBarScrimColor(behavior === 'lift' ? liftedColor : barColor);
516
+ }
517
+ toolbar.setSubtitleTextColor(onBarColor);
518
+ if (this._collapsing) {
519
+ this._collapsing.setCollapsedTitleTextColor(onBarColor);
520
+ this._collapsing.setExpandedTitleColor(onBarColor);
521
+ // the subtitle API of the CollapsingToolbarLayout (Material 1.13+)
522
+ const c = this._collapsing;
523
+ c.setCollapsedSubtitleTextColor?.(onBarColor);
524
+ c.setExpandedSubtitleColor?.(onBarColor);
525
+ }
526
+ appBar?.applyTints();
527
+ // 4. translucent: the content goes under the bar. The behavior of the content places it below the bar: without it the content fills the page
528
+ // (also `hide` with a scroll target: the content fills the page, the bar moves over it)
529
+ const overlayContent = translucent || customHide;
530
+ const contentParams = this._nativeContent.getLayoutParams();
531
+ const hasBehavior = !!contentParams.getBehavior();
532
+ if (overlayContent === hasBehavior) {
533
+ const AppBarLayout = com.google.android.material.appbar.AppBarLayout;
534
+ contentParams.setBehavior(overlayContent ? null : new AppBarLayout.ScrollingViewBehavior());
535
+ this._nativeContent.setLayoutParams(contentParams);
536
+ if (overlayContent) {
537
+ appBarLayout.bringToFront();
538
+ }
539
+ }
540
+ this.syncTranslucentInset(appBar, overlayContent);
541
+ this.syncHide(customHide ? appBar.scrollTarget : null);
542
+ this.syncBottomBar(appBar, behavior);
543
+ this.syncSearchBar(appBar);
544
+ }
545
+ /**
546
+ * `hide`: the bar leaves the screen while the list scrolls down and comes back while it scrolls up, both at the same time as the list scrolls
547
+ * (the nested scrolling of the AppBarLayout would let the bar consume the scroll first). The bar follows the scroll deltas of the list and settles when the scroll stops.
548
+ */
549
+ syncHide(targetView) {
550
+ const target = targetView?.nativeViewProtected;
551
+ const appBarLayout = this._appBarLayout;
552
+ if (this._hideDetach && this._hideTarget !== target) {
553
+ this._hideDetach();
554
+ this._hideDetach = null;
555
+ this._hideTarget = null;
556
+ appBarLayout.animate().cancel();
557
+ appBarLayout.setTranslationY(0);
558
+ }
559
+ if (!target || this._hideDetach) {
560
+ return;
561
+ }
562
+ this._hideTarget = target;
563
+ this._hideView = targetView;
564
+ let offset = 0;
565
+ // the status bar area stays
566
+ const range = () => Math.max(0, appBarLayout.getHeight() - appBarLayout.getPaddingTop());
567
+ const apply = (value) => {
568
+ appBarLayout.animate().cancel();
569
+ offset = Math.max(-range(), Math.min(0, value));
570
+ appBarLayout.setTranslationY(offset);
571
+ };
572
+ const settle = () => {
573
+ const end = offset < -range() / 2 ? -range() : 0;
574
+ if (end !== offset) {
575
+ offset = end;
576
+ appBarLayout.animate().translationY(end).setDuration(160).start();
577
+ }
578
+ };
579
+ let last;
580
+ let timer;
581
+ const view = this._hideView;
582
+ const listener = (args) => {
583
+ const current = args.scrollOffset ?? args.scrollY ?? args.object?.verticalOffset;
584
+ if (typeof current !== 'number' || isNaN(current)) {
585
+ return;
586
+ }
587
+ const dy = last === undefined ? 0 : current - last;
588
+ last = current;
589
+ if (current <= 0) {
590
+ apply(0);
591
+ }
592
+ else if (dy) {
593
+ apply(offset - dy);
594
+ }
595
+ clearTimeout(timer);
596
+ timer = setTimeout(settle, 150);
597
+ };
598
+ view.on('scroll', listener);
599
+ this._hideDetach = () => {
600
+ clearTimeout(timer);
601
+ view.off('scroll', listener);
602
+ };
603
+ }
604
+ setStatusScrimColor(color) {
605
+ this._statusScrim?.setBackgroundColor(color);
606
+ }
607
+ /** `searchMode="bar"`: a Material 3 SearchBar under the toolbar that opens the full screen SearchView */
608
+ syncSearchBar(appBar) {
609
+ const appBarLayout = this._appBarLayout;
610
+ const wanted = !!appBar?.searchable && appBar.searchMode === 'bar';
611
+ if (!wanted) {
612
+ if (this._searchBar) {
613
+ this._searchBar.setVisibility(android.view.View.GONE);
614
+ }
615
+ return;
616
+ }
617
+ const AppBarLayoutParams = com.google.android.material.appbar.AppBarLayout.LayoutParams;
618
+ if (!this._searchBar) {
619
+ const searchBar = new com.google.android.material.search.SearchBar(this._context);
620
+ const density = Utils.layout.getDisplayDensity();
621
+ const params = new AppBarLayoutParams(MATCH_PARENT, WRAP_CONTENT);
622
+ params.setMargins(Math.round(16 * density), Math.round(8 * density), Math.round(16 * density), Math.round(8 * density));
623
+ params.setScrollFlags(AppBarLayoutParams.SCROLL_FLAG_SCROLL | AppBarLayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
624
+ appBarLayout.addView(searchBar, params);
625
+ this._searchBar = searchBar;
626
+ }
627
+ const searchBar = this._searchBar;
628
+ searchBar.setVisibility(android.view.View.VISIBLE);
629
+ // below the toolbar / collapsing bar
630
+ const last = appBarLayout.getChildCount() - 1;
631
+ if (appBarLayout.indexOfChild(searchBar) !== last) {
632
+ appBarLayout.removeView(searchBar);
633
+ appBarLayout.addView(searchBar);
634
+ }
635
+ searchBar.setHint(appBar.searchHint ?? null);
636
+ const searchView = this._searchView;
637
+ searchView.setHint(appBar.searchHint ?? null);
638
+ if (!this._searchBarSetup) {
639
+ this._searchBarSetup = true;
640
+ searchView.setupWithSearchBar(searchBar);
641
+ const owner = new WeakRef(appBar);
642
+ searchView.getEditText().addTextChangedListener(new android.text.TextWatcher({
643
+ beforeTextChanged: () => null,
644
+ onTextChanged: () => null,
645
+ afterTextChanged: (editable) => {
646
+ const bar = owner.get();
647
+ const text = editable.toString();
648
+ if (bar && bar.searchText !== text) {
649
+ bar.searchText = text;
650
+ bar.notify({ eventName: AppBarBase.searchChangeEvent, object: bar, text });
651
+ }
652
+ }
653
+ }));
654
+ searchView.getEditText().setOnEditorActionListener(new android.widget.TextView.OnEditorActionListener({
655
+ onEditorAction: () => {
656
+ const bar = owner.get();
657
+ bar?.notify({ eventName: AppBarBase.searchSubmitEvent, object: bar, text: searchView.getText().toString() });
658
+ return false;
659
+ }
660
+ }));
661
+ searchView.addTransitionListener(new com.google.android.material.search.SearchView.TransitionListener({
662
+ onStateChanged: (view, previousState, newState) => {
663
+ if (String(newState) === 'HIDDEN') {
664
+ const bar = owner.get();
665
+ if (bar) {
666
+ bar.searchText = '';
667
+ bar.notify({ eventName: AppBarBase.searchCancelEvent, object: bar, text: '' });
668
+ }
669
+ }
670
+ }
671
+ }));
672
+ }
673
+ }
674
+ /** `bottomItems`: a Material BottomAppBar, over the content, below the overlay */
675
+ syncBottomBar(appBar, behavior) {
676
+ const items = (appBar?.bottomItems || []).filter((item) => !item.spacer);
677
+ const coordinator = this._coordinator;
678
+ if (!items.length) {
679
+ if (this._bottomBar) {
680
+ this._bottomBar.setVisibility(android.view.View.GONE);
681
+ this.syncBottomInset(appBar, false);
682
+ }
683
+ return;
684
+ }
685
+ const BottomAppBar = com.google.android.material.bottomappbar.BottomAppBar;
686
+ const CoordinatorLayout = androidx.coordinatorlayout.widget.CoordinatorLayout;
687
+ if (!this._bottomBar) {
688
+ const bar = new BottomAppBar(this._context);
689
+ const params = new CoordinatorLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
690
+ params.gravity = android.view.Gravity.BOTTOM;
691
+ // below the overlay (a FAB, a panel)
692
+ coordinator.addView(bar, coordinator.indexOfChild(this._nativeOverlay), params);
693
+ this._bottomBar = bar;
694
+ }
695
+ const bar = this._bottomBar;
696
+ bar.setVisibility(android.view.View.VISIBLE);
697
+ bar.setHideOnScroll(behavior === 'hide');
698
+ const menu = bar.getMenu();
699
+ menu.clear();
700
+ const owner = new WeakRef(appBar);
701
+ items.forEach((item, index) => {
702
+ const menuItem = menu.add(0, index, index, item.text || '');
703
+ const icon = item.icon ? getNativeIcon(item.icon) : null;
704
+ if (icon) {
705
+ menuItem.setIcon(icon);
706
+ }
707
+ menuItem.setShowAsAction(android.view.MenuItem.SHOW_AS_ACTION_ALWAYS);
708
+ });
709
+ attachBadges(bar, this._bottomBadges, items
710
+ .map((item, index) => ({ id: index, badge: item.badge }))
711
+ .filter((entry) => entry.badge !== undefined && entry.badge !== null && entry.badge !== false && entry.badge !== ''));
712
+ bar.setOnMenuItemClickListener(new androidx.appcompat.widget.Toolbar.OnMenuItemClickListener({
713
+ onMenuItemClick: (menuItem) => {
714
+ const target = owner.get();
715
+ if (target) {
716
+ notifyBottomItemTap(target, items[menuItem.getItemId()]);
717
+ }
718
+ return true;
719
+ }
720
+ }));
721
+ this.syncBottomInset(appBar, true);
722
+ }
723
+ /** the last items of the scrolling view must not stay under the bottom bar */
724
+ syncBottomInset(appBar, visible) {
725
+ const bar = this._bottomBar;
726
+ const target = appBar?.scrollTarget?.nativeViewProtected;
727
+ if (this._bottomInsetListener) {
728
+ bar.removeOnLayoutChangeListener(this._bottomInsetListener);
729
+ this._bottomInsetListener = null;
730
+ }
731
+ if (!target) {
732
+ return;
733
+ }
734
+ if (!visible) {
735
+ if (target['__appBarBottomPadding'] !== undefined) {
736
+ target.setPadding(target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(), target['__appBarBottomPadding']);
737
+ target['__appBarBottomPadding'] = undefined;
738
+ }
739
+ return;
740
+ }
741
+ const apply = () => {
742
+ if (target['__appBarBottomPadding'] === undefined) {
743
+ target['__appBarBottomPadding'] = target.getPaddingBottom();
744
+ }
745
+ if (target instanceof android.view.ViewGroup) {
746
+ target.setClipToPadding(false);
747
+ }
748
+ target.setPadding(target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(), bar.getHeight());
749
+ };
750
+ this._bottomInsetListener = new android.view.View.OnLayoutChangeListener({
751
+ onLayoutChange: () => {
752
+ if (target.getPaddingBottom() !== bar.getHeight() && this._bottomInsetListener) {
753
+ apply();
754
+ }
755
+ }
756
+ });
757
+ bar.addOnLayoutChangeListener(this._bottomInsetListener);
758
+ apply();
759
+ }
760
+ /** translucent: the first items of the scrolling view must start below the bar: its top padding follows the height of the bar */
761
+ syncTranslucentInset(appBar, translucent) {
762
+ const appBarLayout = this._appBarLayout;
763
+ const target = appBar?.scrollTarget?.nativeViewProtected;
764
+ if (this._insetListener) {
765
+ appBarLayout.removeOnLayoutChangeListener(this._insetListener);
766
+ this._insetListener = null;
767
+ }
768
+ if (!target) {
769
+ return;
770
+ }
771
+ if (!translucent) {
772
+ if (target['__appBarPadding'] !== undefined) {
773
+ target.setPadding(target.getPaddingLeft(), target['__appBarPadding'], target.getPaddingRight(), target.getPaddingBottom());
774
+ target['__appBarPadding'] = undefined;
775
+ }
776
+ return;
777
+ }
778
+ const apply = () => {
779
+ if (target['__appBarPadding'] === undefined) {
780
+ target['__appBarPadding'] = target.getPaddingTop();
781
+ }
782
+ if (target instanceof android.view.ViewGroup) {
783
+ target.setClipToPadding(false);
784
+ }
785
+ // only the part of the bar that does not scroll away: the height of the expanded bar
786
+ target.setPadding(target.getPaddingLeft(), appBarLayout.getHeight(), target.getPaddingRight(), target.getPaddingBottom());
787
+ };
788
+ this._insetListener = new android.view.View.OnLayoutChangeListener({
789
+ onLayoutChange: () => {
790
+ if (target.getPaddingTop() !== appBarLayout.getHeight() && this._insetListener) {
791
+ apply();
792
+ }
793
+ }
794
+ });
795
+ appBarLayout.addOnLayoutChangeListener(this._insetListener);
796
+ apply();
797
+ }
798
+ _addViewToNativeVisualTree(child, atIndex = Number.MAX_SAFE_INTEGER) {
799
+ const nativeChild = child.nativeViewProtected;
800
+ if (!this.nativeViewProtected || !nativeChild || !this._coordinator) {
801
+ return false;
802
+ }
803
+ if (child instanceof ActionBar) {
804
+ const AppBarLayout = com.google.android.material.appbar.AppBarLayout;
805
+ this._appBarLayout.addView(nativeChild, new AppBarLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
806
+ this.syncAppBar();
807
+ }
808
+ else if (child === this._overlay) {
809
+ this._nativeOverlay.addView(nativeChild, atIndex);
810
+ }
811
+ else if (child === this._searchContent) {
812
+ this._searchView.addView(nativeChild);
813
+ }
814
+ else {
815
+ this._nativeContent.addView(nativeChild, atIndex);
816
+ }
817
+ return true;
818
+ }
819
+ _removeViewFromNativeVisualTree(child) {
820
+ const nativeChild = child.nativeViewProtected;
821
+ const parent = nativeChild?.getParent();
822
+ parent?.removeView(nativeChild);
823
+ }
824
+ }
825
+ //# sourceMappingURL=index.android.js.map