@nativescript-community/ui-appbar 1.0.4 → 1.2.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.
- package/CHANGELOG.md +12 -0
- package/README.md +20 -4
- package/index-common.d.ts +56 -3
- package/index-common.js +83 -2
- package/index-common.js.map +1 -1
- package/index.android.d.ts +63 -2
- package/index.android.js +548 -29
- package/index.android.js.map +1 -1
- package/index.d.ts +24 -4
- package/index.ios.d.ts +4 -3
- package/index.ios.js +60 -8
- package/index.ios.js.map +1 -1
- package/package.json +4 -4
- package/platforms/android/native-api-usage.json +34 -0
- package/svelte/index.d.ts +1 -1
- package/svelte/index.js +6 -2
- package/svelte/index.js.map +1 -1
package/index.android.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { ActionBar, Application, Color, Page, Utils } from '@nativescript/core';
|
|
1
|
+
import { ActionBar, Application, Color, ContentView, FormattedString, ImageSource, Page, TextField, Utils } from '@nativescript/core';
|
|
2
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 };
|
|
3
|
+
import { getNativeIcon, showMenu } from '@nativescript-community/ui-menu';
|
|
4
|
+
import { AppBarBase, AppBarBottomView, AppBarItem, AppBarSearchField, AppPageOverlay, AppPageSearchContent, notifyBottomItemTap } from './index-common';
|
|
5
|
+
export { AppBarBottomView, AppBarItem, AppBarSearchField, AppPageOverlay, AppPageSearchContent };
|
|
6
6
|
const MATCH_PARENT = -1;
|
|
7
7
|
const WRAP_CONTENT = -2;
|
|
8
8
|
const COLLAPSE_MODE_PIN = 1;
|
|
9
|
+
const MATERIAL_NAVIGATION_ICONS = {
|
|
10
|
+
ic_menu_back: 'abc_ic_ab_back_material',
|
|
11
|
+
ic_menu_close_clear_cancel: 'abc_ic_clear_material'
|
|
12
|
+
};
|
|
9
13
|
function getActionBarSize(context) {
|
|
10
14
|
const value = new android.util.TypedValue();
|
|
11
15
|
if (context.getTheme().resolveAttribute(16843499 /* android.R.attr.actionBarSize */, value, true)) {
|
|
@@ -18,7 +22,38 @@ function getMaterialAttr(name) {
|
|
|
18
22
|
}
|
|
19
23
|
/** a color of the Material theme (colorSurface, colorOnSurface...) */
|
|
20
24
|
function getThemeColor(view, name, fallback) {
|
|
21
|
-
|
|
25
|
+
// the theme of a view context is a copy made when the view was created: with dynamic colors it keeps the day/night colors of that time.
|
|
26
|
+
// The theme of the activity is the one that is applied again when the mode changes
|
|
27
|
+
const activity = Application.android.foregroundActivity || Application.android.startActivity;
|
|
28
|
+
return com.google.android.material.color.MaterialColors.getColor(activity || view.getContext(), getMaterialAttr(name), fallback);
|
|
29
|
+
}
|
|
30
|
+
let noShadowOutline;
|
|
31
|
+
/** a view that only takes part in the stacking order (elevation) casts no shadow */
|
|
32
|
+
function getNoShadowOutline() {
|
|
33
|
+
if (!noShadowOutline) {
|
|
34
|
+
// @ts-ignore abstract class: extended through the runtime
|
|
35
|
+
const NoShadowOutline = android.view.ViewOutlineProvider.extend({ getOutline: (_view, outline) => outline.setEmpty() });
|
|
36
|
+
noShadowOutline = new NoShadowOutline();
|
|
37
|
+
}
|
|
38
|
+
return noShadowOutline;
|
|
39
|
+
}
|
|
40
|
+
/** the SearchBar draws its fill in a MaterialShapeDrawable under a ripple / focus ring layer */
|
|
41
|
+
function findShapeDrawable(drawable) {
|
|
42
|
+
if (drawable instanceof com.google.android.material.shape.MaterialShapeDrawable) {
|
|
43
|
+
return drawable;
|
|
44
|
+
}
|
|
45
|
+
if (drawable instanceof android.graphics.drawable.LayerDrawable) {
|
|
46
|
+
for (let index = 0; index < drawable.getNumberOfLayers(); index++) {
|
|
47
|
+
const found = findShapeDrawable(drawable.getDrawable(index));
|
|
48
|
+
if (found) {
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function toColorStateList(value) {
|
|
56
|
+
return android.content.res.ColorStateList.valueOf(toNativeColor(value));
|
|
22
57
|
}
|
|
23
58
|
function toNativeColor(value) {
|
|
24
59
|
if (!value) {
|
|
@@ -106,20 +141,33 @@ export class AppBar extends AppBarBase {
|
|
|
106
141
|
toolbar.setOverflowIcon(wrapped);
|
|
107
142
|
}
|
|
108
143
|
}
|
|
109
|
-
// the back arrow of the core
|
|
144
|
+
// the back arrow and the close icon of the core are the old holo ones: use the Material (appcompat) ones
|
|
110
145
|
_updateNavigationButton() {
|
|
111
146
|
// @ts-ignore
|
|
112
147
|
super._updateNavigationButton();
|
|
113
148
|
const nav = this.navigationButton;
|
|
114
149
|
const toolbar = this.nativeViewProtected;
|
|
115
|
-
|
|
150
|
+
const drawableName = nav?.visibility === 'visible' ? MATERIAL_NAVIGATION_ICONS[nav.android?.systemIcon] : undefined;
|
|
151
|
+
if (drawableName) {
|
|
116
152
|
const context = this._context;
|
|
117
|
-
const id = context.getResources().getIdentifier(
|
|
153
|
+
const id = context.getResources().getIdentifier(drawableName, 'drawable', context.getPackageName());
|
|
118
154
|
if (id) {
|
|
119
155
|
toolbar.setNavigationIcon(androidx.core.content.ContextCompat.getDrawable(context, id));
|
|
120
156
|
}
|
|
121
157
|
}
|
|
122
158
|
}
|
|
159
|
+
_addViewToNativeVisualTree(child, atIndex) {
|
|
160
|
+
// the page places them (search field, floating toolbar), not the toolbar
|
|
161
|
+
return child === this.searchField || child === this.bottomView || super._addViewToNativeVisualTree(child, atIndex);
|
|
162
|
+
}
|
|
163
|
+
_removeViewFromNativeVisualTree(child) {
|
|
164
|
+
if (child === this.searchField || child === this.bottomView) {
|
|
165
|
+
const nativeChild = child.nativeViewProtected;
|
|
166
|
+
nativeChild?.getParent()?.removeView(nativeChild);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
super._removeViewFromNativeVisualTree(child);
|
|
170
|
+
}
|
|
123
171
|
onAppBarPropertyChanged() {
|
|
124
172
|
super.onAppBarPropertyChanged();
|
|
125
173
|
const page = this.page;
|
|
@@ -127,6 +175,15 @@ export class AppBar extends AppBarBase {
|
|
|
127
175
|
page.syncAppBar();
|
|
128
176
|
}
|
|
129
177
|
}
|
|
178
|
+
// @ts-ignore
|
|
179
|
+
_onTitlePropertyChanged() {
|
|
180
|
+
// @ts-ignore
|
|
181
|
+
super._onTitlePropertyChanged();
|
|
182
|
+
const page = this.page;
|
|
183
|
+
if (page instanceof AppPage) {
|
|
184
|
+
page.syncAppBar();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
130
187
|
update() {
|
|
131
188
|
super.update();
|
|
132
189
|
const page = this.page;
|
|
@@ -134,15 +191,44 @@ export class AppBar extends AppBarBase {
|
|
|
134
191
|
page.syncAppBar();
|
|
135
192
|
}
|
|
136
193
|
}
|
|
194
|
+
/** the toolbar is hidden while the SearchBar hosts the actions: the menu opens under the SearchBar */
|
|
195
|
+
getMenuAnchor() {
|
|
196
|
+
const page = this.page;
|
|
197
|
+
const searchBar = page instanceof AppPage ? page.hostingSearchBar : null;
|
|
198
|
+
return searchBar ? { nativeViewProtected: searchBar } : this;
|
|
199
|
+
}
|
|
200
|
+
closeSearch() {
|
|
201
|
+
const page = this.page;
|
|
202
|
+
return page instanceof AppPage ? page.closeInlineSearch() : false;
|
|
203
|
+
}
|
|
204
|
+
focusSearch() {
|
|
205
|
+
const page = this.page;
|
|
206
|
+
return page instanceof AppPage ? page.openInlineSearch() : false;
|
|
207
|
+
}
|
|
137
208
|
_addActionItems() {
|
|
138
209
|
// @ts-ignore
|
|
139
210
|
super._addActionItems();
|
|
211
|
+
this.applyItemIcons();
|
|
140
212
|
this.syncBadges();
|
|
141
|
-
if (this.searchable && this.searchMode
|
|
213
|
+
if (this.searchable && this.searchMode === 'collapsible') {
|
|
142
214
|
this.addSearchItem();
|
|
143
215
|
}
|
|
144
216
|
this.applyTints();
|
|
145
217
|
}
|
|
218
|
+
/** core only loads string icons: an ImageSource or a FormattedString (font icon) is converted here */
|
|
219
|
+
applyItemIcons() {
|
|
220
|
+
const menu = this.nativeViewProtected.getMenu();
|
|
221
|
+
const items = (this.actionItems?.getVisibleItems() ?? []);
|
|
222
|
+
items.forEach((item) => {
|
|
223
|
+
const icon = item.icon;
|
|
224
|
+
if (icon instanceof ImageSource || icon instanceof FormattedString) {
|
|
225
|
+
const drawable = getNativeIcon(icon);
|
|
226
|
+
if (drawable) {
|
|
227
|
+
menu.findItem(item._getItemId())?.setIcon(drawable);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
146
232
|
/** the badges of the actions (a number, a text or a dot): attached once the action views exist */
|
|
147
233
|
syncBadges() {
|
|
148
234
|
const toolbar = this.nativeViewProtected;
|
|
@@ -243,7 +329,24 @@ export class AppPage extends Page {
|
|
|
243
329
|
constructor() {
|
|
244
330
|
super(...arguments);
|
|
245
331
|
this._scrollBehavior = 'none';
|
|
332
|
+
// the activity handles `uiMode` itself (no recreate): the theme resolves the new colors right away, but the views keep the ones they were created with
|
|
333
|
+
this._appearanceListener = () => {
|
|
334
|
+
const bar = this.hasActionBar ? this.actionBar : null;
|
|
335
|
+
if (bar instanceof AppBar) {
|
|
336
|
+
bar.update();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this.syncAppBar();
|
|
340
|
+
}
|
|
341
|
+
this.syncStatusBarIcons();
|
|
342
|
+
};
|
|
246
343
|
this._searchBarSetup = false;
|
|
344
|
+
// the native view of a hosted view of the bar is created after the bar is added to the page
|
|
345
|
+
this._syncOnLoaded = (args) => {
|
|
346
|
+
args.object.off('loaded', this._syncOnLoaded);
|
|
347
|
+
this.syncAppBar();
|
|
348
|
+
};
|
|
349
|
+
this._inlineOpen = false;
|
|
247
350
|
this._bottomBadges = [];
|
|
248
351
|
}
|
|
249
352
|
/** the CollapsingToolbarLayout of a medium / large bar */
|
|
@@ -281,6 +384,10 @@ export class AppPage extends Page {
|
|
|
281
384
|
else if (value instanceof AppPageSearchContent) {
|
|
282
385
|
this.searchContent = value;
|
|
283
386
|
}
|
|
387
|
+
else if (value instanceof AppBarBase) {
|
|
388
|
+
// core only matches the constructor name 'ActionBar' (@akylas/nativescript)
|
|
389
|
+
this.actionBar = value;
|
|
390
|
+
}
|
|
284
391
|
else {
|
|
285
392
|
super._addChildFromBuilder(name, value);
|
|
286
393
|
}
|
|
@@ -337,6 +444,7 @@ export class AppPage extends Page {
|
|
|
337
444
|
coordinator.addView(scrim, new CoordinatorLayout.LayoutParams(MATCH_PARENT, 0));
|
|
338
445
|
// the AppBarLayout is elevated (lift on scroll): a sibling is drawn below it unless it is elevated more
|
|
339
446
|
scrim.setElevation(Math.round(12 * Utils.layout.getDisplayDensity()));
|
|
447
|
+
scrim.setOutlineProvider(getNoShadowOutline());
|
|
340
448
|
this._statusScrim = scrim;
|
|
341
449
|
// the full screen search of the M3 SearchBar: on top of everything, hidden until the bar is tapped
|
|
342
450
|
const searchView = new com.google.android.material.search.SearchView(context);
|
|
@@ -359,8 +467,21 @@ export class AppPage extends Page {
|
|
|
359
467
|
activity.getWindow().setSoftInputMode(this._softInputMode);
|
|
360
468
|
}
|
|
361
469
|
this._softInputMode = undefined;
|
|
470
|
+
Application.off(Application.systemAppearanceChangedEvent, this._appearanceListener);
|
|
362
471
|
super.onUnloaded();
|
|
363
472
|
}
|
|
473
|
+
/** the icons of the status bar follow the luminance of the bar drawn under it: the style of the page is not applied again by a live dark / light switch */
|
|
474
|
+
syncStatusBarIcons() {
|
|
475
|
+
const window = (Application.android.foregroundActivity || Application.android.startActivity)?.getWindow();
|
|
476
|
+
if (!window) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// without an AppBar (actionBarHidden) the status bar is over the background of the page
|
|
480
|
+
const bar = this.hasActionBar ? this.actionBar : null;
|
|
481
|
+
const color = bar instanceof AppBar ? bar.getBarColor() : (toNativeColor(this.backgroundColor) ?? getThemeColor(this.nativeViewProtected, 'colorSurface', android.graphics.Color.WHITE));
|
|
482
|
+
const light = androidx.core.graphics.ColorUtils.calculateLuminance(color) > 0.5;
|
|
483
|
+
androidx.core.view.WindowCompat.getInsetsController(window, window.getDecorView()).setAppearanceLightStatusBars(light);
|
|
484
|
+
}
|
|
364
485
|
onLoaded() {
|
|
365
486
|
super.onLoaded();
|
|
366
487
|
// The root of the page pads itself with the keyboard (edge to edge): when the window also resizes for the keyboard (adjustResize)
|
|
@@ -371,6 +492,7 @@ export class AppPage extends Page {
|
|
|
371
492
|
this._softInputMode = window.getAttributes().softInputMode;
|
|
372
493
|
window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING | (this._softInputMode & 0x0f));
|
|
373
494
|
}
|
|
495
|
+
Application.on(Application.systemAppearanceChangedEvent, this._appearanceListener);
|
|
374
496
|
// the bar draws under the status bar: the AppBarLayout pads itself with the inset
|
|
375
497
|
this.androidOverflowEdge = 'top';
|
|
376
498
|
// the root does not pad itself with the status bar inset anymore: the AppBarLayout (and its background) does
|
|
@@ -387,6 +509,8 @@ export class AppPage extends Page {
|
|
|
387
509
|
this._coordinator = null;
|
|
388
510
|
this._appBarLayout = null;
|
|
389
511
|
this._nativeContent = null;
|
|
512
|
+
this._liftTarget = null;
|
|
513
|
+
this._liftContentListener = null;
|
|
390
514
|
this._nativeOverlay = null;
|
|
391
515
|
this._bottomBar = null;
|
|
392
516
|
this._statusScrim = null;
|
|
@@ -458,7 +582,8 @@ export class AppPage extends Page {
|
|
|
458
582
|
const child = this._collapsing || toolbar;
|
|
459
583
|
const params = child.getLayoutParams();
|
|
460
584
|
// `hide` with a scroll target: the bar follows the scroll of the list itself (see syncHide), the AppBarLayout does not scroll
|
|
461
|
-
const
|
|
585
|
+
const scrollTargetView = appBar?.getScrollTarget();
|
|
586
|
+
const scrollTargetNative = scrollTargetView?.nativeViewProtected;
|
|
462
587
|
const customHide = behavior === 'hide' && !!scrollTargetNative;
|
|
463
588
|
let flags = 0;
|
|
464
589
|
if (behavior === 'hide' && !customHide) {
|
|
@@ -497,10 +622,7 @@ export class AppPage extends Page {
|
|
|
497
622
|
background.setFillColor(android.content.res.ColorStateList.valueOf(barColor));
|
|
498
623
|
appBarLayout.setLiftOnScrollColor(android.content.res.ColorStateList.valueOf(liftedColor));
|
|
499
624
|
appBarLayout.setLiftOnScroll(behavior === 'lift');
|
|
500
|
-
|
|
501
|
-
if (behavior === 'lift' && scrollTarget) {
|
|
502
|
-
appBarLayout.setLiftOnScrollTargetView(scrollTarget);
|
|
503
|
-
}
|
|
625
|
+
this.syncLiftTarget(appBar, behavior);
|
|
504
626
|
this.setStatusScrimColor(appBarLayout.isLifted() && behavior === 'lift' ? liftedColor : barColor);
|
|
505
627
|
if (!this._liftListener) {
|
|
506
628
|
// the status bar area follows the color of the bar while it lifts
|
|
@@ -514,6 +636,7 @@ export class AppPage extends Page {
|
|
|
514
636
|
this._collapsing.setContentScrimColor(behavior === 'lift' ? liftedColor : barColor);
|
|
515
637
|
this._collapsing.setStatusBarScrimColor(behavior === 'lift' ? liftedColor : barColor);
|
|
516
638
|
}
|
|
639
|
+
toolbar.setTitleTextColor(onBarColor);
|
|
517
640
|
toolbar.setSubtitleTextColor(onBarColor);
|
|
518
641
|
if (this._collapsing) {
|
|
519
642
|
this._collapsing.setCollapsedTitleTextColor(onBarColor);
|
|
@@ -538,7 +661,7 @@ export class AppPage extends Page {
|
|
|
538
661
|
}
|
|
539
662
|
}
|
|
540
663
|
this.syncTranslucentInset(appBar, overlayContent);
|
|
541
|
-
this.syncHide(customHide ?
|
|
664
|
+
this.syncHide(customHide ? scrollTargetView : null);
|
|
542
665
|
this.syncBottomBar(appBar, behavior);
|
|
543
666
|
this.syncSearchBar(appBar);
|
|
544
667
|
}
|
|
@@ -601,17 +724,66 @@ export class AppPage extends Page {
|
|
|
601
724
|
view.off('scroll', listener);
|
|
602
725
|
};
|
|
603
726
|
}
|
|
727
|
+
/** the first vertically scrolling view under `view`: the lift state must follow one view, else several nested scroll events disagree and the bar blinks */
|
|
728
|
+
findScrollingView(view) {
|
|
729
|
+
if (view instanceof androidx.recyclerview.widget.RecyclerView || view instanceof androidx.core.widget.NestedScrollView || view instanceof android.widget.ScrollView) {
|
|
730
|
+
return view;
|
|
731
|
+
}
|
|
732
|
+
if (view instanceof android.view.ViewGroup) {
|
|
733
|
+
for (let index = 0; index < view.getChildCount(); index++) {
|
|
734
|
+
const found = this.findScrollingView(view.getChildAt(index));
|
|
735
|
+
if (found) {
|
|
736
|
+
return found;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
/** `lift` follows `scrollTarget` / `scrollTargetId`, by default the first scrolling view of the content (searched again when the content is laid out) */
|
|
743
|
+
syncLiftTarget(appBar, behavior) {
|
|
744
|
+
const content = this._nativeContent;
|
|
745
|
+
if (behavior !== 'lift') {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const explicit = appBar?.getScrollTarget()?.nativeViewProtected;
|
|
749
|
+
const target = explicit ?? this.findScrollingView(content);
|
|
750
|
+
if (target && target !== this._liftTarget) {
|
|
751
|
+
this._liftTarget = target;
|
|
752
|
+
this._appBarLayout.setLiftOnScrollTargetView(target);
|
|
753
|
+
}
|
|
754
|
+
if (!explicit && !this._liftContentListener) {
|
|
755
|
+
this._liftContentListener = new android.view.View.OnLayoutChangeListener({
|
|
756
|
+
onLayoutChange: () => {
|
|
757
|
+
if (!this._liftTarget || !this._liftTarget.isAttachedToWindow()) {
|
|
758
|
+
this._liftTarget = null;
|
|
759
|
+
const bar = this.hasActionBar ? this.actionBar : null;
|
|
760
|
+
this.syncLiftTarget(bar instanceof AppBar ? bar : null, behavior);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
content.addOnLayoutChangeListener(this._liftContentListener);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
604
767
|
setStatusScrimColor(color) {
|
|
605
768
|
this._statusScrim?.setBackgroundColor(color);
|
|
606
769
|
}
|
|
770
|
+
/** the SearchBar when it hosts the navigation icon and the actions of the bar (the toolbar is hidden) */
|
|
771
|
+
get hostingSearchBar() {
|
|
772
|
+
const toolbar = this.actionBar?.nativeViewProtected;
|
|
773
|
+
return this._searchBar?.getVisibility() === android.view.View.VISIBLE && toolbar?.getVisibility() === android.view.View.GONE ? this._searchBar : null;
|
|
774
|
+
}
|
|
607
775
|
/** `searchMode="bar"`: a Material 3 SearchBar under the toolbar that opens the full screen SearchView */
|
|
608
776
|
syncSearchBar(appBar) {
|
|
609
777
|
const appBarLayout = this._appBarLayout;
|
|
610
|
-
const wanted = !!appBar?.searchable && appBar.searchMode === 'bar';
|
|
778
|
+
const wanted = !!appBar?.searchable && (appBar.searchMode === 'bar' || appBar.searchMode === 'inline');
|
|
611
779
|
if (!wanted) {
|
|
612
780
|
if (this._searchBar) {
|
|
613
781
|
this._searchBar.setVisibility(android.view.View.GONE);
|
|
782
|
+
// the toolbar was hidden while the SearchBar hosted its content
|
|
783
|
+
appBar?.nativeViewProtected?.setVisibility(android.view.View.VISIBLE);
|
|
614
784
|
}
|
|
785
|
+
this._inlineOpen = false;
|
|
786
|
+
this._inlineField?.setVisibility(android.view.View.GONE);
|
|
615
787
|
return;
|
|
616
788
|
}
|
|
617
789
|
const AppBarLayoutParams = com.google.android.material.appbar.AppBarLayout.LayoutParams;
|
|
@@ -633,6 +805,12 @@ export class AppPage extends Page {
|
|
|
633
805
|
appBarLayout.addView(searchBar);
|
|
634
806
|
}
|
|
635
807
|
searchBar.setHint(appBar.searchHint ?? null);
|
|
808
|
+
this.syncSearchBarHost(appBar, searchBar);
|
|
809
|
+
this.syncSearchBarColors(searchBar);
|
|
810
|
+
if (appBar.searchMode === 'inline') {
|
|
811
|
+
this.syncInlineSearch(appBar, searchBar);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
636
814
|
const searchView = this._searchView;
|
|
637
815
|
searchView.setHint(appBar.searchHint ?? null);
|
|
638
816
|
if (!this._searchBarSetup) {
|
|
@@ -671,15 +849,262 @@ export class AppPage extends Page {
|
|
|
671
849
|
}));
|
|
672
850
|
}
|
|
673
851
|
}
|
|
852
|
+
syncWhenLoaded(view) {
|
|
853
|
+
view.off('loaded', this._syncOnLoaded);
|
|
854
|
+
view.on('loaded', this._syncOnLoaded);
|
|
855
|
+
}
|
|
856
|
+
getAppCompatDrawable(name) {
|
|
857
|
+
const context = this._context;
|
|
858
|
+
const id = context.getResources().getIdentifier(name, 'drawable', context.getPackageName());
|
|
859
|
+
return id ? androidx.core.content.ContextCompat.getDrawable(context, id) : null;
|
|
860
|
+
}
|
|
861
|
+
createInlineButton(drawableName, onClick) {
|
|
862
|
+
const density = Utils.layout.getDisplayDensity();
|
|
863
|
+
const button = new android.widget.ImageView(this._context);
|
|
864
|
+
button.setImageDrawable(this.getAppCompatDrawable(drawableName));
|
|
865
|
+
button.setScaleType(android.widget.ImageView.ScaleType.CENTER);
|
|
866
|
+
button.setLayoutParams(new android.widget.LinearLayout.LayoutParams(Math.round(48 * density), Math.round(48 * density)));
|
|
867
|
+
button.setOnClickListener(new android.view.View.OnClickListener({ onClick }));
|
|
868
|
+
return button;
|
|
869
|
+
}
|
|
870
|
+
notifySearchChange(text) {
|
|
871
|
+
const bar = this._inlineOwner?.get();
|
|
872
|
+
if (bar && this._inlineOpen && bar.searchText !== text) {
|
|
873
|
+
bar.searchText = text;
|
|
874
|
+
bar.notify({ eventName: AppBarBase.searchChangeEvent, object: bar, text });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
notifySearchSubmit(text) {
|
|
878
|
+
const bar = this._inlineOwner?.get();
|
|
879
|
+
bar?.notify({ eventName: AppBarBase.searchSubmitEvent, object: bar, text });
|
|
880
|
+
}
|
|
881
|
+
getInlineText() {
|
|
882
|
+
return this._inlineTextField ? (this._inlineTextField.text ?? '') : this._inlineEdit.getText().toString();
|
|
883
|
+
}
|
|
884
|
+
clearInlineText() {
|
|
885
|
+
if (this._inlineTextField) {
|
|
886
|
+
this._inlineTextField.text = '';
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
this._inlineEdit.setText('');
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
dismissInlineKeyboard() {
|
|
893
|
+
if (this._inlineTextField) {
|
|
894
|
+
this._inlineTextField.dismissSoftInput();
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
Utils.dismissSoftInput(this._inlineEdit);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/** the field of the search: the `searchField` of the bar when it holds a TextField, else the built-in EditText */
|
|
901
|
+
bindInlineTextField(appBar) {
|
|
902
|
+
const host = appBar.searchField;
|
|
903
|
+
if (host && !host.nativeViewProtected) {
|
|
904
|
+
this.syncWhenLoaded(host);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const textField = host instanceof TextField ? host : host instanceof ContentView && host.content instanceof TextField ? host.content : null;
|
|
908
|
+
if (textField === this._inlineTextField) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this._inlineTextFieldDetach?.();
|
|
912
|
+
this._inlineTextFieldDetach = null;
|
|
913
|
+
this._inlineTextField = textField;
|
|
914
|
+
const field = this._inlineField;
|
|
915
|
+
const input = textField ? host.nativeViewProtected : this._inlineEdit;
|
|
916
|
+
input.getParent()?.removeView(input);
|
|
917
|
+
field.removeViewAt(1);
|
|
918
|
+
field.addView(input, 1, new android.widget.LinearLayout.LayoutParams(0, WRAP_CONTENT, 1));
|
|
919
|
+
if (textField) {
|
|
920
|
+
const onTextChange = () => this.notifySearchChange(this.getInlineText());
|
|
921
|
+
const onReturnPress = () => {
|
|
922
|
+
this.notifySearchSubmit(this.getInlineText());
|
|
923
|
+
textField.dismissSoftInput();
|
|
924
|
+
};
|
|
925
|
+
textField.on('textChange', onTextChange);
|
|
926
|
+
textField.on('returnPress', onReturnPress);
|
|
927
|
+
this._inlineTextFieldDetach = () => {
|
|
928
|
+
textField.off('textChange', onTextChange);
|
|
929
|
+
textField.off('returnPress', onReturnPress);
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/** `searchMode="inline"`: the SearchBar turns into an editable field in place, the app filters its own content from `searchChange` */
|
|
934
|
+
syncInlineSearch(appBar, searchBar) {
|
|
935
|
+
this._inlineOwner = new WeakRef(appBar);
|
|
936
|
+
searchBar.setOnClickListener(new android.view.View.OnClickListener({ onClick: () => this.openInlineSearch() }));
|
|
937
|
+
if (!this._inlineField) {
|
|
938
|
+
const context = this._context;
|
|
939
|
+
const field = new android.widget.LinearLayout(context);
|
|
940
|
+
field.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
|
941
|
+
field.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
|
942
|
+
field.setBackground(searchBar.getBackground()?.getConstantState()?.newDrawable()?.mutate() ?? null);
|
|
943
|
+
const edit = new android.widget.EditText(context);
|
|
944
|
+
edit.setBackground(null);
|
|
945
|
+
edit.setSingleLine(true);
|
|
946
|
+
edit.setTextSize(16);
|
|
947
|
+
edit.setPadding(0, 0, 0, 0);
|
|
948
|
+
edit.setImeOptions(android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH);
|
|
949
|
+
edit.addTextChangedListener(new android.text.TextWatcher({
|
|
950
|
+
beforeTextChanged: () => null,
|
|
951
|
+
onTextChanged: () => null,
|
|
952
|
+
afterTextChanged: (editable) => this.notifySearchChange(editable.toString())
|
|
953
|
+
}));
|
|
954
|
+
edit.setOnEditorActionListener(new android.widget.TextView.OnEditorActionListener({
|
|
955
|
+
onEditorAction: () => {
|
|
956
|
+
this.notifySearchSubmit(edit.getText().toString());
|
|
957
|
+
Utils.dismissSoftInput(edit);
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
}));
|
|
961
|
+
this._inlineBack = this.createInlineButton('abc_ic_ab_back_material', () => this.closeInlineSearch());
|
|
962
|
+
this._inlineClear = this.createInlineButton('abc_ic_clear_material', () => (this.getInlineText().length ? this.clearInlineText() : this.closeInlineSearch()));
|
|
963
|
+
field.addView(this._inlineBack);
|
|
964
|
+
field.addView(edit, new android.widget.LinearLayout.LayoutParams(0, WRAP_CONTENT, 1));
|
|
965
|
+
field.addView(this._inlineClear);
|
|
966
|
+
const appBarLayout = this._appBarLayout;
|
|
967
|
+
field.setVisibility(android.view.View.GONE);
|
|
968
|
+
appBarLayout.addView(field, searchBar.getLayoutParams());
|
|
969
|
+
this._inlineField = field;
|
|
970
|
+
this._inlineEdit = edit;
|
|
971
|
+
}
|
|
972
|
+
this.bindInlineTextField(appBar);
|
|
973
|
+
this.syncInlineColors(appBar);
|
|
974
|
+
this._inlineEdit.setHint(appBar.searchHint ?? null);
|
|
975
|
+
searchBar.setVisibility(this._inlineOpen ? android.view.View.GONE : android.view.View.VISIBLE);
|
|
976
|
+
this._inlineField.setVisibility(this._inlineOpen ? android.view.View.VISIBLE : android.view.View.GONE);
|
|
977
|
+
}
|
|
978
|
+
/** the colors of the theme are read again at each sync (dark / light switch): the field follows the bar */
|
|
979
|
+
syncInlineColors(appBar) {
|
|
980
|
+
const onBar = appBar.getOnBarColor();
|
|
981
|
+
findShapeDrawable(this._inlineField.getBackground())?.setFillColor(android.content.res.ColorStateList.valueOf(this.getSearchBarColor()));
|
|
982
|
+
this._inlineEdit.setTextColor(onBar);
|
|
983
|
+
this._inlineEdit.setHintTextColor(androidx.core.graphics.ColorUtils.setAlphaComponent(onBar, 153));
|
|
984
|
+
this._inlineBack.setColorFilter(onBar);
|
|
985
|
+
this._inlineClear.setColorFilter(onBar);
|
|
986
|
+
}
|
|
987
|
+
getSearchBarColor() {
|
|
988
|
+
return getThemeColor(this._searchBar, 'colorSurfaceContainerHigh', getThemeColor(this._searchBar, 'colorSurface', android.graphics.Color.WHITE));
|
|
989
|
+
}
|
|
990
|
+
/** cross fades the search bar and the editable field (same size and position) */
|
|
991
|
+
morphSearch(from, to) {
|
|
992
|
+
const fade = new android.transition.Fade();
|
|
993
|
+
fade.setDuration(250);
|
|
994
|
+
fade.setInterpolator(new android.view.animation.PathInterpolator(0.4, 0, 0.2, 1));
|
|
995
|
+
android.transition.TransitionManager.beginDelayedTransition(this._appBarLayout, fade);
|
|
996
|
+
from.setVisibility(android.view.View.GONE);
|
|
997
|
+
to.setVisibility(android.view.View.VISIBLE);
|
|
998
|
+
}
|
|
999
|
+
openInlineSearch() {
|
|
1000
|
+
if (this._inlineOpen || !this._inlineField) {
|
|
1001
|
+
return !!this._inlineOpen;
|
|
1002
|
+
}
|
|
1003
|
+
this._inlineOpen = true;
|
|
1004
|
+
const height = this._searchBar.getHeight();
|
|
1005
|
+
if (height > 0) {
|
|
1006
|
+
this._inlineField.setMinimumHeight(height);
|
|
1007
|
+
}
|
|
1008
|
+
this.morphSearch(this._searchBar, this._inlineField);
|
|
1009
|
+
if (this._inlineTextField) {
|
|
1010
|
+
this._inlineTextField.focus();
|
|
1011
|
+
Utils.android.showSoftInput(this._inlineTextField.nativeViewProtected);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
this._inlineEdit.requestFocus();
|
|
1015
|
+
Utils.android.showSoftInput(this._inlineEdit);
|
|
1016
|
+
}
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
/** returns true when an inline search was open */
|
|
1020
|
+
closeInlineSearch() {
|
|
1021
|
+
if (!this._inlineOpen) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
this._inlineOpen = false;
|
|
1025
|
+
this.dismissInlineKeyboard();
|
|
1026
|
+
this.clearInlineText();
|
|
1027
|
+
this.morphSearch(this._inlineField, this._searchBar);
|
|
1028
|
+
const bar = this._inlineOwner?.get();
|
|
1029
|
+
if (bar) {
|
|
1030
|
+
bar.searchText = '';
|
|
1031
|
+
bar.notify({ eventName: AppBarBase.searchCancelEvent, object: bar, text: '' });
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
/** the SearchBar reads the colors of the theme when it is created: they are applied again at each sync */
|
|
1036
|
+
syncSearchBarColors(searchBar) {
|
|
1037
|
+
findShapeDrawable(searchBar.getBackground())?.setFillColor(android.content.res.ColorStateList.valueOf(this.getSearchBarColor()));
|
|
1038
|
+
const onVariant = getThemeColor(searchBar, 'colorOnSurfaceVariant', android.graphics.Color.BLACK);
|
|
1039
|
+
searchBar.getTextView().setTextColor(onVariant);
|
|
1040
|
+
searchBar.getTextView().setHintTextColor(onVariant);
|
|
1041
|
+
searchBar.getNavigationIcon()?.mutate().setTint(onVariant);
|
|
1042
|
+
}
|
|
1043
|
+
/** without a title the SearchBar is the whole bar (M3 "search app bar"): it takes the navigation icon and the actions of the toolbar */
|
|
1044
|
+
syncSearchBarHost(appBar, searchBar) {
|
|
1045
|
+
const toolbar = appBar.nativeViewProtected;
|
|
1046
|
+
if (this._defaultSearchBarIcon === undefined) {
|
|
1047
|
+
this._defaultSearchBarIcon = searchBar.getNavigationIcon();
|
|
1048
|
+
}
|
|
1049
|
+
const hosted = !appBar.title && !appBar.subtitle && appBar.size === 'small';
|
|
1050
|
+
const menu = searchBar.getMenu();
|
|
1051
|
+
menu.clear();
|
|
1052
|
+
toolbar.setVisibility(hosted ? android.view.View.GONE : android.view.View.VISIBLE);
|
|
1053
|
+
if (!hosted) {
|
|
1054
|
+
searchBar.setNavigationIcon(this._defaultSearchBarIcon);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const owner = new WeakRef(appBar);
|
|
1058
|
+
searchBar.setNavigationIcon(toolbar.getNavigationIcon() ?? this._defaultSearchBarIcon);
|
|
1059
|
+
searchBar.setNavigationOnClickListener(new android.view.View.OnClickListener({
|
|
1060
|
+
onClick: () => owner.get()?.navigationButton?._raiseTap()
|
|
1061
|
+
}));
|
|
1062
|
+
const tint = android.content.res.ColorStateList.valueOf(appBar.getOnBarColor());
|
|
1063
|
+
const source = toolbar.getMenu();
|
|
1064
|
+
for (let index = 0; index < source.size(); index++) {
|
|
1065
|
+
const item = source.getItem(index);
|
|
1066
|
+
const copy = menu.add(item.getGroupId(), item.getItemId(), item.getOrder(), item.getTitle());
|
|
1067
|
+
copy.setIcon(item.getIcon());
|
|
1068
|
+
copy.setShowAsAction(android.view.MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
1069
|
+
androidx.core.view.MenuItemCompat.setIconTintList(copy, tint);
|
|
1070
|
+
}
|
|
1071
|
+
searchBar.setOnMenuItemClickListener(new androidx.appcompat.widget.Toolbar.OnMenuItemClickListener({
|
|
1072
|
+
onMenuItemClick: (menuItem) => {
|
|
1073
|
+
const items = (owner.get()?.actionItems?.getVisibleItems() ?? []);
|
|
1074
|
+
items.find((item) => item._getItemId() === menuItem.getItemId())?._raiseTap();
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
674
1079
|
/** `bottomItems`: a Material BottomAppBar, over the content, below the overlay */
|
|
675
1080
|
syncBottomBar(appBar, behavior) {
|
|
676
|
-
const items = (appBar?.bottomItems || []).filter((item) => !item.spacer);
|
|
1081
|
+
const items = (appBar?.bottomItems || []).filter((item) => !item.spacer && !item.search);
|
|
677
1082
|
const coordinator = this._coordinator;
|
|
678
|
-
|
|
1083
|
+
const floating = appBar?.bottomBarStyle !== 'bottomAppBar';
|
|
1084
|
+
// a bottomView only fits the floating toolbar: the BottomAppBar is a Toolbar with a menu
|
|
1085
|
+
if (floating && appBar?.bottomView && !appBar.bottomView.nativeViewProtected) {
|
|
1086
|
+
this.syncWhenLoaded(appBar.bottomView);
|
|
1087
|
+
}
|
|
1088
|
+
const hasContent = items.length > 0 || (floating && !!appBar?.bottomView?.nativeViewProtected);
|
|
1089
|
+
if (!hasContent || floating) {
|
|
679
1090
|
if (this._bottomBar) {
|
|
680
1091
|
this._bottomBar.setVisibility(android.view.View.GONE);
|
|
681
|
-
|
|
1092
|
+
if (!floating) {
|
|
1093
|
+
this.syncBottomInset(appBar, false, this._bottomBar);
|
|
1094
|
+
}
|
|
682
1095
|
}
|
|
1096
|
+
}
|
|
1097
|
+
if (!hasContent || !floating) {
|
|
1098
|
+
if (this._floatingBar) {
|
|
1099
|
+
this._floatingBar.setVisibility(android.view.View.GONE);
|
|
1100
|
+
this.syncBottomInset(appBar, false, this._floatingBar);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (!hasContent) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (floating) {
|
|
1107
|
+
this.syncFloatingBar(appBar, items, behavior);
|
|
683
1108
|
return;
|
|
684
1109
|
}
|
|
685
1110
|
const BottomAppBar = com.google.android.material.bottomappbar.BottomAppBar;
|
|
@@ -698,12 +1123,14 @@ export class AppPage extends Page {
|
|
|
698
1123
|
const menu = bar.getMenu();
|
|
699
1124
|
menu.clear();
|
|
700
1125
|
const owner = new WeakRef(appBar);
|
|
1126
|
+
const onBarList = android.content.res.ColorStateList.valueOf(appBar.getOnBarColor());
|
|
701
1127
|
items.forEach((item, index) => {
|
|
702
1128
|
const menuItem = menu.add(0, index, index, item.text || '');
|
|
703
1129
|
const icon = item.icon ? getNativeIcon(item.icon) : null;
|
|
704
1130
|
if (icon) {
|
|
705
1131
|
menuItem.setIcon(icon);
|
|
706
1132
|
}
|
|
1133
|
+
androidx.core.view.MenuItemCompat.setIconTintList(menuItem, item.color ? toColorStateList(item.color) : onBarList);
|
|
707
1134
|
menuItem.setShowAsAction(android.view.MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
708
1135
|
});
|
|
709
1136
|
attachBadges(bar, this._bottomBadges, items
|
|
@@ -712,20 +1139,109 @@ export class AppPage extends Page {
|
|
|
712
1139
|
bar.setOnMenuItemClickListener(new androidx.appcompat.widget.Toolbar.OnMenuItemClickListener({
|
|
713
1140
|
onMenuItemClick: (menuItem) => {
|
|
714
1141
|
const target = owner.get();
|
|
715
|
-
|
|
716
|
-
|
|
1142
|
+
const item = items[menuItem.getItemId()];
|
|
1143
|
+
if (target && item?.menu?.length) {
|
|
1144
|
+
showMenu({ anchor: { nativeViewProtected: bar }, items: item.menu, placement: { horizontal: 'end', vertical: 'above' } });
|
|
1145
|
+
}
|
|
1146
|
+
else if (target) {
|
|
1147
|
+
notifyBottomItemTap(target, item);
|
|
717
1148
|
}
|
|
718
1149
|
return true;
|
|
719
1150
|
}
|
|
720
1151
|
}));
|
|
721
|
-
this.syncBottomInset(appBar, true);
|
|
1152
|
+
this.syncBottomInset(appBar, true, bar);
|
|
1153
|
+
}
|
|
1154
|
+
/** `bottomBarStyle="floating"`: a Material 3 Expressive FloatingToolbarLayout (a pill over the content, centered above the gesture bar) holding icon buttons, or the `bottomView` of the bar */
|
|
1155
|
+
syncFloatingBar(appBar, items, behavior) {
|
|
1156
|
+
const coordinator = this._coordinator;
|
|
1157
|
+
const context = this._context;
|
|
1158
|
+
const density = Utils.layout.getDisplayDensity();
|
|
1159
|
+
const CoordinatorLayout = androidx.coordinatorlayout.widget.CoordinatorLayout;
|
|
1160
|
+
if (!this._floatingBar) {
|
|
1161
|
+
// the typings of the Material version of the plugin do not have the floating toolbar yet
|
|
1162
|
+
const FloatingToolbarLayout = com.google.android.material.floatingtoolbar.FloatingToolbarLayout;
|
|
1163
|
+
const bar = new FloatingToolbarLayout(context);
|
|
1164
|
+
const params = new CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
|
|
1165
|
+
params.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.CENTER_HORIZONTAL;
|
|
1166
|
+
const margin = Math.round(16 * density);
|
|
1167
|
+
params.setMargins(margin, margin, margin, margin);
|
|
1168
|
+
// below the overlay (a FAB, a panel)
|
|
1169
|
+
coordinator.addView(bar, coordinator.indexOfChild(this._nativeOverlay), params);
|
|
1170
|
+
// the insets are known once the bar is attached
|
|
1171
|
+
bar.addOnLayoutChangeListener(new android.view.View.OnLayoutChangeListener({
|
|
1172
|
+
onLayoutChange: () => this.applyFloatingBarMargin(bar)
|
|
1173
|
+
}));
|
|
1174
|
+
const holder = new android.widget.LinearLayout(context);
|
|
1175
|
+
holder.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
|
1176
|
+
holder.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
|
1177
|
+
bar.addView(holder, new android.widget.FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
|
|
1178
|
+
this._floatingBar = bar;
|
|
1179
|
+
this._floatingHolder = holder;
|
|
1180
|
+
}
|
|
1181
|
+
const bar = this._floatingBar;
|
|
1182
|
+
const holder = this._floatingHolder;
|
|
1183
|
+
const params = bar.getLayoutParams();
|
|
1184
|
+
this.applyFloatingBarMargin(bar);
|
|
1185
|
+
params.setBehavior(behavior === 'hide' ? new com.google.android.material.behavior.HideBottomViewOnScrollBehavior() : null);
|
|
1186
|
+
bar.setVisibility(android.view.View.VISIBLE);
|
|
1187
|
+
const background = bar.getBackground();
|
|
1188
|
+
if (background instanceof com.google.android.material.shape.MaterialShapeDrawable) {
|
|
1189
|
+
background.setFillColor(android.content.res.ColorStateList.valueOf(getThemeColor(bar, 'colorSurfaceContainer', getThemeColor(bar, 'colorSurface', android.graphics.Color.WHITE))));
|
|
1190
|
+
}
|
|
1191
|
+
holder.removeAllViews();
|
|
1192
|
+
const customView = appBar.bottomView?.nativeViewProtected;
|
|
1193
|
+
if (customView) {
|
|
1194
|
+
customView.getParent()?.removeView(customView);
|
|
1195
|
+
holder.addView(customView, new android.widget.LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
|
|
1196
|
+
this.syncBottomInset(appBar, true, bar);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const owner = new WeakRef(appBar);
|
|
1200
|
+
const ripple = new android.util.TypedValue();
|
|
1201
|
+
context.getTheme().resolveAttribute(16843868 /* android.R.attr.selectableItemBackgroundBorderless */, ripple, true);
|
|
1202
|
+
const size = Math.round(48 * density);
|
|
1203
|
+
const onBarList = android.content.res.ColorStateList.valueOf(appBar.getOnBarColor());
|
|
1204
|
+
items.forEach((item) => {
|
|
1205
|
+
const button = new android.widget.ImageButton(context);
|
|
1206
|
+
const icon = item.icon ? getNativeIcon(item.icon) : null;
|
|
1207
|
+
if (icon) {
|
|
1208
|
+
button.setImageDrawable(icon);
|
|
1209
|
+
}
|
|
1210
|
+
button.setImageTintList(item.color ? toColorStateList(item.color) : onBarList);
|
|
1211
|
+
button.setContentDescription(item.text || '');
|
|
1212
|
+
button.setBackground(androidx.core.content.ContextCompat.getDrawable(context, ripple.resourceId));
|
|
1213
|
+
button.setOnClickListener(new android.view.View.OnClickListener({
|
|
1214
|
+
onClick: () => {
|
|
1215
|
+
const target = owner.get();
|
|
1216
|
+
if (target && item.menu?.length) {
|
|
1217
|
+
showMenu({ anchor: { nativeViewProtected: button }, items: item.menu, placement: { horizontal: 'end', vertical: 'above' } });
|
|
1218
|
+
}
|
|
1219
|
+
else if (target) {
|
|
1220
|
+
notifyBottomItemTap(target, item);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}));
|
|
1224
|
+
holder.addView(button, new android.widget.LinearLayout.LayoutParams(size, size));
|
|
1225
|
+
});
|
|
1226
|
+
this.syncBottomInset(appBar, true, bar);
|
|
1227
|
+
}
|
|
1228
|
+
/** the coordinator consumes the insets: the floating bar is moved above the navigation bar by hand */
|
|
1229
|
+
applyFloatingBarMargin(bar) {
|
|
1230
|
+
const window = (Application.android.foregroundActivity || Application.android.startActivity)?.getWindow();
|
|
1231
|
+
const rootInsets = window ? androidx.core.view.ViewCompat.getRootWindowInsets(window.getDecorView()) : null;
|
|
1232
|
+
const navigationInset = rootInsets ? rootInsets.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars()).bottom : 0;
|
|
1233
|
+
const margin = Math.round(16 * Utils.layout.getDisplayDensity());
|
|
1234
|
+
const params = bar.getLayoutParams();
|
|
1235
|
+
if (params.bottomMargin !== margin + navigationInset || params.leftMargin !== margin) {
|
|
1236
|
+
params.setMargins(margin, margin, margin, margin + navigationInset);
|
|
1237
|
+
bar.setLayoutParams(params);
|
|
1238
|
+
}
|
|
722
1239
|
}
|
|
723
1240
|
/** the last items of the scrolling view must not stay under the bottom bar */
|
|
724
|
-
syncBottomInset(appBar, visible) {
|
|
725
|
-
const
|
|
726
|
-
const target = appBar?.scrollTarget?.nativeViewProtected;
|
|
1241
|
+
syncBottomInset(appBar, visible, bar) {
|
|
1242
|
+
const target = appBar?.getScrollTarget()?.nativeViewProtected ?? this._liftTarget;
|
|
727
1243
|
if (this._bottomInsetListener) {
|
|
728
|
-
|
|
1244
|
+
this._bottomInsetView?.removeOnLayoutChangeListener(this._bottomInsetListener);
|
|
729
1245
|
this._bottomInsetListener = null;
|
|
730
1246
|
}
|
|
731
1247
|
if (!target) {
|
|
@@ -738,6 +1254,8 @@ export class AppPage extends Page {
|
|
|
738
1254
|
}
|
|
739
1255
|
return;
|
|
740
1256
|
}
|
|
1257
|
+
// a floating bar also has margins around it
|
|
1258
|
+
const occupied = () => bar.getHeight() + (bar.getLayoutParams()?.bottomMargin ?? 0);
|
|
741
1259
|
const apply = () => {
|
|
742
1260
|
if (target['__appBarBottomPadding'] === undefined) {
|
|
743
1261
|
target['__appBarBottomPadding'] = target.getPaddingBottom();
|
|
@@ -745,22 +1263,23 @@ export class AppPage extends Page {
|
|
|
745
1263
|
if (target instanceof android.view.ViewGroup) {
|
|
746
1264
|
target.setClipToPadding(false);
|
|
747
1265
|
}
|
|
748
|
-
target.setPadding(target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(),
|
|
1266
|
+
target.setPadding(target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(), occupied());
|
|
749
1267
|
};
|
|
750
1268
|
this._bottomInsetListener = new android.view.View.OnLayoutChangeListener({
|
|
751
1269
|
onLayoutChange: () => {
|
|
752
|
-
if (target.getPaddingBottom() !==
|
|
1270
|
+
if (target.getPaddingBottom() !== occupied() && this._bottomInsetListener) {
|
|
753
1271
|
apply();
|
|
754
1272
|
}
|
|
755
1273
|
}
|
|
756
1274
|
});
|
|
1275
|
+
this._bottomInsetView = bar;
|
|
757
1276
|
bar.addOnLayoutChangeListener(this._bottomInsetListener);
|
|
758
1277
|
apply();
|
|
759
1278
|
}
|
|
760
1279
|
/** translucent: the first items of the scrolling view must start below the bar: its top padding follows the height of the bar */
|
|
761
1280
|
syncTranslucentInset(appBar, translucent) {
|
|
762
1281
|
const appBarLayout = this._appBarLayout;
|
|
763
|
-
const target = appBar?.
|
|
1282
|
+
const target = appBar?.getScrollTarget()?.nativeViewProtected;
|
|
764
1283
|
if (this._insetListener) {
|
|
765
1284
|
appBarLayout.removeOnLayoutChangeListener(this._insetListener);
|
|
766
1285
|
this._insetListener = null;
|