From ecb844f9f277c04d0eb34e76472e37978eb2e125 Mon Sep 17 00:00:00 2001 From: Vasko Date: Tue, 31 Mar 2020 20:51:30 +0300 Subject: [PATCH] chore: addimg tabbable component android --- api-reports/NativeScript.api.md | 25 +- .../bottom-navigation.android.ts | 499 +----------------- .../bottom-navigation/bottom-navigation.d.ts | 4 +- nativescript-core/ui/index.d.ts | 1 + nativescript-core/ui/index.ts | 1 + .../ui/tabbable-component/package.json | 5 + .../tabbable-component.android.ts | 486 +++++++++++++++++ .../tabbable-component.d.ts | 58 ++ .../tabbable-component.ios.ts | 6 + .../ui/tabs/tabs-common.android.ts | 41 ++ nativescript-core/ui/tabs/tabs-common.d.ts | 33 ++ .../{tabs-common.ts => tabs-common.ios.ts} | 4 +- nativescript-core/ui/tabs/tabs.android.ts | 484 +---------------- nativescript-core/ui/tabs/tabs.d.ts | 7 +- .../widgets/BottomNavigationBar.java | 2 +- .../org/nativescript/widgets/TabbableBar.java | 33 ++ .../org/nativescript/widgets/TabsBar.java | 2 +- .../android/org.nativescript.widgets.d.ts | 18 +- 18 files changed, 757 insertions(+), 952 deletions(-) create mode 100644 nativescript-core/ui/tabbable-component/package.json create mode 100644 nativescript-core/ui/tabbable-component/tabbable-component.android.ts create mode 100644 nativescript-core/ui/tabbable-component/tabbable-component.d.ts create mode 100644 nativescript-core/ui/tabbable-component/tabbable-component.ios.ts create mode 100644 nativescript-core/ui/tabs/tabs-common.android.ts create mode 100644 nativescript-core/ui/tabs/tabs-common.d.ts rename nativescript-core/ui/tabs/{tabs-common.ts => tabs-common.ios.ts} (88%) create mode 100644 tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabbableBar.java diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index f8662a406..39a4b74a1 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -332,7 +332,7 @@ export interface BackstackEntry { } // @public -export class BottomNavigation extends TabNavigationBase { +export class BottomNavigation extends TabbableComponent { android: any /* android.view.View */; ios: any /* UITabBarController */; @@ -2195,6 +2195,26 @@ export class Switch extends View { offBackgroundColor: Color; } +// @public +export abstract class TabbableComponent extends TabNavigationBase { + android: any /* android.view.View */; + + ios: any /* UITabBarController */; + + items: Array; + + on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + + // Warning: (ae-forgotten-export) The symbol "SelectedIndexChangedEventData" needs to be exported by the entry point index.d.ts + on(event: "selectedIndexChanged", callback: (args: SelectedIndexChangedEventData_2) => void, thisArg?: any); + + selectedIndex: number; + + public static selectedIndexChangedEvent: string; + + tabStrip: TabStrip; +} + // @public export class TabContentItem extends ContentView { // (undocumented) @@ -2237,7 +2257,6 @@ export class TabNavigationBase extends View { on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); - // Warning: (ae-forgotten-export) The symbol "SelectedIndexChangedEventData" needs to be exported by the entry point index.d.ts on(event: "selectedIndexChanged", callback: (args: SelectedIndexChangedEventData_2) => void, thisArg?: any); onItemsChanged(oldItems: TabContentItem[], newItems: TabContentItem[]): void; @@ -2282,7 +2301,7 @@ export class TabNavigationBase extends View { } // @public -export class Tabs extends TabNavigationBase { +export class Tabs extends TabbableComponent { android: any /* android.view.View */; ios: any /* UITabBarController */; diff --git a/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts b/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts index 26e4a4989..7e537aa0d 100644 --- a/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts +++ b/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts @@ -6,15 +6,10 @@ import { TextTransform } from "../text-base"; // Requires import * as application from "../../application"; -import { ImageSource } from "../../image-source"; -import { ad, isFontIconURI, layout, RESOURCE_PREFIX } from "../../utils/utils"; +import { ad, layout } from "../../utils/utils"; import { Color, CSSType } from "../core/view"; -import { Frame, View } from "../frame"; -import { Font } from "../styling/font"; -import { - getIconSpecSize, itemsProperty, selectedIndexProperty, TabNavigationBase, tabStripProperty -} from "../tab-navigation-base/tab-navigation-base"; -import { getTransformedText } from "../text-base"; +import { View } from "../frame"; +import { TabbableComponent } from "../tabbable-component/tabbable-component.android"; // TODO: Impl trace // import { isEnabled as traceEnabled, write as traceWrite } from "../../../trace"; @@ -36,11 +31,6 @@ let BottomNavigationBar: any; let AttachStateChangeListener: any; let appResources: android.content.res.Resources; -class IconInfo { - drawable: android.graphics.drawable.BitmapDrawable; - height: number; -} - function makeFragmentName(viewId: number, id: number): string { return "android:bottomnavigation:" + viewId + ":" + id; } @@ -239,29 +229,16 @@ function setElevation(bottomNavigationBar: org.nativescript.widgets.BottomNaviga export const tabs = new Array>(); -function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) { - const rangeStart = Math.max(0, index - eps); - const rangeEnd = Math.min(index + eps, lastIndex); - for (let i = rangeStart; i <= rangeEnd; i++) { - callback(i); - } -} - @CSSType("BottomNavigation") -export class BottomNavigation extends TabNavigationBase { +export class BottomNavigation extends TabbableComponent { private _contentView: org.nativescript.widgets.ContentLayout; - private _contentViewId: number = -1; - private _bottomNavigationBar: org.nativescript.widgets.BottomNavigationBar; private _currentFragment: androidx.fragment.app.Fragment; private _currentTransaction: androidx.fragment.app.FragmentTransaction; private _attachedToWindow = false; - public _originalBackground: any; - private _textTransform: TextTransform = "none"; - private _selectedItemColor: Color; - private _unSelectedItemColor: Color; constructor() { super(); + this._textTransform = "none"; tabs.push(new WeakRef(this)); } @@ -269,24 +246,16 @@ export class BottomNavigation extends TabNavigationBase { return true; } - public onItemsChanged(oldItems: TabContentItem[], newItems: TabContentItem[]): void { - super.onItemsChanged(oldItems, newItems); + public getBottomNavigationBar(): org.nativescript.widgets.BottomNavigationBar { + return this.nativeBar; + } - if (oldItems) { - oldItems.forEach((item: TabContentItem, i, arr) => { - (item).index = 0; - (item).tabItemSpec = null; - item.setNativeView(null); - }); - } + protected setNativeItems(items: Array) { + this.getBottomNavigationBar().setItems(items); } public createNativeView() { initializeNativeClasses(); - // if (traceEnabled()) { - // traceWrite("BottomNavigation._createUI(" + this + ");", traceCategory); - // } - const context: android.content.Context = this._context; const nativeView = new org.nativescript.widgets.GridLayout(context); @@ -322,83 +291,40 @@ export class BottomNavigation extends TabNavigationBase { public initNativeView(): void { super.initNativeView(); - if (this._contentViewId < 0) { - this._contentViewId = android.view.View.generateViewId(); - } + this.nativeViewProtected.addOnAttachStateChangeListener(AttachStateChangeListener); + this.nativeViewProtected[ownerSymbol] = this; - const nativeView: any = this.nativeViewProtected; - - nativeView.addOnAttachStateChangeListener(AttachStateChangeListener); - nativeView[ownerSymbol] = this; - - this._contentView = (nativeView).contentView; + this._contentView = (this.nativeViewProtected).contentView; this._contentView.setId(this._contentViewId); - this._bottomNavigationBar = (nativeView).bottomNavigationBar; - (this._bottomNavigationBar).owner = this; - if (this.tabStrip) { - this.tabStrip.setNativeView(this._bottomNavigationBar); + this.tabStrip.setNativeView(this.nativeBar); } } - public _loadUnloadTabItems(newIndex: number) { - const items = this.items; - const lastIndex = this.items.length - 1; - const offsideItems = 0; - - let toUnload = []; - let toLoad = []; - - iterateIndexRange(newIndex, offsideItems, lastIndex, (i) => toLoad.push(i)); - - items.forEach((item, i) => { - const indexOfI = toLoad.indexOf(i); - if (indexOfI < 0) { - toUnload.push(i); - } - }); - - toUnload.forEach(index => { - const item = items[index]; - if (items[index]) { - item.unloadView(item.content); - } - }); - - const newItem = items[newIndex]; - const selectedView = newItem && newItem.content; - if (selectedView instanceof Frame) { - selectedView._pushInFrameStackRecursive(); - } - - toLoad.forEach(index => { - const item = items[index]; - if (this.isLoaded && items[index]) { - item.loadView(item.content); - } - }); - } - public onLoaded(): void { super.onLoaded(); - if (this._originalBackground) { - this.backgroundColor = null; - this.backgroundColor = this._originalBackground; - this._originalBackground = null; - } - if (this.tabStrip) { this.setTabStripItems(this.tabStrip.items); } else { // manually set the visibility, so that the grid layout remeasures - this._bottomNavigationBar.setVisibility(android.view.View.GONE); + this.nativeBar.setVisibility(android.view.View.GONE); } this.changeTab(this.selectedIndex); } + public onUnloaded(): void { + super.onUnloaded(); + + const fragmentToDetach = this._currentFragment; + if (fragmentToDetach) { + this.destroyItem((fragmentToDetach).index, fragmentToDetach); + this.commitCurrentTransaction(); + } + } + _onAttachedToWindow(): void { super._onAttachedToWindow(); @@ -418,23 +344,9 @@ export class BottomNavigation extends TabNavigationBase { this._attachedToWindow = false; } - public onUnloaded(): void { - super.onUnloaded(); - - if (this.tabStrip) { - this.setTabStripItems(null); - } - - const fragmentToDetach = this._currentFragment; - if (fragmentToDetach) { - this.destroyItem((fragmentToDetach).index, fragmentToDetach); - this.commitCurrentTransaction(); - } - } - public disposeNativeView() { - this._bottomNavigationBar.setItems(null); - this._bottomNavigationBar = null; + this.setNativeItems(null); + this.nativeBar = null; this.nativeViewProtected.removeOnAttachStateChangeListener(AttachStateChangeListener); this.nativeViewProtected[ownerSymbol] = null; @@ -442,26 +354,6 @@ export class BottomNavigation extends TabNavigationBase { super.disposeNativeView(); } - public _onRootViewReset(): void { - super._onRootViewReset(); - - // call this AFTER the super call to ensure descendants apply their rootview-reset logic first - // i.e. in a scenario with tab frames let the frames cleanup their fragments first, and then - // cleanup the tab fragments to avoid - // android.content.res.Resources$NotFoundException: Unable to find resource ID #0xfffffff6 - this.disposeTabFragments(); - } - - private disposeTabFragments(): void { - const fragmentManager = this._getFragmentManager(); - const transaction = fragmentManager.beginTransaction(); - for (let fragment of (>fragmentManager.getFragments().toArray())) { - transaction.remove(fragment); - } - - transaction.commitNowAllowingStateLoss(); - } - private get currentTransaction(): androidx.fragment.app.FragmentTransaction { if (!this._currentTransaction) { const fragmentManager = this._getFragmentManager(); @@ -555,344 +447,11 @@ export class BottomNavigation extends TabNavigationBase { } } - private setTabStripItems(items: Array) { - if (!this.tabStrip || !items) { - this._bottomNavigationBar.setItems(null); - - return; - } - - const tabItems = new Array(); - items.forEach((tabStripItem, i, arr) => { - tabStripItem._index = i; - if (items[i]) { - const tabItemSpec = this.createTabItemSpec(items[i]); - tabItems.push(tabItemSpec); - } - }); - - this._bottomNavigationBar.setItems(tabItems); - - items.forEach((item, i, arr) => { - const textView = this._bottomNavigationBar.getTextViewForItemAt(i); - item.setNativeView(textView); - this._setItemColor(item); - }); - } - - private getItemLabelTextTransform(tabStripItem: TabStripItem): TextTransform { - const nestedLabel = tabStripItem.label; - let textTransform: TextTransform = null; - if (nestedLabel && nestedLabel.style.textTransform !== "initial") { - textTransform = nestedLabel.style.textTransform; - } else if (tabStripItem.style.textTransform !== "initial") { - textTransform = tabStripItem.style.textTransform; - } - - return textTransform || this._textTransform; - } - - private createTabItemSpec(tabStripItem: TabStripItem): org.nativescript.widgets.TabItemSpec { - const tabItemSpec = new org.nativescript.widgets.TabItemSpec(); - - if (tabStripItem.isLoaded) { - const titleLabel = tabStripItem.label; - let title = titleLabel.text; - - // TEXT-TRANSFORM - const textTransform = this.getItemLabelTextTransform(tabStripItem); - title = getTransformedText(title, textTransform); - tabItemSpec.title = title; - - // BACKGROUND-COLOR - const backgroundColor = tabStripItem.style.backgroundColor; - tabItemSpec.backgroundColor = backgroundColor ? backgroundColor.android : this.getTabBarBackgroundArgbColor(); - - // COLOR - let itemColor = this.selectedIndex === tabStripItem._index ? this._selectedItemColor : this._unSelectedItemColor; - const color = itemColor || titleLabel.style.color; - tabItemSpec.color = color && color.android; - - // FONT - const fontInternal = titleLabel.style.fontInternal; - if (fontInternal) { - tabItemSpec.fontSize = fontInternal.fontSize; - tabItemSpec.typeFace = fontInternal.getAndroidTypeface(); - } - - // ICON - const iconSource = tabStripItem.image && tabStripItem.image.src; - if (iconSource) { - const iconInfo = this.getIconInfo(tabStripItem, itemColor); - - if (iconInfo) { - // TODO: Make this native call that accepts string so that we don't load Bitmap in JS. - // tslint:disable-next-line:deprecation - tabItemSpec.iconDrawable = iconInfo.drawable; - tabItemSpec.imageHeight = iconInfo.height; - } else { - // TODO: - // traceMissingIcon(iconSource); - } - } - } - - return tabItemSpec; - } - - private getOriginalIcon(tabStripItem: TabStripItem, color?: Color): android.graphics.Bitmap { - const iconSource = tabStripItem.image && tabStripItem.image.src; - if (!iconSource) { - return null; - } - - let is: ImageSource; - if (isFontIconURI(iconSource)) { - const fontIconCode = iconSource.split("//")[1]; - const target = tabStripItem.image ? tabStripItem.image : tabStripItem; - const font = target.style.fontInternal; - if (!color) { - color = target.style.color; - } - is = ImageSource.fromFontIconCodeSync(fontIconCode, font, color); - } else { - is = ImageSource.fromFileOrResourceSync(iconSource); - } - - return is && is.android; - } - - private getDrawableInfo(image: android.graphics.Bitmap): IconInfo { - if (image) { - if (this.tabStrip && this.tabStrip.isIconSizeFixed) { - image = this.getFixedSizeIcon(image); - } - - let imageDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), image); - - return { - drawable: imageDrawable, - height: image.getHeight() - }; - } - - return new IconInfo(); - } - - private getIconInfo(tabStripItem: TabStripItem, color?: Color): IconInfo { - let originalIcon = this.getOriginalIcon(tabStripItem, color); - - return this.getDrawableInfo(originalIcon); - } - - private getFixedSizeIcon(image: android.graphics.Bitmap): android.graphics.Bitmap { - const inWidth = image.getWidth(); - const inHeight = image.getHeight(); - - const iconSpecSize = getIconSpecSize({ width: inWidth, height: inHeight }); - - const widthPixels = iconSpecSize.width * layout.getDisplayDensity(); - const heightPixels = iconSpecSize.height * layout.getDisplayDensity(); - - const scaledImage = android.graphics.Bitmap.createScaledBitmap(image, widthPixels, heightPixels, true); - - return scaledImage; - } - - public updateAndroidItemAt(index: number, spec: org.nativescript.widgets.TabItemSpec) { - this._bottomNavigationBar.updateItemAt(index, spec); - } - - public getTabBarBackgroundColor(): android.graphics.drawable.Drawable { - return this._bottomNavigationBar.getBackground(); - } - - public setTabBarBackgroundColor(value: android.graphics.drawable.Drawable | Color): void { - if (value instanceof Color) { - this._bottomNavigationBar.setBackgroundColor(value.android); - } else { - this._bottomNavigationBar.setBackground(tryCloneDrawable(value, this.nativeViewProtected.getResources())); - } - - this.updateTabStripItems(); - } - - private updateTabStripItems(): void { - this.tabStrip.items.forEach((tabStripItem: TabStripItem) => { - if (tabStripItem.nativeView) { - const tabItemSpec = this.createTabItemSpec(tabStripItem); - this.updateAndroidItemAt(tabStripItem._index, tabItemSpec); - } - }); - } - - public _setItemsColors(items: Array): void { - items.forEach((item) => { - if (item.nativeView) { - this._setItemColor(item); - } - }); - } - - public getTabBarSelectedItemColor(): Color { - return this._selectedItemColor; - } - - public setTabBarSelectedItemColor(value: Color) { - this._selectedItemColor = value; - this._setItemsColors(this.tabStrip.items); - } - - public getTabBarUnSelectedItemColor(): Color { - return this._unSelectedItemColor; - } - - public setTabBarUnSelectedItemColor(value: Color) { - this._unSelectedItemColor = value; - this._setItemsColors(this.tabStrip.items); - } - - private updateItem(tabStripItem: TabStripItem): void { - // TODO: Should figure out a way to do it directly with the the nativeView - const tabStripItemIndex = this.tabStrip.items.indexOf(tabStripItem); - const tabItemSpec = this.createTabItemSpec(tabStripItem); - this.updateAndroidItemAt(tabStripItemIndex, tabItemSpec); - } - - public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): void { - this.updateItem(tabStripItem); - } - - public setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: android.graphics.drawable.Drawable | Color): void { - this.updateItem(tabStripItem); - } - - public _setItemColor(tabStripItem: TabStripItem) { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (!itemColor) { - return; - } - - // set label color - tabStripItem.nativeViewProtected.setTextColor(itemColor.android); - - // set icon color - this.setIconColor(tabStripItem, itemColor); - } - - private setIconColor(tabStripItem: TabStripItem, color?: Color) { - const tabBarItem = this._bottomNavigationBar.getViewForItemAt(tabStripItem._index); - - const drawableInfo = this.getIconInfo(tabStripItem, color); - const imgView = tabBarItem.getChildAt(0); - imgView.setImageDrawable(drawableInfo.drawable); - if (color) { - imgView.setColorFilter(color.android); - } - } - - public setTabBarItemColor(tabStripItem: TabStripItem, value: number | Color): void { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (itemColor) { - // the itemColor is set through the selectedItemColor and unSelectedItemColor properties - // so it does not respect the css color - return; - } - - const androidColor = value instanceof Color ? value.android : value; - tabStripItem.nativeViewProtected.setTextColor(androidColor); - } - - public setTabBarIconColor(tabStripItem: TabStripItem, value: number | Color): void { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (itemColor) { - // the itemColor is set through the selectedItemColor and unSelectedItemColor properties - // so it does not respect the css color - return; - } - - this.setIconColor(tabStripItem); - } - - public setTabBarIconSource(tabStripItem: TabStripItem, value: number | Color): void { - this.updateItem(tabStripItem); - } - - public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { - if (value.fontSize) { - tabStripItem.nativeViewProtected.setTextSize(value.fontSize); - } - - tabStripItem.nativeViewProtected.setTypeface(value.getAndroidTypeface()); - } - - public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform): void { - const titleLabel = tabStripItem.label; - const title = getTransformedText(titleLabel.text, value); - tabStripItem.nativeViewProtected.setText(title); - } - - public getTabBarTextTransform(): TextTransform { - return this._textTransform; - } - - public setTabBarTextTransform(value: TextTransform): void { - let items = this.tabStrip && this.tabStrip.items; - if (items) { - items.forEach((tabStripItem) => { - if (tabStripItem.label && tabStripItem.nativeViewProtected) { - const nestedLabel = tabStripItem.label; - const title = getTransformedText(nestedLabel.text, value); - tabStripItem.nativeViewProtected.setText(title); - } - }); - } - this._textTransform = value; - } - - [selectedIndexProperty.setNative](value: number) { - // const smoothScroll = false; - - // if (traceEnabled()) { - // traceWrite("TabView this._viewPager.setCurrentItem(" + value + ", " + smoothScroll + ");", traceCategory); - // } - + protected setSelectedItem(value: number) { if (this.tabStrip) { - this._bottomNavigationBar.setSelectedPosition(value); + this.getBottomNavigationBar().setSelectedPosition(value); } else { this.changeTab(value); } } - - [itemsProperty.getDefault](): TabContentItem[] { - return null; - } - [itemsProperty.setNative](value: TabContentItem[]) { - if (value) { - value.forEach((item: TabContentItem, i) => { - (item).index = i; - }); - } - - selectedIndexProperty.coerce(this); - } - - [tabStripProperty.getDefault](): TabStrip { - return null; - } - [tabStripProperty.setNative](value: TabStrip) { - const items = this.tabStrip ? this.tabStrip.items : null; - this.setTabStripItems(items); - } -} - -function tryCloneDrawable(value: android.graphics.drawable.Drawable, resources: android.content.res.Resources): android.graphics.drawable.Drawable { - if (value) { - const constantState = value.getConstantState(); - if (constantState) { - return constantState.newDrawable(resources); - } - } - - return value; } diff --git a/nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts b/nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts index 5f25dee5f..03f17e082 100644 --- a/nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts +++ b/nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts @@ -3,7 +3,7 @@ * @module "ui/tab-navigation/bottom-navigation" */ /** */ -import { TabNavigationBase } from "../tab-navigation-base/tab-navigation-base"; +import { TabbableComponent } from "../tabbable-component"; import { TabContentItem } from "../tab-navigation-base/tab-content-item"; import { TabStrip } from "../tab-navigation-base/tab-strip"; import { Property, CoercibleProperty, EventData } from "../core/view"; @@ -31,7 +31,7 @@ export interface SelectedIndexChangedEventData extends EventData { /** * Represents a tab navigation widget with static tabs at the bottom. */ -export class BottomNavigation extends TabNavigationBase { +export class BottomNavigation extends TabbableComponent { /** * Gets or sets the items of the BottomNavigation. */ diff --git a/nativescript-core/ui/index.d.ts b/nativescript-core/ui/index.d.ts index ac5934ff0..f0de62894 100644 --- a/nativescript-core/ui/index.d.ts +++ b/nativescript-core/ui/index.d.ts @@ -29,6 +29,7 @@ export { SegmentedBar, SegmentedBarItem } from "./segmented-bar"; export { Slider } from "./slider"; export { Style, CommonLayoutParams } from "./styling/style"; export { Switch } from "./switch"; +export { TabbableComponent } from "./tabbable-component"; export { TabContentItem } from "./tab-navigation-base/tab-content-item"; export { TabNavigationBase } from "./tab-navigation-base/tab-navigation-base"; export { TabStrip, TabStripItemEventData } from "./tab-navigation-base/tab-strip"; diff --git a/nativescript-core/ui/index.ts b/nativescript-core/ui/index.ts index 073b18e27..e69632a13 100644 --- a/nativescript-core/ui/index.ts +++ b/nativescript-core/ui/index.ts @@ -56,6 +56,7 @@ export { Style, CommonLayoutParams } from "./styling/style"; export { Switch } from "./switch"; export { TabContentItem } from "./tab-navigation-base/tab-content-item"; export { TabNavigationBase } from "./tab-navigation-base/tab-navigation-base"; +export { TabbableComponent } from "./tabbable-component"; export { TabStrip, TabStripItemEventData } from "./tab-navigation-base/tab-strip"; export { TabStripItem } from "./tab-navigation-base/tab-strip-item"; export { TabView, TabViewItem } from "./tab-view"; diff --git a/nativescript-core/ui/tabbable-component/package.json b/nativescript-core/ui/tabbable-component/package.json new file mode 100644 index 000000000..5329a4f2a --- /dev/null +++ b/nativescript-core/ui/tabbable-component/package.json @@ -0,0 +1,5 @@ +{ + "name": "tabbable-component", + "main": "tabbable-component", + "types": "tabbable-component.d.ts" +} diff --git a/nativescript-core/ui/tabbable-component/tabbable-component.android.ts b/nativescript-core/ui/tabbable-component/tabbable-component.android.ts new file mode 100644 index 000000000..4ddab4a86 --- /dev/null +++ b/nativescript-core/ui/tabbable-component/tabbable-component.android.ts @@ -0,0 +1,486 @@ +// Types +import { TabNavigationBase, getIconSpecSize } from "../tab-navigation-base/tab-navigation-base"; +import { TextTransform, getTransformedText } from "../text-base/text-base"; +import { Color } from "../core/view/view"; +import { Frame } from "../frame"; +import { Font } from "../styling/font"; +import { ImageSource } from "../../image-source/image-source"; +import { isFontIconURI, layout } from "../../utils/utils"; +import * as application from "../../application/application"; +import { TabContentItem } from "../tab-navigation-base/tab-content-item"; +import { TabStrip } from "../tab-navigation-base/tab-strip"; +import { TabStripItem } from "../tab-navigation-base/tab-strip-item"; +import { itemsProperty, selectedIndexProperty, tabStripProperty } from "../tab-navigation-base/tab-navigation-base"; + +class IconInfo { + drawable: android.graphics.drawable.BitmapDrawable; + height: number; +} + +export abstract class TabbableComponent extends TabNavigationBase { + protected nativeBar: org.nativescript.widgets.TabbableBar; + public _originalBackground: any; + protected _textTransform: TextTransform = "uppercase"; + protected _selectedItemColor: Color; + protected _unSelectedItemColor: Color; + protected _offscreenTabLimit: number; + protected _contentViewId: number = -1; + + protected abstract setNativeItems(items: Array); + protected abstract setSelectedItem(value: number); + + public onLoaded(): void { + super.onLoaded(); + + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + } + + public onUnloaded(): void { + super.onUnloaded(); + + this.setItems(null); + if (this.tabStrip) { + this.setTabStripItems(null); + } + } + + public initNativeView(): void { + super.initNativeView(); + if (this._contentViewId < 0) { + this._contentViewId = android.view.View.generateViewId(); + } + + const nativeView: any = this.nativeViewProtected; + this.nativeBar = (nativeView).bottomNavigationBar; + + (this.nativeBar).owner = this; + } + + protected getOffscreenTabLimit(): number { + return 0; + } + + public _loadUnloadTabItems(newIndex: number) { + const items = this.items; + if (!items) { + return; + } + + const lastIndex = items.length - 1; + const offsideItems = this.getOffscreenTabLimit(); + + let toUnload = []; + let toLoad = []; + + iterateIndexRange(newIndex, offsideItems, lastIndex, (i) => toLoad.push(i)); + + items.forEach((item, i) => { + const indexOfI = toLoad.indexOf(i); + if (indexOfI < 0) { + toUnload.push(i); + } + }); + + toUnload.forEach(index => { + const item = items[index]; + if (items[index]) { + item.unloadView(item.content); + } + }); + + const newItem = items[newIndex]; + const selectedView = newItem && newItem.content; + if (selectedView instanceof Frame) { + (selectedView)._pushInFrameStackRecursive(); + } + + toLoad.forEach(index => { + const item = items[index]; + if (this.isLoaded && items[index]) { + item.loadView(item.content); + } + }); + } + + public _onRootViewReset(): void { + super._onRootViewReset(); + + // call this AFTER the super call to ensure descendants apply their rootview-reset logic first + // i.e. in a scenario with tab frames let the frames cleanup their fragments first, and then + // cleanup the tab fragments to avoid + // android.content.res.Resources$NotFoundException: Unable to find resource ID #0xfffffff6 + this.disposeCurrentFragments(); + } + + private disposeCurrentFragments(): void { + const fragmentManager = this._getFragmentManager(); + const transaction = fragmentManager.beginTransaction(); + for (let fragment of (>fragmentManager.getFragments().toArray())) { + transaction.remove(fragment); + } + transaction.commitNowAllowingStateLoss(); + } + + public onItemsChanged(oldItems: TabContentItem[], newItems: TabContentItem[]): void { + super.onItemsChanged(oldItems, newItems); + + if (oldItems) { + oldItems.forEach((item: TabContentItem, i, arr) => { + (item).index = 0; + (item).tabItemSpec = null; + item.setNativeView(null); + }); + } + } + + protected setItems(items: Array) { + //overriden + } + + protected setTabStripItems(items: Array) { + if (!this.tabStrip || !items) { + this.setNativeItems(null); + + return; + } + + const tabItems = new Array(); + items.forEach((tabStripItem, i) => { + tabStripItem._index = i; + const tabItemSpec = this.createTabItemSpec(tabStripItem); + (tabStripItem).tabItemSpec = tabItemSpec; + tabItems.push(tabItemSpec); + }); + + this.setNativeItems(tabItems); + this.tabStrip.setNativeView(this.nativeBar); + items.forEach((item, i, arr) => { + const textView = this.nativeBar.getTextViewForItemAt(i); + item.setNativeView(textView); + this._setItemColor(item); + }); + } + + public _setItemsColors(items: Array): void { + items.forEach((item) => { + if (item.nativeView) { + this._setItemColor(item); + } + }); + } + + public setTabBarItemColor(tabStripItem: TabStripItem, value: number | Color): void { + const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; + if (itemColor) { + // the itemColor is set through the selectedItemColor and unSelectedItemColor properties + // so it does not respect the css color + return; + } + + const androidColor = value instanceof Color ? value.android : value; + tabStripItem.nativeViewProtected.setTextColor(androidColor); + } + + public setTabBarIconColor(tabStripItem: TabStripItem, value: number | Color): void { + const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; + if (itemColor) { + // the itemColor is set through the selectedItemColor and unSelectedItemColor properties + // so it does not respect the css color + return; + } + + this.setIconColor(tabStripItem); + } + + public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { + if (value.fontSize) { + tabStripItem.nativeViewProtected.setTextSize(value.fontSize); + } + tabStripItem.nativeViewProtected.setTypeface(value.getAndroidTypeface()); + } + + public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform): void { + const nestedLabel = tabStripItem.label; + const title = getTransformedText(nestedLabel.text, value); + tabStripItem.nativeViewProtected.setText(title); + } + + public getTabBarTextTransform(): TextTransform { + return this._textTransform; + } + + public setTabBarTextTransform(value: TextTransform): void { + let items = this.tabStrip && this.tabStrip.items; + if (items) { + items.forEach((tabStripItem) => { + if (tabStripItem.label && tabStripItem.nativeViewProtected) { + const nestedLabel = tabStripItem.label; + const title = getTransformedText(nestedLabel.text, value); + tabStripItem.nativeViewProtected.setText(title); + } + }); + } + this._textTransform = value; + } + + public getTabBarSelectedItemColor(): Color { + return this._selectedItemColor; + } + + public setTabBarSelectedItemColor(value: Color) { + this._selectedItemColor = value; + this._setItemsColors(this.tabStrip.items); + } + + public getTabBarUnSelectedItemColor(): Color { + return this._unSelectedItemColor; + } + + public setTabBarUnSelectedItemColor(value: Color) { + this._unSelectedItemColor = value; + this._setItemsColors(this.tabStrip.items); + } + + protected updateItem(tabStripItem: TabStripItem): void { + // TODO: Should figure out a way to do it directly with the the nativeView + const tabStripItemIndex = this.tabStrip.items.indexOf(tabStripItem); + const tabItemSpec = this.createTabItemSpec(tabStripItem); + this.updateAndroidItemAt(tabStripItemIndex, tabItemSpec); + } + + public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): void { + this.updateItem(tabStripItem); + } + + public setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: android.graphics.drawable.Drawable | Color): void { + this.updateItem(tabStripItem); + } + + public setTabBarIconSource(tabStripItem: TabStripItem, value: number | Color): void { + this.updateItem(tabStripItem); + } + + public updateAndroidItemAt(index: number, spec: org.nativescript.widgets.TabItemSpec) { + this.nativeBar.updateItemAt(index, spec); + } + + public getTabBarBackgroundColor(): android.graphics.drawable.Drawable { + return this.nativeBar.getBackground(); + } + + public setTabBarBackgroundColor(value: android.graphics.drawable.Drawable | Color): void { + if (value instanceof Color) { + this.nativeBar.setBackgroundColor(value.android); + } else { + this.nativeBar.setBackground(tryCloneDrawable(value, this.nativeViewProtected.getResources())); + } + + this.updateTabStripItems(); + } + + private updateTabStripItems(): void { + this.tabStrip.items.forEach((tabStripItem: TabStripItem) => { + if (tabStripItem.nativeView) { + const tabItemSpec = this.createTabItemSpec(tabStripItem); + this.updateAndroidItemAt(tabStripItem._index, tabItemSpec); + } + }); + } + + public _setItemColor(tabStripItem: TabStripItem) { + const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; + if (!itemColor) { + return; + } + + // set label color + tabStripItem.nativeViewProtected.setTextColor(itemColor.android); + + // set icon color + this.setIconColor(tabStripItem, itemColor); + } + + protected setIconColor(tabStripItem: TabStripItem, color?: Color) { + const tabBarItem = this.nativeBar.getViewForItemAt(tabStripItem._index); + + const drawableInfo = this.getIconInfo(tabStripItem, color); + const imgView = tabBarItem.getChildAt(0); + imgView.setImageDrawable(drawableInfo.drawable); + if (color) { + imgView.setColorFilter(color.android); + } + } + + protected createTabItemSpec(tabStripItem: TabStripItem): org.nativescript.widgets.TabItemSpec { + const tabItemSpec = new org.nativescript.widgets.TabItemSpec(); + + if (tabStripItem.isLoaded) { + const titleLabel = tabStripItem.label; + let title = titleLabel.text; + + // TEXT-TRANSFORM + const textTransform = this.getItemLabelTextTransform(tabStripItem); + title = getTransformedText(title, textTransform); + tabItemSpec.title = title; + + // BACKGROUND-COLOR + const backgroundColor = tabStripItem.style.backgroundColor; + tabItemSpec.backgroundColor = backgroundColor ? backgroundColor.android : this.getTabBarBackgroundArgbColor(); + + // COLOR + let itemColor = this.selectedIndex === tabStripItem._index ? this._selectedItemColor : this._unSelectedItemColor; + const color = itemColor || titleLabel.style.color; + tabItemSpec.color = color && color.android; + + // FONT + const fontInternal = titleLabel.style.fontInternal; + if (fontInternal) { + tabItemSpec.fontSize = fontInternal.fontSize; + tabItemSpec.typeFace = fontInternal.getAndroidTypeface(); + } + + // ICON + const iconSource = tabStripItem.image && tabStripItem.image.src; + if (iconSource) { + const iconInfo = this.getIconInfo(tabStripItem, itemColor); + + if (iconInfo) { + // TODO: Make this native call that accepts string so that we don't load Bitmap in JS. + // tslint:disable-next-line:deprecation + tabItemSpec.iconDrawable = iconInfo.drawable; + tabItemSpec.imageHeight = iconInfo.height; + } else { + // TODO: + // traceMissingIcon(iconSource); + } + } + } + + return tabItemSpec; + } + + public getTabBarItemTextTransform(tabStripItem: TabStripItem): TextTransform { + return this.getItemLabelTextTransform(tabStripItem); + } + + private getItemLabelTextTransform(tabStripItem: TabStripItem): TextTransform { + const nestedLabel = tabStripItem.label; + let textTransform: TextTransform = null; + if (nestedLabel && nestedLabel.style.textTransform !== "initial") { + textTransform = nestedLabel.style.textTransform; + } else if (tabStripItem.style.textTransform !== "initial") { + textTransform = tabStripItem.style.textTransform; + } + + return textTransform || this._textTransform; + } + + private getOriginalIcon(tabStripItem: TabStripItem, color?: Color): android.graphics.Bitmap { + const iconSource = tabStripItem.image && tabStripItem.image.src; + if (!iconSource) { + return null; + } + + let is: ImageSource; + if (isFontIconURI(iconSource)) { + const fontIconCode = iconSource.split("//")[1]; + const target = tabStripItem.image ? tabStripItem.image : tabStripItem; + const font = target.style.fontInternal; + if (!color) { + color = target.style.color; + } + is = ImageSource.fromFontIconCodeSync(fontIconCode, font, color); + } else { + is = ImageSource.fromFileOrResourceSync(iconSource); + } + + return is && is.android; + } + + private getFixedSizeIcon(image: android.graphics.Bitmap): android.graphics.Bitmap { + const inWidth = image.getWidth(); + const inHeight = image.getHeight(); + + const iconSpecSize = getIconSpecSize({ width: inWidth, height: inHeight }); + + const widthPixels = iconSpecSize.width * layout.getDisplayDensity(); + const heightPixels = iconSpecSize.height * layout.getDisplayDensity(); + + const scaledImage = android.graphics.Bitmap.createScaledBitmap(image, widthPixels, heightPixels, true); + + return scaledImage; + } + + protected getIconInfo(tabStripItem: TabStripItem, color?: Color): IconInfo { + let originalIcon = this.getOriginalIcon(tabStripItem, color); + + return this.getDrawableInfo(originalIcon); + } + + private getDrawableInfo(image: android.graphics.Bitmap): IconInfo { + if (image) { + if (this.tabStrip && this.tabStrip.isIconSizeFixed) { + image = this.getFixedSizeIcon(image); + } + + let imageDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), image); + + return { + drawable: imageDrawable, + height: image.getHeight() + }; + } + + return new IconInfo(); + } + + [itemsProperty.getDefault](): TabContentItem[] { + return null; + } + + [itemsProperty.setNative](value: TabContentItem[]) { + if (value) { + value.forEach((item: TabContentItem, i) => { + (item).index = i; + }); + } + + selectedIndexProperty.coerce(this); + } + + [tabStripProperty.getDefault](): TabStrip { + return null; + } + + [tabStripProperty.setNative](value: TabStrip) { + const items = this.tabStrip ? this.tabStrip.items : null; + this.setTabStripItems(items); + } + + [selectedIndexProperty.setNative](value: number) { + this.setSelectedItem(value); + } +} + +function tryCloneDrawable(value: android.graphics.drawable.Drawable, resources: android.content.res.Resources): android.graphics.drawable.Drawable { + if (value) { + const constantState = value.getConstantState(); + if (constantState) { + return constantState.newDrawable(resources); + } + } + + return value; +} + +function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) { + const rangeStart = Math.max(0, index - eps); + const rangeEnd = Math.min(index + eps, lastIndex); + for (let i = rangeStart; i <= rangeEnd; i++) { + callback(i); + } +} \ No newline at end of file diff --git a/nativescript-core/ui/tabbable-component/tabbable-component.d.ts b/nativescript-core/ui/tabbable-component/tabbable-component.d.ts new file mode 100644 index 000000000..384487b04 --- /dev/null +++ b/nativescript-core/ui/tabbable-component/tabbable-component.d.ts @@ -0,0 +1,58 @@ +/** + * Contains the TabNavigationBase class, which serves as the base class for tab navigation. + * @module "ui/tabbable-component/tabbable-component" + */ /** */ + import { SelectedIndexChangedEventData, TabNavigationBase } from "../tab-navigation-base/tab-navigation-base"; + + +import { TabContentItem } from "../tab-navigation-base/tab-content-item/tab-content-item"; +import { TabStrip } from "../tab-navigation-base/tab-strip"; +import { Property, CoercibleProperty, EventData } from "../core/view/view"; + +/** + * Represents a tab navigation widget with static tabs at the bottom. + */ +export abstract class TabbableComponent extends TabNavigationBase { + /** + * Gets or sets the items of the tab navigation. + */ + items: Array; + + /** + * Gets or sets the tab strip of the BottomNavigation. + */ + tabStrip: TabStrip; + + /** + * Gets or sets the selectedIndex of the BottomNavigation. + */ + selectedIndex: number; + + /** + * Gets the native [android widget](http://developer.android.com/reference/android/support/v4/view/ViewPager.html) that represents the user interface for this component. Valid only when running on Android OS. + */ + android: any /* android.view.View */; //android.support.v4.view.ViewPager; + + /** + * Gets the native iOS [UITabBarController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITabBarController_Class/) that represents the user interface for this component. Valid only when running on iOS. + */ + ios: any /* UITabBarController */; + + /** + * String value used when hooking to the selectedIndexChanged event. + */ + public static selectedIndexChangedEvent: string; + + /** + * A basic method signature to hook an event listener (shortcut alias to the addEventListener method). + * @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change"). + * @param callback - Callback function which will be executed when event is raised. + * @param thisArg - An optional parameter which will be used as `this` context for callback execution. + */ + on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + + /** + * Raised when the selected index changes. + */ + on(event: "selectedIndexChanged", callback: (args: SelectedIndexChangedEventData) => void, thisArg?: any); +} \ No newline at end of file diff --git a/nativescript-core/ui/tabbable-component/tabbable-component.ios.ts b/nativescript-core/ui/tabbable-component/tabbable-component.ios.ts new file mode 100644 index 000000000..ec3d9239a --- /dev/null +++ b/nativescript-core/ui/tabbable-component/tabbable-component.ios.ts @@ -0,0 +1,6 @@ +// Types +import { TabNavigationBase } from "../tab-navigation-base/tab-navigation-base"; + +export abstract class TabbableComponent extends TabNavigationBase { + +} \ No newline at end of file diff --git a/nativescript-core/ui/tabs/tabs-common.android.ts b/nativescript-core/ui/tabs/tabs-common.android.ts new file mode 100644 index 000000000..0837fcabb --- /dev/null +++ b/nativescript-core/ui/tabs/tabs-common.android.ts @@ -0,0 +1,41 @@ +// Types +import { Tabs as TabsDefinition } from "."; + +// Requires +import { CSSType, booleanConverter } from "../core/view"; +import { Property } from "../core/properties"; +import { TabbableComponent } from "../tabbable-component/tabbable-component.android"; + +export const traceCategory = "TabView"; + +export module knownCollections { + export const items = "items"; +} + +@CSSType("Tabs") +export abstract class TabsBase extends TabbableComponent implements TabsDefinition { + public swipeEnabled: boolean; + public offscreenTabLimit: number; + public tabsPosition: "top" | "bottom"; + public iOSTabBarItemsAlignment: IOSTabBarItemsAlignment; +} + +// TODO: Add Unit tests +export const swipeEnabledProperty = new Property({ + name: "swipeEnabled", defaultValue: true, valueConverter: booleanConverter +}); +swipeEnabledProperty.register(TabsBase); + +// TODO: Add Unit tests +// TODO: Coerce to max number of items? +export const offscreenTabLimitProperty = new Property({ + name: "offscreenTabLimit", defaultValue: 1, valueConverter: (v) => parseInt(v) +}); +offscreenTabLimitProperty.register(TabsBase); + +export const tabsPositionProperty = new Property({ name: "tabsPosition", defaultValue: "top" }); +tabsPositionProperty.register(TabsBase); + +export type IOSTabBarItemsAlignment = "leading" | "justified" | "center" | "centerSelected"; +export const iOSTabBarItemsAlignmentProperty = new Property({ name: "iOSTabBarItemsAlignment", defaultValue: "justified" }); +iOSTabBarItemsAlignmentProperty.register(TabsBase); \ No newline at end of file diff --git a/nativescript-core/ui/tabs/tabs-common.d.ts b/nativescript-core/ui/tabs/tabs-common.d.ts new file mode 100644 index 000000000..093bcd93e --- /dev/null +++ b/nativescript-core/ui/tabs/tabs-common.d.ts @@ -0,0 +1,33 @@ +// Types +import { Tabs as TabsDefinition } from "."; + +// Requires +import { CSSType, booleanConverter } from "../core/view"; +import { Property } from "../core/properties"; +import { TabbableComponent } from "../tabbable-component"; + +export * from "../tab-navigation-base/tab-content-item"; +export * from "../tab-navigation-base/tab-navigation-base"; +export * from "../tab-navigation-base/tab-strip"; +export * from "../tab-navigation-base/tab-strip-item"; + +export const traceCategory = "TabView"; + +export module knownCollections { + export const items = "items"; +} + +@CSSType("Tabs") +export abstract class TabsBase extends TabbableComponent implements TabsDefinition { + public swipeEnabled: boolean; + public offscreenTabLimit: number; + public tabsPosition: "top" | "bottom"; + public iOSTabBarItemsAlignment: IOSTabBarItemsAlignment; +} + +export const swipeEnabledProperty: Property; +export const offscreenTabLimitProperty: Property; +export const tabsPositionProperty: Property; + +export type IOSTabBarItemsAlignment = "leading" | "justified" | "center" | "centerSelected"; +export const iOSTabBarItemsAlignmentProperty: Property; \ No newline at end of file diff --git a/nativescript-core/ui/tabs/tabs-common.ts b/nativescript-core/ui/tabs/tabs-common.ios.ts similarity index 88% rename from nativescript-core/ui/tabs/tabs-common.ts rename to nativescript-core/ui/tabs/tabs-common.ios.ts index 56f3f2ad9..7abbf0768 100644 --- a/nativescript-core/ui/tabs/tabs-common.ts +++ b/nativescript-core/ui/tabs/tabs-common.ios.ts @@ -2,9 +2,9 @@ import { Tabs as TabsDefinition } from "."; // Requires -import { TabNavigationBase } from "../tab-navigation-base/tab-navigation-base"; import { CSSType, booleanConverter } from "../core/view"; import { Property } from "../core/properties"; +import { TabbableComponent } from "../tabbable-component"; export * from "../tab-navigation-base/tab-content-item"; export * from "../tab-navigation-base/tab-navigation-base"; @@ -18,7 +18,7 @@ export module knownCollections { } @CSSType("Tabs") -export class TabsBase extends TabNavigationBase implements TabsDefinition { +export abstract class TabsBase extends TabbableComponent implements TabsDefinition { public swipeEnabled: boolean; public offscreenTabLimit: number; public tabsPosition: "top" | "bottom"; diff --git a/nativescript-core/ui/tabs/tabs.android.ts b/nativescript-core/ui/tabs/tabs.android.ts index 8fc36ef26..1aafb464b 100644 --- a/nativescript-core/ui/tabs/tabs.android.ts +++ b/nativescript-core/ui/tabs/tabs.android.ts @@ -2,21 +2,12 @@ import { TabContentItem } from "../tab-navigation-base/tab-content-item"; import { TabStrip } from "../tab-navigation-base/tab-strip"; import { TabStripItem } from "../tab-navigation-base/tab-strip-item"; -import { TextTransform } from "../text-base"; // Requires import * as application from "../../application"; -import { ImageSource } from "../../image-source"; -import { ad, isFontIconURI, layout, RESOURCE_PREFIX } from "../../utils/utils"; +import { ad, layout } from "../../utils/utils"; import { Color } from "../core/view"; -import { Frame } from "../frame"; -import { Font } from "../styling/font"; -import { - getIconSpecSize, itemsProperty, selectedIndexProperty, tabStripProperty -} from "../tab-navigation-base/tab-navigation-base"; -import { getTransformedText } from "../text-base"; -import { offscreenTabLimitProperty, swipeEnabledProperty, TabsBase } from "./tabs-common"; - +import { offscreenTabLimitProperty, swipeEnabledProperty, TabsBase } from "./tabs-common.android"; export * from "./tabs-common"; const ACCENT_COLOR = "colorAccent"; @@ -361,23 +352,9 @@ function setElevation(grid: org.nativescript.widgets.GridLayout, tabsBar: org.na export const tabs = new Array>(); -function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) { - const rangeStart = Math.max(0, index - eps); - const rangeEnd = Math.min(index + eps, lastIndex); - for (let i = rangeStart; i <= rangeEnd; i++) { - callback(i); - } -} - export class Tabs extends TabsBase { - private _tabsBar: org.nativescript.widgets.TabsBar; private _viewPager: androidx.viewpager.widget.ViewPager; private _pagerAdapter: androidx.viewpager.widget.PagerAdapter; - private _androidViewId: number = -1; - public _originalBackground: any; - private _textTransform: TextTransform = "uppercase"; - private _selectedItemColor: Color; - private _unSelectedItemColor: Color; constructor() { super(); @@ -388,16 +365,12 @@ export class Tabs extends TabsBase { return true; } - public onItemsChanged(oldItems: TabContentItem[], newItems: TabContentItem[]): void { - super.onItemsChanged(oldItems, newItems); + public getTabsBar(): org.nativescript.widgets.TabsBar { + return (this).nativeBar; + } - if (oldItems) { - oldItems.forEach((item: TabContentItem, i, arr) => { - (item).index = 0; - (item).tabItemSpec = null; - item.setNativeView(null); - }); - } + protected setNativeItems(items: Array) { + this.getTabsBar().setItems(items, items ? this._viewPager : null); } public createNativeView() { @@ -454,71 +427,21 @@ export class Tabs extends TabsBase { public initNativeView(): void { super.initNativeView(); - if (this._androidViewId < 0) { - this._androidViewId = android.view.View.generateViewId(); - } - const nativeView: any = this.nativeViewProtected; - this._tabsBar = (nativeView).tabsBar; - - const viewPager = (nativeView).viewPager; - viewPager.setId(this._androidViewId); + const viewPager = (this.nativeViewProtected).viewPager; + viewPager.setId(this._contentViewId); this._viewPager = viewPager; this._pagerAdapter = (viewPager).adapter; (this._pagerAdapter).owner = this; } - public _loadUnloadTabItems(newIndex: number) { - const items = this.items; - if (!items) { - return; - } - - const lastIndex = items.length - 1; - const offsideItems = this.offscreenTabLimit; - - let toUnload = []; - let toLoad = []; - - iterateIndexRange(newIndex, offsideItems, lastIndex, (i) => toLoad.push(i)); - - items.forEach((item, i) => { - const indexOfI = toLoad.indexOf(i); - if (indexOfI < 0) { - toUnload.push(i); - } - }); - - toUnload.forEach(index => { - const item = items[index]; - if (items[index]) { - item.unloadView(item.content); - } - }); - - const newItem = items[newIndex]; - const selectedView = newItem && newItem.content; - if (selectedView instanceof Frame) { - (selectedView)._pushInFrameStackRecursive(); - } - - toLoad.forEach(index => { - const item = items[index]; - if (this.isLoaded && items[index]) { - item.loadView(item.content); - } - }); + protected getOffscreenTabLimit(): number { + return this.offscreenTabLimit; } public onLoaded(): void { super.onLoaded(); - if (this._originalBackground) { - this.backgroundColor = null; - this.backgroundColor = this._originalBackground; - this._originalBackground = null; - } - this.setItems((this.items)); if (this.tabStrip) { @@ -528,44 +451,16 @@ export class Tabs extends TabsBase { // this.setAdapterItems(this.items); } - public onUnloaded(): void { - super.onUnloaded(); - - this.setItems(null); - this.setTabStripItems(null); - - // this.setAdapterItems(null); - } - public disposeNativeView() { - this._tabsBar.setItems(null, null); + this.setNativeItems(null); (this._pagerAdapter).owner = null; this._pagerAdapter = null; - this._tabsBar = null; + this.nativeBar = null; this._viewPager = null; super.disposeNativeView(); } - public _onRootViewReset(): void { - super._onRootViewReset(); - - // call this AFTER the super call to ensure descendants apply their rootview-reset logic first - // i.e. in a scenario with tab frames let the frames cleanup their fragments first, and then - // cleanup the tab fragments to avoid - // android.content.res.Resources$NotFoundException: Unable to find resource ID #0xfffffff6 - this.disposeCurrentFragments(); - } - - private disposeCurrentFragments(): void { - const fragmentManager = this._getFragmentManager(); - const transaction = fragmentManager.beginTransaction(); - for (let fragment of (>fragmentManager.getFragments().toArray())) { - transaction.remove(fragment); - } - transaction.commitNowAllowingStateLoss(); - } - private shouldUpdateAdapter(items: Array) { if (!this._pagerAdapter) { return false; @@ -603,7 +498,7 @@ export class Tabs extends TabsBase { return false; } - private setItems(items: Array) { + protected setItems(items: Array) { if (this.shouldUpdateAdapter(items)) { (this._pagerAdapter).items = items; @@ -617,335 +512,16 @@ export class Tabs extends TabsBase { } } - private setTabStripItems(items: Array) { - const length = items ? items.length : 0; - if (length === 0) { - this._tabsBar.setItems(null, null); - - return; - } - - const tabItems = new Array(); - items.forEach((tabStripItem: TabStripItem, i, arr) => { - tabStripItem._index = i; - const tabItemSpec = this.createTabItemSpec(tabStripItem); - (tabStripItem).tabItemSpec = tabItemSpec; - tabItems.push(tabItemSpec); - }); - - const tabsBar = this._tabsBar; - tabsBar.setItems(tabItems, this._viewPager); - this.tabStrip.setNativeView(tabsBar); - items.forEach((item, i, arr) => { - const tv = tabsBar.getTextViewForItemAt(i); - item.setNativeView(tv); - this._setItemColor(item); - }); - } - - private getItemLabelTextTransform(tabStripItem: TabStripItem): TextTransform { - const nestedLabel = tabStripItem.label; - let textTransform: TextTransform = null; - if (nestedLabel && nestedLabel.style.textTransform !== "initial") { - textTransform = nestedLabel.style.textTransform; - } else if (tabStripItem.style.textTransform !== "initial") { - textTransform = tabStripItem.style.textTransform; - } - - return textTransform || this._textTransform; - } - - private createTabItemSpec(tabStripItem: TabStripItem): org.nativescript.widgets.TabItemSpec { - const tabItemSpec = new org.nativescript.widgets.TabItemSpec(); - - if (tabStripItem.isLoaded) { - const nestedLabel = tabStripItem.label; - let title = nestedLabel.text; - - // TEXT-TRANSFORM - const textTransform = this.getItemLabelTextTransform(tabStripItem); - title = getTransformedText(title, textTransform); - tabItemSpec.title = title; - - // BACKGROUND-COLOR - const backgroundColor = tabStripItem.style.backgroundColor; - tabItemSpec.backgroundColor = backgroundColor ? backgroundColor.android : this.getTabBarBackgroundArgbColor(); - - // COLOR - let itemColor = this.selectedIndex === tabStripItem._index ? this._selectedItemColor : this._unSelectedItemColor; - const color = itemColor || nestedLabel.style.color; - tabItemSpec.color = color && color.android; - - // FONT - const fontInternal = nestedLabel.style.fontInternal; - if (fontInternal) { - tabItemSpec.fontSize = fontInternal.fontSize; - tabItemSpec.typeFace = fontInternal.getAndroidTypeface(); - } - - // ICON - const iconSource = tabStripItem.image && tabStripItem.image.src; - if (iconSource) { - const icon = this.getIcon(tabStripItem, itemColor); - - if (icon) { - // TODO: Make this native call that accepts string so that we don't load Bitmap in JS. - // tslint:disable-next-line:deprecation - tabItemSpec.iconDrawable = icon; - } else { - // TODO: - // traceMissingIcon(iconSource); - } - } - } - - return tabItemSpec; - } - - private getIcon(tabStripItem: TabStripItem, color?: Color): android.graphics.drawable.BitmapDrawable { - const iconSource = tabStripItem.image && tabStripItem.image.src; - if (!iconSource) { - return null; - } - - let is: ImageSource; - if (isFontIconURI(iconSource)) { - const fontIconCode = iconSource.split("//")[1]; - const target = tabStripItem.image ? tabStripItem.image : tabStripItem; - const font = target.style.fontInternal; - if (!color) { - color = target.style.color; - } - is = ImageSource.fromFontIconCodeSync(fontIconCode, font, color); - } else { - is = ImageSource.fromFileOrResourceSync(iconSource); - } - - let imageDrawable: android.graphics.drawable.BitmapDrawable; - if (is && is.android) { - let image = is.android; - - if (this.tabStrip && this.tabStrip.isIconSizeFixed) { - image = this.getFixedSizeIcon(image); - } - - imageDrawable = new android.graphics.drawable.BitmapDrawable(appResources, image); - } else { - // TODO - // traceMissingIcon(iconSource); - } - - return imageDrawable; - } - - private getFixedSizeIcon(image: android.graphics.Bitmap): android.graphics.Bitmap { - const inWidth = image.getWidth(); - const inHeight = image.getHeight(); - - const iconSpecSize = getIconSpecSize({ width: inWidth, height: inHeight }); - - const widthPixels = iconSpecSize.width * layout.getDisplayDensity(); - const heightPixels = iconSpecSize.height * layout.getDisplayDensity(); - - const scaledImage = android.graphics.Bitmap.createScaledBitmap(image, widthPixels, heightPixels, true); - - return scaledImage; - } - - // private setAdapterItems(items: Array) { - // if (this.shouldUpdateAdapter(items)) { - // (this._pagerAdapter).items = items; - - // const length = items ? items.length : 0; - // if (length === 0) { - // this._tabLayout.setItems(null, null); - // this._pagerAdapter.notifyDataSetChanged(); - // return; - // } - - // const tabItems = new Array(); - // items.forEach((item: TabStripItem, i, arr) => { - // const tabItemSpec = createTabItemSpec(item); - // (item).index = i; - // (item).tabItemSpec = tabItemSpec; - // tabItems.push(tabItemSpec); - // }); - - // const tabLayout = this._tabLayout; - // tabLayout.setItems(tabItems, this._viewPager); - // items.forEach((item, i, arr) => { - // const tv = tabLayout.getTextViewForItemAt(i); - // item.setNativeView(tv); - // }); - - // this._pagerAdapter.notifyDataSetChanged(); - // } - // } - - public updateAndroidItemAt(index: number, spec: org.nativescript.widgets.TabItemSpec) { - this._tabsBar.updateItemAt(index, spec); - } - - public getTabBarBackgroundColor(): android.graphics.drawable.Drawable { - return this._tabsBar.getBackground(); - } - - public setTabBarBackgroundColor(value: android.graphics.drawable.Drawable | Color): void { - if (value instanceof Color) { - this._tabsBar.setBackgroundColor(value.android); - } else { - this._tabsBar.setBackground(tryCloneDrawable(value, this.nativeViewProtected.getResources())); - } - - this.updateTabStripItems(); - } - - private updateTabStripItems(): void { - this.tabStrip.items.forEach((tabStripItem: TabStripItem) => { - if (tabStripItem.nativeView) { - const tabItemSpec = this.createTabItemSpec(tabStripItem); - this.updateAndroidItemAt(tabStripItem._index, tabItemSpec); - } - }); - } - public getTabBarHighlightColor(): number { return getDefaultAccentColor(this._context); } public setTabBarHighlightColor(value: number | Color) { const color = value instanceof Color ? value.android : value; - this._tabsBar.setSelectedIndicatorColors([color]); + this.getTabsBar().setSelectedIndicatorColors([color]); } - private setItemsColors(items: Array): void { - items.forEach((item) => { - if (item.nativeView) { - this._setItemColor(item); - } - }); - } - - public getTabBarSelectedItemColor(): Color { - return this._selectedItemColor; - } - - public setTabBarSelectedItemColor(value: Color) { - this._selectedItemColor = value; - this.setItemsColors(this.tabStrip.items); - } - - public getTabBarUnSelectedItemColor(): Color { - return this._unSelectedItemColor; - } - - public setTabBarUnSelectedItemColor(value: Color) { - this._unSelectedItemColor = value; - this.setItemsColors(this.tabStrip.items); - } - - private updateItem(tabStripItem: TabStripItem): void { - // TODO: Should figure out a way to do it directly with the the nativeView - const tabStripItemIndex = this.tabStrip.items.indexOf(tabStripItem); - const tabItemSpec = this.createTabItemSpec(tabStripItem); - this.updateAndroidItemAt(tabStripItemIndex, tabItemSpec); - } - - public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): void { - this.updateItem(tabStripItem); - } - - public setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: android.graphics.drawable.Drawable | Color): void { - this.updateItem(tabStripItem); - } - - public _setItemColor(tabStripItem: TabStripItem) { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (!itemColor) { - return; - } - - // set label color - tabStripItem.nativeViewProtected.setTextColor(itemColor.android); - - // set icon color - this.setIconColor(tabStripItem, itemColor); - } - - private setIconColor(tabStripItem: TabStripItem, color?: Color) { - const tabBarItem = this._tabsBar.getViewForItemAt(tabStripItem._index); - - const drawable = this.getIcon(tabStripItem, color); - const imgView = tabBarItem.getChildAt(0); - imgView.setImageDrawable(drawable); - if (color) { - imgView.setColorFilter(color.android); - } - } - - public setTabBarItemColor(tabStripItem: TabStripItem, value: number | Color): void { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (itemColor) { - // the itemColor is set through the selectedItemColor and unSelectedItemColor properties - // so it does not respect the css color - return; - } - - const androidColor = value instanceof Color ? value.android : value; - tabStripItem.nativeViewProtected.setTextColor(androidColor); - } - - public setTabBarIconColor(tabStripItem: TabStripItem, value: number | Color): void { - const itemColor = (tabStripItem._index === this.selectedIndex) ? this._selectedItemColor : this._unSelectedItemColor; - if (itemColor) { - // the itemColor is set through the selectedItemColor and unSelectedItemColor properties - // so it does not respect the css color - return; - } - - this.setIconColor(tabStripItem); - } - - public setTabBarIconSource(tabStripItem: TabStripItem, value: number | Color): void { - this.updateItem(tabStripItem); - } - - public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { - if (value.fontSize) { - tabStripItem.nativeViewProtected.setTextSize(value.fontSize); - } - tabStripItem.nativeViewProtected.setTypeface(value.getAndroidTypeface()); - } - - public getTabBarItemTextTransform(tabStripItem: TabStripItem): TextTransform { - return this.getItemLabelTextTransform(tabStripItem); - } - - public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform): void { - const nestedLabel = tabStripItem.label; - const title = getTransformedText(nestedLabel.text, value); - tabStripItem.nativeViewProtected.setText(title); - } - - public getTabBarTextTransform(): TextTransform { - return this._textTransform; - } - - public setTabBarTextTransform(value: TextTransform): void { - let items = this.tabStrip && this.tabStrip.items; - if (items) { - items.forEach((tabStripItem) => { - if (tabStripItem.label && tabStripItem.nativeViewProtected) { - const nestedLabel = tabStripItem.label; - const title = getTransformedText(nestedLabel.text, value); - tabStripItem.nativeViewProtected.setText(title); - } - }); - } - this._textTransform = value; - } - - [selectedIndexProperty.setNative](value: number) { + protected setSelectedItem(value: number) { const smoothScroll = true; // TODO @@ -956,21 +532,6 @@ export class Tabs extends TabsBase { this._viewPager.setCurrentItem(value, smoothScroll); } - [itemsProperty.getDefault](): TabContentItem[] { - return null; - } - [itemsProperty.setNative](value: TabContentItem[]) { - this.setItems(value); - selectedIndexProperty.coerce(this); - } - - [tabStripProperty.getDefault](): TabStrip { - return null; - } - [tabStripProperty.setNative](value: TabStrip) { - this.setTabStripItems(value.items); - } - [swipeEnabledProperty.getDefault](): boolean { // TODO: create native method and get native? return true; @@ -985,15 +546,4 @@ export class Tabs extends TabsBase { [offscreenTabLimitProperty.setNative](value: number) { this._viewPager.setOffscreenPageLimit(value); } -} - -function tryCloneDrawable(value: android.graphics.drawable.Drawable, resources: android.content.res.Resources): android.graphics.drawable.Drawable { - if (value) { - const constantState = value.getConstantState(); - if (constantState) { - return constantState.newDrawable(resources); - } - } - - return value; -} +} \ No newline at end of file diff --git a/nativescript-core/ui/tabs/tabs.d.ts b/nativescript-core/ui/tabs/tabs.d.ts index feb397d9e..153136b29 100644 --- a/nativescript-core/ui/tabs/tabs.d.ts +++ b/nativescript-core/ui/tabs/tabs.d.ts @@ -5,9 +5,8 @@ import { EventData, Property } from "../core/view"; import { TabContentItem } from "../tab-navigation-base/tab-content-item"; -import { - SelectedIndexChangedEventData, TabNavigationBase -} from "../tab-navigation-base/tab-navigation-base"; +import { SelectedIndexChangedEventData } from "../tab-navigation-base/tab-navigation-base"; +import { TabbableComponent } from "../tabbable-component"; import { TabStrip } from "../tab-navigation-base/tab-strip"; export * from "../tab-navigation-base/tab-content-item"; @@ -18,7 +17,7 @@ export * from "../tab-navigation-base/tab-strip-item"; /** * Represents a swipeable tabs view. */ -export class Tabs extends TabNavigationBase { +export class Tabs extends TabbableComponent { /** * Gets or sets the items of the Tabs. */ diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BottomNavigationBar.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BottomNavigationBar.java index 13760a683..b9a6e62d1 100644 --- a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BottomNavigationBar.java +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BottomNavigationBar.java @@ -32,7 +32,7 @@ import android.widget.ImageView.ScaleType; import android.widget.LinearLayout; import android.widget.TextView; -public class BottomNavigationBar extends LinearLayout { +public class BottomNavigationBar extends LinearLayout implements TabbableBar { /** * Allows complete control over the colors drawn in the tab layout. Set with * {@link #setCustomTabColorizer(TabColorizer)}. diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabbableBar.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabbableBar.java new file mode 100644 index 000000000..f49e2f0a7 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabbableBar.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nativescript.widgets; + +import android.widget.LinearLayout; +import android.widget.TextView; + +public interface TabbableBar { + void setTabTextColor(int color); + int getTabTextColor(); + void setSelectedTabTextColor(int color); + int getSelectedTabTextColor(); + void setTabTextFontSize(float fontSize); + float getTabTextFontSize(); + void updateItemAt(int position, TabItemSpec tabItem); + TextView getTextViewForItemAt(int index); + LinearLayout getViewForItemAt(int index); + int getItemCount(); +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabsBar.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabsBar.java index b34137ba4..34b23a586 100644 --- a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabsBar.java +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabsBar.java @@ -48,7 +48,7 @@ import android.widget.TextView; * complete control over which color is used for any individual position. *

