mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 11:01:21 +08:00

BREAKING CHANGES: `Application.orientation` is no longer a function. Migration: Remove `()` from the `Application.orientation()` call: ```diff import { Application } from "@nativescript/core"; -console.log(Application.orientation()); +console.log(Application.orientation); ``` `Application.systemAppearance` is no longer a function. Migration: Remove `()` from the `Application.systemAppearance()` call: ```diff import { Application } from "@nativescript/core"; -console.log(Application.systemAppearance()); +console.log(Application.systemAppearance); ```
618 lines
18 KiB
TypeScript
618 lines
18 KiB
TypeScript
import { AndroidActionItemSettings, AndroidActionBarSettings as AndroidActionBarSettingsDefinition, ActionItem as ActionItemDefinition } from '.';
|
|
import { ActionItemBase, ActionBarBase, isVisible, flatProperty, traceMissingIcon, androidContentInsetLeftProperty, androidContentInsetRightProperty } from './action-bar-common';
|
|
import { View } from '../core/view';
|
|
import { Color } from '../../color';
|
|
import { layout, RESOURCE_PREFIX, isFontIconURI } from '../../utils';
|
|
import { colorProperty } from '../styling/style-properties';
|
|
import { ImageSource } from '../../image-source';
|
|
import { Application } from '../../application';
|
|
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../accessibility';
|
|
import type { Background } from '../styling/background';
|
|
import { SDK_VERSION } from '../../utils/constants';
|
|
|
|
export * from './action-bar-common';
|
|
|
|
const R_ID_HOME = 0x0102002c;
|
|
const ACTION_ITEM_ID_OFFSET = 10000;
|
|
const DEFAULT_ELEVATION = 4;
|
|
|
|
let AppCompatTextView;
|
|
let actionItemIdGenerator = ACTION_ITEM_ID_OFFSET;
|
|
function generateItemId(): number {
|
|
actionItemIdGenerator++;
|
|
|
|
return actionItemIdGenerator;
|
|
}
|
|
|
|
function loadActionIconDrawableOrResourceId(item: ActionItemDefinition): any {
|
|
const itemIcon = item.icon;
|
|
const itemStyle = item.style;
|
|
let drawableOrId = null;
|
|
|
|
if (isFontIconURI(itemIcon)) {
|
|
const fontIconCode = itemIcon.split('//')[1];
|
|
const font = itemStyle.fontInternal;
|
|
const color = itemStyle.color;
|
|
const is = ImageSource.fromFontIconCodeSync(fontIconCode, font, color);
|
|
|
|
if (is && is.android) {
|
|
drawableOrId = new android.graphics.drawable.BitmapDrawable(appResources, is.android);
|
|
}
|
|
} else {
|
|
drawableOrId = getDrawableOrResourceId(itemIcon, appResources);
|
|
}
|
|
|
|
if (!drawableOrId) {
|
|
traceMissingIcon(itemIcon);
|
|
}
|
|
|
|
return drawableOrId;
|
|
}
|
|
|
|
interface MenuItemClickListener {
|
|
new (owner: ActionBar): androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
|
|
}
|
|
|
|
let appResources: android.content.res.Resources;
|
|
let MenuItemClickListener: MenuItemClickListener;
|
|
|
|
function initializeMenuItemClickListener(): void {
|
|
if (MenuItemClickListener) {
|
|
return;
|
|
}
|
|
|
|
AppCompatTextView = androidx.appcompat.widget.AppCompatTextView;
|
|
|
|
@NativeClass
|
|
@Interfaces([androidx.appcompat.widget.Toolbar.OnMenuItemClickListener])
|
|
class MenuItemClickListenerImpl extends java.lang.Object implements androidx.appcompat.widget.Toolbar.OnMenuItemClickListener {
|
|
constructor(public owner: ActionBar) {
|
|
super();
|
|
|
|
return global.__native(this);
|
|
}
|
|
|
|
onMenuItemClick(item: android.view.MenuItem): boolean {
|
|
const itemId = item.getItemId();
|
|
|
|
return this.owner._onAndroidItemSelected(itemId);
|
|
}
|
|
}
|
|
|
|
MenuItemClickListener = MenuItemClickListenerImpl;
|
|
appResources = Application.android.context.getResources();
|
|
}
|
|
|
|
export class ActionItem extends ActionItemBase {
|
|
private _androidPosition: AndroidActionItemSettings = {
|
|
position: 'actionBar',
|
|
systemIcon: undefined,
|
|
};
|
|
|
|
private _itemId;
|
|
constructor() {
|
|
super();
|
|
this._itemId = generateItemId();
|
|
}
|
|
|
|
// @ts-ignore
|
|
public get android(): AndroidActionItemSettings {
|
|
return this._androidPosition;
|
|
}
|
|
public set android(value: AndroidActionItemSettings) {
|
|
throw new Error('ActionItem.android is read-only');
|
|
}
|
|
|
|
public _getItemId() {
|
|
return this._itemId;
|
|
}
|
|
}
|
|
|
|
export class AndroidActionBarSettings implements AndroidActionBarSettingsDefinition {
|
|
private _actionBar: ActionBar;
|
|
private _icon: string;
|
|
private _iconVisibility: 'auto' | 'never' | 'always' = 'auto';
|
|
|
|
constructor(actionBar: ActionBar) {
|
|
this._actionBar = actionBar;
|
|
}
|
|
|
|
public get icon(): string {
|
|
return this._icon;
|
|
}
|
|
public set icon(value: string) {
|
|
if (value !== this._icon) {
|
|
this._icon = value;
|
|
this._actionBar._onIconPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public get iconVisibility(): 'auto' | 'never' | 'always' {
|
|
return this._iconVisibility;
|
|
}
|
|
public set iconVisibility(value: 'auto' | 'never' | 'always') {
|
|
if (value !== this._iconVisibility) {
|
|
this._iconVisibility = value;
|
|
this._actionBar._onIconPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
export class NavigationButton extends ActionItem {}
|
|
|
|
export class ActionBar extends ActionBarBase {
|
|
private _android: AndroidActionBarSettings;
|
|
public nativeViewProtected: androidx.appcompat.widget.Toolbar;
|
|
|
|
constructor() {
|
|
super();
|
|
this._android = new AndroidActionBarSettings(this);
|
|
}
|
|
|
|
get android(): AndroidActionBarSettings {
|
|
return this._android;
|
|
}
|
|
|
|
public _addChildFromBuilder(name: string, value: any) {
|
|
if (value instanceof NavigationButton) {
|
|
this.navigationButton = value;
|
|
} else if (value instanceof ActionItem) {
|
|
this.actionItems?.addItem(value);
|
|
} else if (value instanceof View) {
|
|
this.titleView = value;
|
|
}
|
|
}
|
|
|
|
public createNativeView() {
|
|
return new androidx.appcompat.widget.Toolbar(this._context);
|
|
}
|
|
|
|
public initNativeView(): void {
|
|
super.initNativeView();
|
|
const nativeView = this.nativeViewProtected;
|
|
initializeMenuItemClickListener();
|
|
const menuItemClickListener = new MenuItemClickListener(this);
|
|
nativeView.setOnMenuItemClickListener(menuItemClickListener);
|
|
(<any>nativeView).menuItemClickListener = menuItemClickListener;
|
|
}
|
|
|
|
public disposeNativeView() {
|
|
if ((<any>this.nativeViewProtected)?.menuItemClickListener) {
|
|
(<any>this.nativeViewProtected).menuItemClickListener.owner = null;
|
|
}
|
|
super.disposeNativeView();
|
|
}
|
|
|
|
public onLoaded() {
|
|
super.onLoaded();
|
|
this.update();
|
|
}
|
|
|
|
public update() {
|
|
if (!this.nativeViewProtected) {
|
|
return;
|
|
}
|
|
|
|
const page = this.page;
|
|
if (!page.frame || !page.frame._getNavBarVisible(page)) {
|
|
this.nativeViewProtected.setVisibility(android.view.View.GONE);
|
|
|
|
// If action bar is hidden - no need to fill it with items.
|
|
return;
|
|
}
|
|
|
|
this.nativeViewProtected.setVisibility(android.view.View.VISIBLE);
|
|
|
|
// Add menu items
|
|
this._addActionItems();
|
|
|
|
// Set title
|
|
this._updateTitleAndTitleView();
|
|
|
|
// Set home icon
|
|
this._updateIcon();
|
|
|
|
// Set navigation button
|
|
this._updateNavigationButton();
|
|
}
|
|
|
|
public _applyBackground(background: Background, isBorderDrawable, onlyColor: boolean, backgroundDrawable: any) {
|
|
const nativeView = this.nativeViewProtected;
|
|
if (backgroundDrawable && onlyColor && SDK_VERSION >= 21) {
|
|
if (isBorderDrawable && (<any>nativeView)._cachedDrawable) {
|
|
backgroundDrawable = (<any>nativeView)._cachedDrawable;
|
|
// we need to duplicate the drawable or we lose the "default" cached drawable
|
|
const constantState = backgroundDrawable.getConstantState();
|
|
if (constantState) {
|
|
try {
|
|
backgroundDrawable = constantState.newDrawable(nativeView.getResources());
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
}
|
|
nativeView.setBackground(backgroundDrawable);
|
|
}
|
|
|
|
const backgroundColor = ((<any>backgroundDrawable).backgroundColor = background.color.android);
|
|
backgroundDrawable.mutate();
|
|
backgroundDrawable.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN);
|
|
backgroundDrawable.invalidateSelf(); // Make sure the drawable is invalidated. Android forgets to invalidate it in some cases: toolbar
|
|
(<any>backgroundDrawable).backgroundColor = backgroundColor;
|
|
} else {
|
|
super._applyBackground(background, isBorderDrawable, onlyColor, backgroundDrawable);
|
|
}
|
|
}
|
|
|
|
public _onAndroidItemSelected(itemId: number): boolean {
|
|
// Handle home button
|
|
if (this.navigationButton && itemId === R_ID_HOME) {
|
|
this.navigationButton._raiseTap();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Find item with the right ID;
|
|
let menuItem: ActionItem = undefined;
|
|
const items = this.actionItems?.getItems() ?? [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
if ((<ActionItem>items[i])._getItemId() === itemId) {
|
|
menuItem = <ActionItem>items[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (menuItem) {
|
|
menuItem._raiseTap();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public _updateNavigationButton() {
|
|
const navButton = this.navigationButton;
|
|
if (navButton && isVisible(navButton)) {
|
|
const systemIcon = navButton.android.systemIcon;
|
|
if (systemIcon !== undefined) {
|
|
// Try to look in the system resources.
|
|
const systemResourceId = getSystemResourceId(systemIcon);
|
|
if (systemResourceId) {
|
|
this.nativeViewProtected.setNavigationIcon(systemResourceId);
|
|
}
|
|
} else if (navButton.icon) {
|
|
const drawableOrId = loadActionIconDrawableOrResourceId(navButton);
|
|
if (drawableOrId) {
|
|
this.nativeViewProtected.setNavigationIcon(drawableOrId);
|
|
}
|
|
}
|
|
|
|
// Set navigation content description, used by screen readers for the vision-impaired users
|
|
this.nativeViewProtected.setNavigationContentDescription(navButton.text || null);
|
|
|
|
const navBtn = new WeakRef(navButton);
|
|
this.nativeViewProtected.setNavigationOnClickListener(
|
|
new android.view.View.OnClickListener({
|
|
onClick: function (v) {
|
|
const owner = navBtn?.get();
|
|
if (owner) {
|
|
owner._raiseTap();
|
|
}
|
|
},
|
|
})
|
|
);
|
|
} else {
|
|
this.nativeViewProtected.setNavigationIcon(null);
|
|
}
|
|
}
|
|
|
|
public _updateIcon() {
|
|
const visibility = getIconVisibility(this.android.iconVisibility);
|
|
if (visibility) {
|
|
const icon = this.android.icon;
|
|
if (icon !== undefined) {
|
|
const drawableOrId = getDrawableOrResourceId(icon, appResources);
|
|
if (drawableOrId) {
|
|
this.nativeViewProtected.setLogo(drawableOrId);
|
|
} else {
|
|
traceMissingIcon(icon);
|
|
}
|
|
} else {
|
|
const defaultIcon = Application.android.nativeApp.getApplicationInfo().icon;
|
|
this.nativeViewProtected.setLogo(defaultIcon);
|
|
}
|
|
} else {
|
|
this.nativeViewProtected.setLogo(null);
|
|
}
|
|
}
|
|
|
|
public _updateTitleAndTitleView(): void {
|
|
if (!this.titleView) {
|
|
// No title view - show the title
|
|
const title = this.title;
|
|
if (title !== undefined) {
|
|
this.nativeViewProtected.setTitle(title);
|
|
} else {
|
|
const appContext = Application.android.context;
|
|
const appInfo = appContext.getApplicationInfo();
|
|
const appLabel = appContext.getPackageManager().getApplicationLabel(appInfo);
|
|
if (appLabel) {
|
|
this.nativeViewProtected.setTitle(appLabel);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update content description for the screen reader.
|
|
updateContentDescription(this, true);
|
|
}
|
|
|
|
public _addActionItems() {
|
|
const menu = this.nativeViewProtected.getMenu();
|
|
const items = this.actionItems?.getVisibleItems() ?? [];
|
|
|
|
menu.clear();
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = <ActionItem>items[i];
|
|
const menuItem = menu.add(android.view.Menu.NONE, item._getItemId(), android.view.Menu.NONE, item.text + '');
|
|
|
|
if (item.actionView && item.actionView.android) {
|
|
// With custom action view, the menuitem cannot be displayed in a popup menu.
|
|
item.android.position = 'actionBar';
|
|
menuItem.setActionView(item.actionView.android);
|
|
ActionBar._setOnClickListener(item);
|
|
} else if (item.android.systemIcon) {
|
|
// Try to look in the system resources.
|
|
const systemResourceId = getSystemResourceId(item.android.systemIcon);
|
|
if (systemResourceId) {
|
|
menuItem.setIcon(systemResourceId);
|
|
}
|
|
} else if (item.icon) {
|
|
const drawableOrId = loadActionIconDrawableOrResourceId(item);
|
|
if (drawableOrId) {
|
|
menuItem.setIcon(drawableOrId);
|
|
}
|
|
}
|
|
|
|
const showAsAction = getShowAsAction(item);
|
|
menuItem.setShowAsAction(showAsAction);
|
|
}
|
|
}
|
|
|
|
private static _setOnClickListener(item: ActionItem): void {
|
|
const weakRef = new WeakRef(item);
|
|
item.actionView.android.setOnClickListener(
|
|
new android.view.View.OnClickListener({
|
|
onClick: function (v: android.view.View) {
|
|
const owner = weakRef?.get();
|
|
if (owner) {
|
|
owner._raiseTap();
|
|
}
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
public _onTitlePropertyChanged() {
|
|
if (this.nativeViewProtected) {
|
|
this._updateTitleAndTitleView();
|
|
}
|
|
}
|
|
|
|
public _onIconPropertyChanged() {
|
|
if (this.nativeViewProtected) {
|
|
this._updateIcon();
|
|
}
|
|
}
|
|
|
|
public _addViewToNativeVisualTree(child: View, atIndex: number = Number.MAX_VALUE): boolean {
|
|
super._addViewToNativeVisualTree(child);
|
|
|
|
if (this.nativeViewProtected && child.nativeViewProtected) {
|
|
if (atIndex >= this.nativeViewProtected.getChildCount()) {
|
|
this.nativeViewProtected.addView(child.nativeViewProtected);
|
|
} else {
|
|
this.nativeViewProtected.addView(child.nativeViewProtected, atIndex);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public _removeViewFromNativeVisualTree(child: View): void {
|
|
super._removeViewFromNativeVisualTree(child);
|
|
|
|
if (this.nativeViewProtected && child.nativeViewProtected) {
|
|
this.nativeViewProtected.removeView(child.nativeViewProtected);
|
|
}
|
|
}
|
|
|
|
[colorProperty.getDefault](): number {
|
|
const nativeView = this.nativeViewProtected;
|
|
if (!defaultTitleTextColor) {
|
|
let tv: android.widget.TextView = getAppCompatTextView(nativeView);
|
|
if (!tv) {
|
|
const title = nativeView.getTitle();
|
|
// setTitle will create AppCompatTextView internally;
|
|
nativeView.setTitle('');
|
|
tv = getAppCompatTextView(nativeView);
|
|
if (title) {
|
|
// restore title.
|
|
nativeView.setTitle(title);
|
|
}
|
|
}
|
|
|
|
// Fallback to hardcoded falue if we don't find TextView instance...
|
|
// using new TextView().getTextColors().getDefaultColor() returns different value: -1979711488
|
|
defaultTitleTextColor = tv ? tv.getTextColors().getDefaultColor() : -570425344;
|
|
}
|
|
|
|
return defaultTitleTextColor;
|
|
}
|
|
[colorProperty.setNative](value: number | Color) {
|
|
const color = value instanceof Color ? value.android : value;
|
|
this.nativeViewProtected.setTitleTextColor(color);
|
|
}
|
|
|
|
[flatProperty.setNative](value: boolean) {
|
|
const compat = <any>androidx.core.view.ViewCompat;
|
|
if (compat.setElevation) {
|
|
if (value) {
|
|
compat.setElevation(this.nativeViewProtected, 0);
|
|
} else {
|
|
const val = DEFAULT_ELEVATION * layout.getDisplayDensity();
|
|
compat.setElevation(this.nativeViewProtected, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
[androidContentInsetLeftProperty.setNative]() {
|
|
if (SDK_VERSION >= 21) {
|
|
this.nativeViewProtected.setContentInsetsAbsolute(this.effectiveContentInsetLeft, this.effectiveContentInsetRight);
|
|
}
|
|
}
|
|
|
|
[androidContentInsetRightProperty.setNative]() {
|
|
if (SDK_VERSION >= 21) {
|
|
this.nativeViewProtected.setContentInsetsAbsolute(this.effectiveContentInsetLeft, this.effectiveContentInsetRight);
|
|
}
|
|
}
|
|
|
|
public accessibilityScreenChanged(): void {
|
|
if (!isAccessibilityServiceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const nativeView = this.nativeViewProtected;
|
|
if (!nativeView) {
|
|
return;
|
|
}
|
|
|
|
const originalFocusableState = SDK_VERSION >= 26 && nativeView.getFocusable();
|
|
const originalImportantForAccessibility = nativeView.getImportantForAccessibility();
|
|
const originalIsAccessibilityHeading = SDK_VERSION >= 28 && nativeView.isAccessibilityHeading();
|
|
|
|
try {
|
|
nativeView.setFocusable(false);
|
|
nativeView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
|
|
let announceView: android.view.View | null = null;
|
|
|
|
const numChildren = nativeView.getChildCount();
|
|
for (let i = 0; i < numChildren; i += 1) {
|
|
const childView = nativeView.getChildAt(i);
|
|
if (!childView) {
|
|
continue;
|
|
}
|
|
|
|
childView.setFocusable(true);
|
|
if (childView instanceof androidx.appcompat.widget.AppCompatTextView) {
|
|
announceView = childView;
|
|
if (SDK_VERSION >= 28) {
|
|
announceView.setAccessibilityHeading(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!announceView) {
|
|
announceView = nativeView;
|
|
}
|
|
|
|
announceView.setFocusable(true);
|
|
announceView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
|
|
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
|
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setTimeout(() => {
|
|
// Reset status after the focus have been reset.
|
|
const localNativeView = this.nativeViewProtected;
|
|
if (!localNativeView) {
|
|
return;
|
|
}
|
|
|
|
if (SDK_VERSION >= 28) {
|
|
nativeView.setAccessibilityHeading(originalIsAccessibilityHeading);
|
|
}
|
|
|
|
if (SDK_VERSION >= 26) {
|
|
localNativeView.setFocusable(originalFocusableState);
|
|
}
|
|
|
|
localNativeView.setImportantForAccessibility(originalImportantForAccessibility);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAppCompatTextView(toolbar: androidx.appcompat.widget.Toolbar): typeof AppCompatTextView {
|
|
for (let i = 0, count = toolbar.getChildCount(); i < count; i++) {
|
|
const child = toolbar.getChildAt(i);
|
|
if (child instanceof AppCompatTextView) {
|
|
return child;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
ActionBar.prototype.recycleNativeView = 'auto';
|
|
|
|
let defaultTitleTextColor: number;
|
|
|
|
function getDrawableOrResourceId(icon: string, resources: android.content.res.Resources): any {
|
|
if (typeof icon !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
let result = null;
|
|
if (icon.indexOf(RESOURCE_PREFIX) === 0) {
|
|
const resourceId: number = resources.getIdentifier(icon.substr(RESOURCE_PREFIX.length), 'drawable', Application.android.packageName);
|
|
if (resourceId > 0) {
|
|
result = resourceId;
|
|
}
|
|
} else {
|
|
let drawable: android.graphics.drawable.BitmapDrawable;
|
|
const is = ImageSource.fromFileOrResourceSync(icon);
|
|
if (is) {
|
|
drawable = new android.graphics.drawable.BitmapDrawable(appResources, is.android);
|
|
}
|
|
|
|
result = drawable;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getShowAsAction(menuItem: ActionItem): number {
|
|
switch (menuItem.android.position) {
|
|
case 'actionBarIfRoom':
|
|
return android.view.MenuItem.SHOW_AS_ACTION_IF_ROOM;
|
|
|
|
case 'popup':
|
|
return android.view.MenuItem.SHOW_AS_ACTION_NEVER;
|
|
|
|
case 'actionBar':
|
|
default:
|
|
return android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
|
|
}
|
|
}
|
|
|
|
function getIconVisibility(iconVisibility: string): boolean {
|
|
switch (iconVisibility) {
|
|
case 'always':
|
|
return true;
|
|
|
|
case 'auto':
|
|
case 'never':
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getSystemResourceId(systemIcon: string): number {
|
|
return android.content.res.Resources.getSystem().getIdentifier(systemIcon, 'drawable', 'android');
|
|
}
|