*/ -public class TabsBar extends HorizontalScrollView { +public class TabsBar extends HorizontalScrollView implements TabbableBar { /** * Allows complete control over the colors drawn in the tab layout. Set with * {@link #setCustomTabColorizer(TabColorizer)}. diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index 35da58adb..5a8cfb662 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -410,7 +410,21 @@ getItemCount(): number; } - export class TabsBar extends android.widget.HorizontalScrollView { + export interface TabbableBar extends android.view.ViewGroup { + setTabTextColor(color: number): void; + getTabTextColor(): number; + setSelectedTabTextColor(color: number): void; + getSelectedTabTextColor(): number; + setTabTextFontSize(fontSize: number): void; + getTabTextFontSize(): number; + + updateItemAt(position: number, itemSpec: TabItemSpec): void; + getTextViewForItemAt(index: number): android.widget.TextView; + getViewForItemAt(index: number): android.widget.LinearLayout; + getItemCount(): number; + } + + export class TabsBar extends android.widget.HorizontalScrollView implements TabbableBar { constructor(context: android.content.Context); constructor(context: android.content.Context, attrs: android.util.AttributeSet); constructor(context: android.content.Context, attrs: android.util.AttributeSet, defStyle: number); @@ -432,7 +446,7 @@ getItemCount(): number; } - export class BottomNavigationBar extends android.widget.LinearLayout { + export class BottomNavigationBar extends android.widget.LinearLayout implements TabbableBar { constructor(context: android.content.Context); constructor(context: android.content.Context, attrs: android.util.AttributeSet); constructor(context: android.content.Context, attrs: android.util.AttributeSet, defStyle: number);