diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index 6bb4354fc..834f4fa39 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -1239,6 +1239,9 @@ export class iOSApplication { window: any /* UIWindow */; } +// @public +export type IOSTabBarItemsAlignment = "leading" | "justified" | "center" | "centerSelected"; + // @public export const isAndroid: boolean; @@ -2274,6 +2277,8 @@ export class Tabs extends TabNavigationBase { ios: any /* UITabBarController */; + iOSTabBarItemsAlignment: IOSTabBarItemsAlignment; + items: Array; offscreenTabLimit: number; diff --git a/e2e/ui-tests-app/app/tabs/main-page.ts b/e2e/ui-tests-app/app/tabs/main-page.ts index 0edd590c1..de21578a6 100644 --- a/e2e/ui-tests-app/app/tabs/main-page.ts +++ b/e2e/ui-tests-app/app/tabs/main-page.ts @@ -32,6 +32,7 @@ export function loadExamples() { examples.set("nested-layout", "tabs/nested-layout-page"); examples.set("nested-bottom-navigation", "tabs/nested-bottom-navigation-page"); examples.set("custom-tabstrip", "tabs/custom-tabstrip-page"); + examples.set("frame-in-tabs", "tabs/frame-in-tabs"); return examples; } diff --git a/e2e/ui-tests-app/app/tabs/tab-strip-items-page.xml b/e2e/ui-tests-app/app/tabs/tab-strip-items-page.xml index e98df7aa5..b27c15739 100644 --- a/e2e/ui-tests-app/app/tabs/tab-strip-items-page.xml +++ b/e2e/ui-tests-app/app/tabs/tab-strip-items-page.xml @@ -1,5 +1,5 @@ - + diff --git a/nativescript-core/platforms/ios/native-api-usage.json b/nativescript-core/platforms/ios/native-api-usage.json index 7e5dff0bd..9d144e8fd 100644 --- a/nativescript-core/platforms/ios/native-api-usage.json +++ b/nativescript-core/platforms/ios/native-api-usage.json @@ -14,6 +14,7 @@ "Foundation.*:*", "MaterialComponents.MDCTabBar:*", + "MaterialComponents.MDCTabBarIndicatorTemplate:*", "NativeScriptEmbedder:*", diff --git a/nativescript-core/platforms/ios/typings/objc!MaterialComponents.d.ts b/nativescript-core/platforms/ios/typings/objc!MaterialComponents.d.ts index 92ce4a5b4..71b6b1478 100644 --- a/nativescript-core/platforms/ios/typings/objc!MaterialComponents.d.ts +++ b/nativescript-core/platforms/ios/typings/objc!MaterialComponents.d.ts @@ -442,13 +442,17 @@ declare class MDCTabBar extends UIView implements UIBarPositioning { delegate: MDCTabBarDelegate; - displaysUppercaseTitles: boolean; + displaysUppercaseTitles: boolean; - inkColor: UIColor; + enableRippleBehavior: boolean; - itemAppearance: MDCTabBarItemAppearance; + inkColor: UIColor; - items: NSArray; + itemAppearance: MDCTabBarItemAppearance; + + items: NSArray; + + rippleColor: UIColor; selectedItem: UITabBarItem; diff --git a/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts b/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts index 34de5c4ef..b59f0414b 100644 --- a/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts +++ b/nativescript-core/ui/bottom-navigation/bottom-navigation.android.ts @@ -36,6 +36,11 @@ 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; } @@ -243,6 +248,7 @@ export class BottomNavigation extends TabNavigationBase { private _currentTransaction: androidx.fragment.app.FragmentTransaction; private _attachedToWindow = false; public _originalBackground: any; + private _textTransform: TextTransform = "none"; constructor() { super(); @@ -562,6 +568,18 @@ export class BottomNavigation extends TabNavigationBase { }); } + 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(); @@ -570,10 +588,8 @@ export class BottomNavigation extends TabNavigationBase { let title = titleLabel.text; // TEXT-TRANSFORM - const textTransform = titleLabel.style.textTransform; - if (textTransform) { - title = getTransformedText(title, textTransform); - } + const textTransform = this.getItemLabelTextTransform(tabStripItem); + title = getTransformedText(title, textTransform); tabItemSpec.title = title; // BACKGROUND-COLOR @@ -596,12 +612,13 @@ export class BottomNavigation extends TabNavigationBase { // ICON const iconSource = tabStripItem.image && tabStripItem.image.src; if (iconSource) { - const icon = this.getIcon(tabStripItem); + const iconInfo = this.getIconInfo(tabStripItem); - if (icon) { + 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 = icon; + tabItemSpec.iconDrawable = iconInfo.drawable; + tabItemSpec.imageHeight = iconInfo.height; } else { // TODO: // traceMissingIcon(iconSource); @@ -612,7 +629,7 @@ export class BottomNavigation extends TabNavigationBase { return tabItemSpec; } - private getIcon(tabStripItem: TabStripItem): android.graphics.drawable.BitmapDrawable { + private getOriginalIcon(tabStripItem: TabStripItem): android.graphics.Bitmap { const iconSource = tabStripItem.image && tabStripItem.image.src; if (!iconSource) { return null; @@ -629,21 +646,30 @@ export class BottomNavigation extends TabNavigationBase { is = ImageSource.fromFileOrResourceSync(iconSource); } - let imageDrawable: android.graphics.drawable.BitmapDrawable; - if (is && is.android) { - let image = is.android; + return is && is.android; + } + private getDrawableInfo(image: android.graphics.Bitmap): IconInfo { + if (image) { if (this.tabStrip && this.tabStrip.isIconSizeFixed) { image = this.getFixedSizeIcon(image); } - imageDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), image); - } else { - // TODO - // traceMissingIcon(iconSource); + let imageDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), image); + + return { + drawable: imageDrawable, + height: image.getHeight() + }; } - return imageDrawable; + return new IconInfo(); + } + + private getIconInfo(tabStripItem: TabStripItem): IconInfo { + let originalIcon = this.getOriginalIcon(tabStripItem); + + return this.getDrawableInfo(originalIcon); } private getFixedSizeIcon(image: android.graphics.Bitmap): android.graphics.Bitmap { @@ -702,9 +728,9 @@ export class BottomNavigation extends TabNavigationBase { const index = tabStripItem._index; const tabBarItem = this._bottomNavigationBar.getViewForItemAt(index); const imgView = tabBarItem.getChildAt(0); - const drawable = this.getIcon(tabStripItem); + const drawableInfo = this.getIconInfo(tabStripItem); - imgView.setImageDrawable(drawable); + imgView.setImageDrawable(drawableInfo.drawable); } public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { @@ -721,6 +747,24 @@ export class BottomNavigation extends TabNavigationBase { 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; diff --git a/nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts b/nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts index f80207282..9c0f4b703 100644 --- a/nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts +++ b/nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts @@ -71,20 +71,19 @@ class UITabBarControllerImpl extends UITabBarController { public viewWillTransitionToSizeWithTransitionCoordinator(size: CGSize, coordinator: UIViewControllerTransitionCoordinator): void { super.viewWillTransitionToSizeWithTransitionCoordinator(size, coordinator); - UIViewControllerTransitionCoordinator.prototype.animateAlongsideTransitionCompletion - .call(coordinator, () => { - const owner = this._owner.get(); - if (owner && owner.tabStrip && owner.tabStrip.items) { - const tabStrip = owner.tabStrip; - tabStrip.items.forEach(tabStripItem => { - updateBackgroundPositions(tabStrip, tabStripItem); + coordinator.animateAlongsideTransitionCompletion(() => { + const owner = this._owner.get(); + if (owner && owner.tabStrip && owner.tabStrip.items) { + const tabStrip = owner.tabStrip; + tabStrip.items.forEach(tabStripItem => { + updateBackgroundPositions(tabStrip, tabStripItem); - const index = tabStripItem._index; - const tabBarItemController = this.viewControllers[index]; - updateTitleAndIconPositions(tabStripItem, tabBarItemController.tabBarItem, tabBarItemController); - }); - } - }, null); + const index = tabStripItem._index; + const tabBarItemController = this.viewControllers[index]; + updateTitleAndIconPositions(tabStripItem, tabBarItemController.tabBarItem, tabBarItemController); + }); + } + }, null); } // Mind implementation for other controllers @@ -207,17 +206,14 @@ class UINavigationControllerDelegateImpl extends NSObject implements UINavigatio function updateBackgroundPositions(tabStrip: TabStrip, tabStripItem: TabStripItem) { let bgView = (tabStripItem).bgView; + const index = tabStripItem._index; + const width = tabStrip.nativeView.frame.size.width / tabStrip.items.length; + const frame = CGRectMake(width * index, 0, width, tabStrip.nativeView.frame.size.width); if (!bgView) { - const index = tabStripItem._index; - const width = tabStrip.nativeView.frame.size.width / tabStrip.items.length; - const frame = CGRectMake(width * index, 0, width, tabStrip.nativeView.frame.size.width); bgView = UIView.alloc().initWithFrame(frame); tabStrip.nativeView.insertSubviewAtIndex(bgView, 0); (tabStripItem).bgView = bgView; } else { - const index = tabStripItem._index; - const width = tabStrip.nativeView.frame.size.width / tabStrip.items.length; - const frame = CGRectMake(width * index, 0, width, tabStrip.nativeView.frame.size.width); bgView.frame = frame; } @@ -376,8 +372,7 @@ export class BottomNavigation extends TabNavigationBase { } public setTabBarItemColor(tabStripItem: TabStripItem, value: UIColor | Color): void { - const states = getTitleAttributesForStates(tabStripItem.label); - applyStatesToItem(tabStripItem.nativeView, states, this.viewController.tabBar); + setViewTextAttributes(tabStripItem.nativeView, tabStripItem.label, this.viewController.tabBar); } public setTabBarIconColor(tabStripItem: TabStripItem, value: UIColor | Color): void { @@ -388,8 +383,7 @@ export class BottomNavigation extends TabNavigationBase { } public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { - const states = getTitleAttributesForStates(tabStripItem.label); - applyStatesToItem(tabStripItem.nativeView, states, this.viewController.tabBar); + setViewTextAttributes(tabStripItem.nativeView, tabStripItem.label, this.viewController.tabBar); } public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform): void { @@ -397,6 +391,15 @@ export class BottomNavigation extends TabNavigationBase { tabStripItem.nativeView.title = title; } + public getTabBarHighlightColor(): UIColor { + return this._ios.tabBar.tintColor; + } + + public setTabBarHighlightColor(value: UIColor | Color) { + const nativeColor = value instanceof Color ? value.ios : value; + this._ios.tabBar.tintColor = nativeColor; + } + public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void { const width = layout.getMeasureSpecSize(widthMeasureSpec); const widthMode = layout.getMeasureSpecMode(widthMeasureSpec); @@ -518,8 +521,7 @@ export class BottomNavigation extends TabNavigationBase { const tabBarItem = this.createTabBarItem(tabStripItem, i); updateTitleAndIconPositions(tabStripItem, tabBarItem, controller); - const states = getTitleAttributesForStates(tabStripItem.label); - applyStatesToItem(tabBarItem, states, this.viewController.tabBar); + setViewTextAttributes(tabBarItem, tabStripItem.label, this.viewController.tabBar); controller.tabBarItem = tabBarItem; tabStripItem._index = i; @@ -688,51 +690,31 @@ export class BottomNavigation extends TabNavigationBase { } } -interface TabStates { - normalState?: any; - selectedState?: any; -} - -function getTitleAttributesForStates(view: View): TabStates { +function setViewTextAttributes(item: UITabBarItem, view: View, tabBar: UITabBar): any { if (!view) { return null; } - const result: TabStates = {}; const defaultTabItemFontSize = 10; const tabItemFontSize = view.style.fontSize || defaultTabItemFontSize; const font: UIFont = view.style.fontInternal.getUIFont(UIFont.systemFontOfSize(tabItemFontSize)); const tabItemTextColor = view.style.color; const textColor = tabItemTextColor instanceof Color ? tabItemTextColor.ios : null; - result.normalState = { [NSFontAttributeName]: font }; + let attributes: any = { [NSFontAttributeName]: font }; if (textColor) { - result.normalState[UITextAttributeTextColor] = textColor; + attributes[UITextAttributeTextColor] = textColor; + attributes[NSForegroundColorAttributeName] = textColor; } - const tabSelectedItemTextColor = view.style.color; - const selectedTextColor = tabSelectedItemTextColor instanceof Color ? tabSelectedItemTextColor.ios : null; - result.selectedState = { [NSFontAttributeName]: font }; - if (selectedTextColor) { - result.selectedState[UITextAttributeTextColor] = selectedTextColor; - } - - return result; -} - -function applyStatesToItem(item: UITabBarItem, states: TabStates, tabBar: UITabBar) { - if (!states) { - return; - } - - item.setTitleTextAttributesForState(states.normalState, UIControlState.Normal); - item.setTitleTextAttributesForState(states.selectedState, UIControlState.Selected); + item.setTitleTextAttributesForState(attributes, UIControlState.Selected); + item.setTitleTextAttributesForState(attributes, UIControlState.Normal); // there's a bug when setting the item color on ios 13 if there's no background set to the tabstrip // https://books.google.bg/books?id=99_BDwAAQBAJ&q=tabBar.unselectedItemTintColor // to fix the above issue we are applying the selected fix only for the case, when there is no background set // in that case we have the following known issue: // we will set the color to all unselected items, so you won't be able to set different colors for the different not selected items - if (!tabBar.barTintColor && states.normalState[UITextAttributeTextColor] && (majorVersion > 9)) { - tabBar.unselectedItemTintColor = states.normalState[UITextAttributeTextColor]; + if (!tabBar.barTintColor && attributes[UITextAttributeTextColor] && (majorVersion > 9)) { + tabBar.unselectedItemTintColor = attributes[UITextAttributeTextColor]; } -} +} \ No newline at end of file diff --git a/nativescript-core/ui/core/view/view-common.ts b/nativescript-core/ui/core/view/view-common.ts index d81d27ee2..8a8cea523 100644 --- a/nativescript-core/ui/core/view/view-common.ts +++ b/nativescript-core/ui/core/view/view-common.ts @@ -26,6 +26,7 @@ import { sanitizeModuleName } from "../../builder/module-name-sanitizer"; import { StyleScope } from "../../styling/style-scope"; import { LinearGradient } from "../../styling/linear-gradient"; import { BackgroundRepeat } from "../../styling/style-properties"; +import { TextTransform } from "../../text-base"; export * from "../../styling/style-properties"; export * from "../view-base"; @@ -718,6 +719,13 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.perspective = value; } + get textTransform(): TextTransform { + return this.style.textTransform; + } + set textTransform(value: TextTransform) { + this.style.textTransform = value; + } + get translateX(): dip { return this.style.translateX; } diff --git a/nativescript-core/ui/core/view/view.ios.ts b/nativescript-core/ui/core/view/view.ios.ts index 18ab564a6..789d76577 100644 --- a/nativescript-core/ui/core/view/view.ios.ts +++ b/nativescript-core/ui/core/view/view.ios.ts @@ -180,7 +180,7 @@ export class View extends ViewCommon implements ViewDefinition { if (adjustedFrame) { nativeView.frame = adjustedFrame; } - + if (this._hasTransfrom) { // re-apply the transform after the frame is adjusted nativeView.layer.transform = transform; @@ -359,7 +359,7 @@ export class View extends ViewCommon implements ViewDefinition { if (this.rotateX || this.rotateY) { transform.m34 = -1 / perspective; } - + transform = CATransform3DTranslate(transform, this.translateX, this.translateY, 0); transform = iosNativeHelper.applyRotateTransform(transform, this.rotateX, this.rotateY, this.rotate); transform = CATransform3DScale(transform, scaleX, scaleY, 1); @@ -479,8 +479,7 @@ export class View extends ViewCommon implements ViewDefinition { parentController.presentViewControllerAnimatedCompletion(controller, animated, null); const transitionCoordinator = parentController.transitionCoordinator; if (transitionCoordinator) { - UIViewControllerTransitionCoordinator.prototype.animateAlongsideTransitionCompletion - .call(transitionCoordinator, null, () => this._raiseShownModallyEvent()); + transitionCoordinator.animateAlongsideTransitionCompletion(null, () => this._raiseShownModallyEvent()); } else { // Apparently iOS 9+ stops all transitions and animations upon application suspend and transitionCoordinator becomes null here in this case. // Since we are not waiting for any transition to complete, i.e. transitionCoordinator is null, we can directly raise our shownModally event. diff --git a/nativescript-core/ui/index.d.ts b/nativescript-core/ui/index.d.ts index 3ac104e6c..ac5934ff0 100644 --- a/nativescript-core/ui/index.d.ts +++ b/nativescript-core/ui/index.d.ts @@ -34,7 +34,7 @@ export { TabNavigationBase } from "./tab-navigation-base/tab-navigation-base"; export { TabStrip, TabStripItemEventData } from "./tab-navigation-base/tab-strip"; export { TabStripItem } from "./tab-navigation-base/tab-strip-item"; export { TabView, TabViewItem } from "./tab-view"; -export { Tabs } from "./tabs"; +export { Tabs, IOSTabBarItemsAlignment } from "./tabs"; export { TextBase } from "./text-base"; export { FormattedString } from "./text-base/formatted-string"; export { Span } from "./text-base/span"; diff --git a/nativescript-core/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts b/nativescript-core/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts index ee035bf1f..011d283ef 100644 --- a/nativescript-core/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts +++ b/nativescript-core/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts @@ -286,4 +286,4 @@ export const tabStripProperty = new Property({ target.onTabStripChanged(oldValue, newValue); } }); -tabStripProperty.register(TabNavigationBase); +tabStripProperty.register(TabNavigationBase); \ No newline at end of file diff --git a/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts b/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts index ead226c81..240cebec7 100644 --- a/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts +++ b/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts @@ -13,6 +13,7 @@ import { import { isIOS } from "../../../platform"; import { Image } from "../../image/image"; import { Label } from "../../label/label"; +import { textTransformProperty, TextTransform } from "../../text-base"; export * from "../../core/view"; export const traceCategory = "TabView"; @@ -236,6 +237,19 @@ export class TabStripItem extends View implements TabStripItemDefinition, AddChi return tabStripParent && tabStripParent.setTabBarItemBackgroundColor(this, value); } + [textTransformProperty.getDefault](): TextTransform { + const parent = this.parent; + const tabStripParent = parent && parent.parent; + + return tabStripParent && tabStripParent.getTabBarItemTextTransform(this); + } + [textTransformProperty.setNative](value: TextTransform) { + const parent = this.parent; + const tabStripParent = parent && parent.parent; + + return tabStripParent && tabStripParent.setTabBarItemTextTransform(this, value); + } + [backgroundInternalProperty.getDefault](): any { return null; } diff --git a/nativescript-core/ui/tab-view/tab-view.ios.ts b/nativescript-core/ui/tab-view/tab-view.ios.ts index 7d0cef33c..ec96eaaab 100644 --- a/nativescript-core/ui/tab-view/tab-view.ios.ts +++ b/nativescript-core/ui/tab-view/tab-view.ios.ts @@ -63,13 +63,12 @@ class UITabBarControllerImpl extends UITabBarController { public viewWillTransitionToSizeWithTransitionCoordinator(size: CGSize, coordinator: UIViewControllerTransitionCoordinator): void { super.viewWillTransitionToSizeWithTransitionCoordinator(size, coordinator); - UIViewControllerTransitionCoordinator.prototype.animateAlongsideTransitionCompletion - .call(coordinator, null, () => { - const owner = this._owner.get(); - if (owner && owner.items) { - owner.items.forEach(tabItem => tabItem._updateTitleAndIconPositions()); - } - }); + coordinator.animateAlongsideTransitionCompletion(null, () => { + const owner = this._owner.get(); + if (owner && owner.items) { + owner.items.forEach(tabItem => tabItem._updateTitleAndIconPositions()); + } + }); } // Mind implementation for other controllers diff --git a/nativescript-core/ui/tabs/tabs-common.ts b/nativescript-core/ui/tabs/tabs-common.ts index 71ff7f7d8..56f3f2ad9 100644 --- a/nativescript-core/ui/tabs/tabs-common.ts +++ b/nativescript-core/ui/tabs/tabs-common.ts @@ -22,6 +22,7 @@ export class TabsBase extends TabNavigationBase implements TabsDefinition { public swipeEnabled: boolean; public offscreenTabLimit: number; public tabsPosition: "top" | "bottom"; + public iOSTabBarItemsAlignment: IOSTabBarItemsAlignment; } // TODO: Add Unit tests @@ -39,3 +40,7 @@ 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.android.ts b/nativescript-core/ui/tabs/tabs.android.ts index a2ab442df..afdbab527 100644 --- a/nativescript-core/ui/tabs/tabs.android.ts +++ b/nativescript-core/ui/tabs/tabs.android.ts @@ -373,6 +373,7 @@ export class Tabs extends TabsBase { private _pagerAdapter: androidx.viewpager.widget.PagerAdapter; private _androidViewId: number = -1; public _originalBackground: any; + private _textTransform: TextTransform = "uppercase"; constructor() { super(); @@ -637,6 +638,18 @@ export class Tabs extends TabsBase { }); } + 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(); @@ -645,10 +658,8 @@ export class Tabs extends TabsBase { let title = nestedLabel.text; // TEXT-TRANSFORM - const textTransform = nestedLabel.style.textTransform; - if (textTransform) { - title = getTransformedText(title, textTransform); - } + const textTransform = this.getItemLabelTextTransform(tabStripItem); + title = getTransformedText(title, textTransform); tabItemSpec.title = title; // BACKGROUND-COLOR @@ -817,7 +828,7 @@ export class Tabs extends TabsBase { const tabBarItem = this._tabsBar.getViewForItemAt(index); const imgView = tabBarItem.getChildAt(0); const drawable = this.getIcon(tabStripItem); - + imgView.setImageDrawable(drawable); } @@ -828,12 +839,34 @@ export class Tabs extends TabsBase { 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) { const smoothScroll = true; diff --git a/nativescript-core/ui/tabs/tabs.d.ts b/nativescript-core/ui/tabs/tabs.d.ts index 17e740e91..feb397d9e 100644 --- a/nativescript-core/ui/tabs/tabs.d.ts +++ b/nativescript-core/ui/tabs/tabs.d.ts @@ -49,6 +49,16 @@ export class Tabs extends TabNavigationBase { */ tabsPosition: "top" | "bottom"; + /** + * Gets or set the MDCTabBarAlignment of the tab bar icons in iOS. Defaults to "justified" + * Valid values are: + * - leading + * - justified + * - center + * - centerSelected + */ + iOSTabBarItemsAlignment: IOSTabBarItemsAlignment; + /** * 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. */ @@ -81,3 +91,12 @@ export class Tabs extends TabNavigationBase { export const itemsProperty: Property; export const tabStripProperty: Property export const selectedIndexProperty: Property; + +/** + * IOS Alignment of the Tabs TabStrip to use. + * - `leading` - tab items are aligned to the left + * - `justified` - tab strip is split equally to all the tab items + * - `center` - tabs items are aligned in the center + * - `centerSelected` - tab items move to make the selected tab in the center + */ +export type IOSTabBarItemsAlignment = "leading" | "justified" | "center" | "centerSelected"; diff --git a/nativescript-core/ui/tabs/tabs.ios.ts b/nativescript-core/ui/tabs/tabs.ios.ts index 9a59ce5b1..c8ce759d4 100644 --- a/nativescript-core/ui/tabs/tabs.ios.ts +++ b/nativescript-core/ui/tabs/tabs.ios.ts @@ -7,14 +7,15 @@ import { TextTransform, ViewBase } from "../text-base"; // Requires import { Color } from "../../color"; import { ImageSource } from "../../image-source"; +import { device } from "../../platform"; import { ios as iosUtils, isFontIconURI, layout } from "../../utils/utils"; import { ios as iosView, View } from "../core/view"; import { Frame } from "../frame"; import { Font } from "../styling/font"; import { - getIconSpecSize, itemsProperty, selectedIndexProperty, tabStripProperty + getIconSpecSize, itemsProperty, selectedIndexProperty, tabStripProperty, } from "../tab-navigation-base/tab-navigation-base"; -import { swipeEnabledProperty, TabsBase } from "./tabs-common"; +import { swipeEnabledProperty, TabsBase, IOSTabBarItemsAlignment, iOSTabBarItemsAlignmentProperty } from "./tabs-common"; // TODO // import { profile } from "../../profiling"; @@ -22,6 +23,7 @@ import { swipeEnabledProperty, TabsBase } from "./tabs-common"; export * from "./tabs-common"; const majorVersion = iosUtils.MajorVersion; +const isPhone = device.deviceType === "Phone"; // Equivalent to dispatch_async(dispatch_get_main_queue(...)) call const invokeOnRunLoop = (function () { @@ -74,6 +76,17 @@ class MDCTabBarDelegateImpl extends NSObject implements MDCTabBarDelegate { } } +class BackgroundIndicatorTemplate extends NSObject implements MDCTabBarIndicatorTemplate { + public static ObjCProtocols = [MDCTabBarIndicatorTemplate]; + + public indicatorAttributesForContext(context: MDCTabBarIndicatorContext): MDCTabBarIndicatorAttributes { + let attributes = new MDCTabBarIndicatorAttributes(); + attributes.path = UIBezierPath.bezierPathWithRect(context.bounds); + + return attributes; + } +} + class UIPageViewControllerImpl extends UIPageViewController { tabBar: MDCTabBar; scrollView: UIScrollView; @@ -114,7 +127,7 @@ class UIPageViewControllerImpl extends UIPageViewController { } tabBar.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleBottomMargin; - tabBar.alignment = MDCTabBarAlignment.Leading; + tabBar.alignment = MDCTabBarAlignment.Justified; tabBar.sizeToFit(); this.tabBar = tabBar; @@ -223,6 +236,24 @@ class UIPageViewControllerImpl extends UIPageViewController { } } } + + public viewWillTransitionToSizeWithTransitionCoordinator(size: CGSize, coordinator: UIViewControllerTransitionCoordinator): void { + super.viewWillTransitionToSizeWithTransitionCoordinator(size, coordinator); + coordinator.animateAlongsideTransitionCompletion(() => { + const owner = this._owner.get(); + if (owner && owner.tabStrip && owner.tabStrip.items) { + const tabStrip = owner.tabStrip; + tabStrip.items.forEach(tabStripItem => { + updateBackgroundPositions(tabStrip, tabStripItem, + this.tabBar.alignment !== MDCTabBarAlignment.Justified || (owner.selectedIndex !== tabStripItem._index) ? owner._defaultItemBackgroundColor : null); + + const index = tabStripItem._index; + const tabBarItemController = owner.viewControllers[index]; + updateTitleAndIconPositions(tabStripItem, tabBarItemController.tabBarItem, tabBarItemController); + }); + } + }, null); + } } class UIPageViewControllerDataSourceImpl extends NSObject implements UIPageViewControllerDataSource { @@ -355,167 +386,6 @@ class UIPageViewControllerDelegateImpl extends NSObject implements UIPageViewCon } } -// class UITabBarControllerImpl extends UITabBarController { - -// private _owner: WeakRef; - -// public static initWithOwner(owner: WeakRef): UITabBarControllerImpl { -// let handler = UITabBarControllerImpl.new(); -// handler._owner = owner; -// return handler; -// } - -// @profile -// public viewWillAppear(animated: boolean): void { -// super.viewWillAppear(animated); -// const owner = this._owner.get(); -// if (!owner) { -// return; -// } - -// // Unify translucent and opaque bars layout -// this.extendedLayoutIncludesOpaqueBars = true; - -// iosView.updateAutoAdjustScrollInsets(this, owner); - -// if (!owner.parent) { -// owner.callLoaded(); -// } -// } - -// @profile -// public viewDidDisappear(animated: boolean): void { -// super.viewDidDisappear(animated); -// const owner = this._owner.get(); -// if (owner && !owner.parent && owner.isLoaded && !this.presentedViewController) { -// owner.callUnloaded(); -// } -// } - -// public viewWillTransitionToSizeWithTransitionCoordinator(size: CGSize, coordinator: UIViewControllerTransitionCoordinator): void { -// super.viewWillTransitionToSizeWithTransitionCoordinator(size, coordinator); -// UIViewControllerTransitionCoordinator.prototype.animateAlongsideTransitionCompletion -// .call(coordinator, null, () => { -// const owner = this._owner.get(); -// if (owner && owner.items) { -// // owner.items.forEach(tabItem => tabItem._updateTitleAndIconPositions()); TODO: -// } -// }); -// } -// } - -// class UITabBarControllerDelegateImpl extends NSObject implements UITabBarControllerDelegate { -// public static ObjCProtocols = [UITabBarControllerDelegate]; - -// private _owner: WeakRef; - -// public static initWithOwner(owner: WeakRef): UITabBarControllerDelegateImpl { -// let delegate = UITabBarControllerDelegateImpl.new(); -// delegate._owner = owner; -// return delegate; -// } - -// public tabBarControllerShouldSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): boolean { -// if (traceEnabled()) { -// traceWrite("TabView.delegate.SHOULD_select(" + tabBarController + ", " + viewController + ");", traceCategories.Debug); -// } - -// let owner = this._owner.get(); -// if (owner) { -// // "< More" cannot be visible after clicking on the main tab bar buttons. -// let backToMoreWillBeVisible = false; -// owner._handleTwoNavigationBars(backToMoreWillBeVisible); -// } - -// if ((tabBarController).selectedViewController === viewController) { -// return false; -// } - -// (tabBarController)._willSelectViewController = viewController; - -// return true; -// } - -// public tabBarControllerDidSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): void { -// if (traceEnabled()) { -// traceWrite("TabView.delegate.DID_select(" + tabBarController + ", " + viewController + ");", traceCategories.Debug); -// } - -// const owner = this._owner.get(); -// if (owner) { -// owner._onViewControllerShown(viewController); -// } - -// (tabBarController)._willSelectViewController = undefined; -// } -// } - -// class UINavigationControllerDelegateImpl extends NSObject implements UINavigationControllerDelegate { -// public static ObjCProtocols = [UINavigationControllerDelegate]; - -// private _owner: WeakRef; - -// public static initWithOwner(owner: WeakRef): UINavigationControllerDelegateImpl { -// let delegate = UINavigationControllerDelegateImpl.new(); -// delegate._owner = owner; -// return delegate; -// } - -// navigationControllerWillShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { -// if (traceEnabled()) { -// traceWrite("TabView.moreNavigationController.WILL_show(" + navigationController + ", " + viewController + ", " + animated + ");", traceCategories.Debug); -// } - -// let owner = this._owner.get(); -// if (owner) { -// // If viewController is one of our tab item controllers, then "< More" will be visible shortly. -// // Otherwise viewController is the UIMoreListController which shows the list of all tabs beyond the 4th tab. -// let backToMoreWillBeVisible = owner._ios.viewControllers.containsObject(viewController); -// owner._handleTwoNavigationBars(backToMoreWillBeVisible); -// } -// } - -// navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { -// if (traceEnabled()) { -// traceWrite("TabView.moreNavigationController.DID_show(" + navigationController + ", " + viewController + ", " + animated + ");", traceCategories.Debug); -// } -// // We don't need Edit button in More screen. -// navigationController.navigationBar.topItem.rightBarButtonItem = null; -// let owner = this._owner.get(); -// if (owner) { -// owner._onViewControllerShown(viewController); -// } -// } -// } - -// function updateTitleAndIconPositions(tabStripItem: TabStripItem, tabBarItem: UITabBarItem, controller: UIViewController) { -// if (!tabStripItem || !tabBarItem) { -// return; -// } - -// // For iOS <11 icon is *always* above the text. -// // For iOS 11 icon is above the text *only* on phones in portrait mode. -// const orientation = controller.interfaceOrientation; -// const isPortrait = orientation !== UIInterfaceOrientation.LandscapeLeft && orientation !== UIInterfaceOrientation.LandscapeRight; -// const isIconAboveTitle = (majorVersion < 11) || (isPhone && isPortrait); - -// if (!tabStripItem.iconSource) { -// if (isIconAboveTitle) { -// tabBarItem.titlePositionAdjustment = { horizontal: 0, vertical: -20 }; -// } else { -// tabBarItem.titlePositionAdjustment = { horizontal: 0, vertical: 0 }; -// } -// } - -// if (!tabStripItem.title) { -// if (isIconAboveTitle) { -// tabBarItem.imageInsets = new UIEdgeInsets({ top: 6, left: 0, bottom: -6, right: 0 }); -// } else { -// tabBarItem.imageInsets = new UIEdgeInsets({ top: 0, left: 0, bottom: 0, right: 0 }); -// } -// } -// } - 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); @@ -524,6 +394,53 @@ function iterateIndexRange(index: number, eps: number, lastIndex: number, callba } } +function updateBackgroundPositions(tabStrip: TabStrip, tabStripItem: TabStripItem, color: UIColor = null) { + let bgView = (tabStripItem).bgView; + const index = tabStripItem._index; + let width = tabStrip.nativeView.frame.size.width / tabStrip.items.length; + const frame = CGRectMake(width * index, 0, width, tabStrip.nativeView.frame.size.width); + if (!bgView) { + bgView = UIView.alloc().initWithFrame(frame); + tabStrip.nativeView.insertSubviewAtIndex(bgView, 0); + (tabStripItem).bgView = bgView; + } + else { + bgView.frame = frame; + } + + const backgroundColor = tabStripItem.style.backgroundColor; + bgView.backgroundColor = color || (backgroundColor instanceof Color ? backgroundColor.ios : backgroundColor); + +} + +function updateTitleAndIconPositions(tabStripItem: TabStripItem, tabBarItem: UITabBarItem, controller: UIViewController) { + if (!tabStripItem || !tabBarItem) { + return; + } + + // For iOS <11 icon is *always* above the text. + // For iOS 11 icon is above the text *only* on phones in portrait mode. + const orientation = controller.interfaceOrientation; + const isPortrait = orientation !== UIInterfaceOrientation.LandscapeLeft && orientation !== UIInterfaceOrientation.LandscapeRight; + const isIconAboveTitle = (majorVersion < 11) || (isPhone && isPortrait); + + if (!tabStripItem.iconSource) { + if (isIconAboveTitle) { + tabBarItem.titlePositionAdjustment = { horizontal: 0, vertical: -20 }; + } else { + tabBarItem.titlePositionAdjustment = { horizontal: 0, vertical: 0 }; + } + } + + if (!tabStripItem.title) { + if (isIconAboveTitle) { + tabBarItem.imageInsets = new UIEdgeInsets({ top: 6, left: 0, bottom: -6, right: 0 }); + } else { + tabBarItem.imageInsets = new UIEdgeInsets({ top: 0, left: 0, bottom: 0, right: 0 }); + } + } +} + export class Tabs extends TabsBase { public nativeViewProtected: UIView; public selectedIndex: number; @@ -542,11 +459,12 @@ export class Tabs extends TabsBase { private _delegate: UIPageViewControllerDelegateImpl; // private _moreNavigationControllerDelegate: UINavigationControllerDelegateImpl; private _iconsCache = {}; + private _backgroundIndicatorColor: UIColor; + public _defaultItemBackgroundColor: UIColor; constructor() { super(); - // this.viewController = this._ios = UIPageViewControllerImpl.initWithOwner(new WeakRef(this)); // .alloc().initWithTransitionStyleNavigationOrientationOptions(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal, null); // UITabBarControllerImpl.initWithOwner(new WeakRef(this)); this.viewController = this._ios = UIPageViewControllerImpl.initWithOwner(new WeakRef(this)); //alloc().initWithTransitionStyleNavigationOrientationOptions(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal, null);; } @@ -558,8 +476,6 @@ export class Tabs extends TabsBase { super.initNativeView(); this._dataSource = UIPageViewControllerDataSourceImpl.initWithOwner(new WeakRef(this)); this._delegate = UIPageViewControllerDelegateImpl.initWithOwner(new WeakRef(this)); - // this._delegate = UITabBarControllerDelegateImpl.initWithOwner(new WeakRef(this)); - // this._moreNavigationControllerDelegate = UINavigationControllerDelegateImpl.initWithOwner(new WeakRef(this)); } disposeNativeView() { @@ -567,7 +483,6 @@ export class Tabs extends TabsBase { this._delegate = null; this._ios.tabBarDelegate = null; this._ios.tabBar = null; - // this._moreNavigationControllerDelegate = null; super.disposeNativeView(); } @@ -576,23 +491,21 @@ export class Tabs extends TabsBase { public onLoaded() { super.onLoaded(); + this.setViewControllers(this.items); + const selectedIndex = this.selectedIndex; const selectedView = this.items && this.items[selectedIndex] && this.items[selectedIndex].content; if (selectedView instanceof Frame) { - (selectedView)._pushInFrameStackRecursive(); + selectedView._pushInFrameStackRecursive(); } this._ios.dataSource = this._dataSource; this._ios.delegate = this._delegate; - - const tabStripItems = this.tabStrip ? this.tabStrip.items : null; - this.setTabStripItems(tabStripItems); } public onUnloaded() { this._ios.dataSource = null; this._ios.delegate = null; - // this._ios.moreNavigationController.delegate = null; super.onUnloaded(); } @@ -614,20 +527,33 @@ export class Tabs extends TabsBase { return; } - // const oldItem = items[oldIndex]; - // if (oldItem) { - // oldItem.unloadView(oldItem.view); - // } + const oldItem = items[oldIndex]; + if (oldItem) { + oldItem.canBeLoaded = false; + oldItem.unloadView(oldItem.content); + } - // const newItem = items[newIndex]; - // if (newItem && this.isLoaded) { - // const selectedView = items[newIndex].view; - // if (selectedView instanceof Frame) { - // selectedView._pushInFrameStackRecursive(); - // } + const newItem = items[newIndex]; + if (newItem && this.isLoaded) { + const selectedView = items[newIndex].content; + if (selectedView instanceof Frame) { + selectedView._pushInFrameStackRecursive(); + } - // newItem.loadView(newItem.view); - // } + newItem.canBeLoaded = true; + newItem.loadView(newItem.content); + } + + const tabStripItems = this.tabStrip && this.tabStrip.items; + if (tabStripItems) { + if (tabStripItems[newIndex]) { + tabStripItems[newIndex]._emit(TabStripItem.selectEvent); + } + + if (tabStripItems[oldIndex]) { + tabStripItems[oldIndex]._emit(TabStripItem.unselectEvent); + } + } this._loadUnloadTabItems(newIndex); @@ -705,46 +631,6 @@ export class Tabs extends TabsBase { } } - // private _actionBarHiddenByTabView: boolean; - // public _handleTwoNavigationBars(backToMoreWillBeVisible: boolean) { - // // TODO - // // if (traceEnabled()) { - // // traceWrite(`TabView._handleTwoNavigationBars(backToMoreWillBeVisible: ${backToMoreWillBeVisible})`, traceCategories.Debug); - // // } - - // // The "< Back" and "< More" navigation bars should not be visible simultaneously. - // const page = this.page || this._selectedView.page || (this)._selectedView.currentPage; - // if (!page || !page.frame) { - // return; - // } - - // let actionBarVisible = page.frame._getNavBarVisible(page); - - // if (backToMoreWillBeVisible && actionBarVisible) { - // page.frame.ios._disableNavBarAnimation = true; - // page.actionBarHidden = true; - // page.frame.ios._disableNavBarAnimation = false; - // this._actionBarHiddenByTabView = true; - // // TODO - // // if (traceEnabled()) { - // // traceWrite(`TabView hid action bar`, traceCategories.Debug); - // // } - // return; - // } - - // if (!backToMoreWillBeVisible && this._actionBarHiddenByTabView) { - // page.frame.ios._disableNavBarAnimation = true; - // page.actionBarHidden = false; - // page.frame.ios._disableNavBarAnimation = false; - // this._actionBarHiddenByTabView = undefined; - // // TODO - // // if (traceEnabled()) { - // // traceWrite(`TabView restored action bar`, traceCategories.Debug); - // // } - // return; - // } - // } - private getViewController(item: TabContentItem): UIViewController { let newController: UIViewController = item.content ? item.content.viewController : null; @@ -789,115 +675,57 @@ export class Tabs extends TabsBase { private setViewControllers(items: TabContentItem[]) { const length = items ? items.length : 0; if (length === 0) { - // this._ios.setViewControllersDirectionAnimatedCompletion(null, null, false, null); + this.viewControllers = null; + return; } const viewControllers = []; + const tabBarItems = []; - items.forEach((item) => { + if (this.tabStrip) { + this.tabStrip.setNativeView(this._ios.tabBar); + } + + const tabStripItems = this.tabStrip && this.tabStrip.items; + if (tabStripItems) { + if (tabStripItems[this.selectedIndex]) { + tabStripItems[this.selectedIndex]._emit(TabStripItem.selectEvent); + } + } + + items.forEach((item, i) => { const controller = this.getViewController(item); + + if (this.tabStrip && this.tabStrip.items && this.tabStrip.items[i]) { + const tabStripItem = this.tabStrip.items[i]; + const tabBarItem = this.createTabBarItem(tabStripItem, i); + updateTitleAndIconPositions(tabStripItem, tabBarItem, controller); + + setViewTextAttributes(this._ios.tabBar, tabStripItem.label, i === this.selectedIndex); + + controller.tabBarItem = tabBarItem; + tabStripItem._index = i; + tabBarItems.push(tabBarItem); + tabStripItem.setNativeView(tabBarItem); + } + + item.canBeLoaded = true; viewControllers.push(controller); }); this.viewControllers = viewControllers; - - // const controllers = NSMutableArray.alloc().initWithCapacity(length); - - // const selectedItem = items[this.selectedIndex]; - // const controller = this.getViewController(selectedItem); - // controllers.addObject(controller); - - // this._ios.setViewControllersDirectionAnimatedCompletion(controllers, UIPageViewControllerNavigationDirection.Forward, false, null); - - iterateIndexRange(this.selectedIndex, 1, this.items.length, (index) => { - (items[index]).canBeLoaded = true; - }); - - // (selectedItem).canBeLoaded = true; - - // const nextItem = items[this.selectedIndex + 1]; - // (nextItem).canBeLoaded = true; - - // const states = getTitleAttributesForStates(this); - - // items.forEach((item, i) => { - // const controller = this.getViewController(item); - - // let icon = null; - // let title = ""; - - // if (this.tabStrip && this.tabStrip.items && this.tabStrip.items[i]) { - // const tabStripItem = this.tabStrip.items[i]; - // icon = this._getIcon(tabStripItem.iconSource); - // title = tabStripItem.title; - - // const tabBarItem = UITabBarItem.alloc().initWithTitleImageTag((title || ""), icon, i); - // updateTitleAndIconPositions(tabStripItem, tabBarItem, controller); - - // applyStatesToItem(tabBarItem, states); - - // controller.tabBarItem = tabBarItem; - // } - - // controllers.addObject(controller); - // (item).canBeLoaded = true; - // }); - - // this._ios.viewControllers = controllers; - // this._ios.customizableViewControllers = null; - - // // When we set this._ios.viewControllers, someone is clearing the moreNavigationController.delegate, so we have to reassign it each time here. - // this._ios.moreNavigationController.delegate = this._moreNavigationControllerDelegate; - } - - private setTabStripItems(items: Array) { - if (!this.tabStrip || !items) { - return; - } - - const tabBarItems = []; - - items.forEach((tabStripItem: TabStripItem, i) => { - tabStripItem._index = i; - const tabBarItem = this.createTabBarItem(tabStripItem, i); - tabBarItems.push(tabBarItem); - tabStripItem.setNativeView(tabBarItem); - }); - this.tabBarItems = tabBarItems; if (this.viewController && this.viewController.tabBar) { this.viewController.tabBar.itemAppearance = this.getTabBarItemAppearance(); - this.viewController.tabBar.items = NSArray.arrayWithArray(tabBarItems); + this.viewController.tabBar.items = NSArray.arrayWithArray(this.tabBarItems); // TODO: investigate why this call is necessary to actually toggle item appearance this.viewController.tabBar.sizeToFit(); - this.tabStrip.setNativeView(this.viewController.tabBar); if (this.selectedIndex) { this.viewController.tabBar.setSelectedItemAnimated(this.tabBarItems[this.selectedIndex], false); } } - - // const length = items ? items.length : 0; - // if (length === 0) { - // this._tabLayout.setItems(null, null); - // 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); - // }); } private createTabBarItem(item: TabStripItem, index: number): UITabBarItem { @@ -924,9 +752,9 @@ export class Tabs extends TabsBase { private getTabBarItemAppearance(): MDCTabBarItemAppearance { let itemAppearance; - if (this.tabStrip._hasImage && this.tabStrip._hasTitle) { + if (this.tabStrip && this.tabStrip._hasImage && this.tabStrip._hasTitle) { itemAppearance = MDCTabBarItemAppearance.TitledImages; - } else if (this.tabStrip._hasImage) { + } else if (this.tabStrip && this.tabStrip._hasImage) { itemAppearance = MDCTabBarItemAppearance.Images; } else { itemAppearance = MDCTabBarItemAppearance.Titles; @@ -941,6 +769,8 @@ export class Tabs extends TabsBase { } private getIcon(tabStripItem: TabStripItem): UIImage { + // Image and Label children of TabStripItem + // take priority over its `iconSource` and `title` properties const iconSource = tabStripItem.image && tabStripItem.image.src; if (!iconSource) { return null; @@ -948,13 +778,15 @@ export class Tabs extends TabsBase { const target = tabStripItem.image; const font = target.style.fontInternal; - const color = tabStripItem.parent.style.color; + const color = target.style.color; const iconTag = [iconSource, font.fontStyle, font.fontWeight, font.fontSize, font.fontFamily, color].join(";"); + let isFontIcon = false; let image: UIImage = this._iconsCache[iconTag]; if (!image) { let is = new ImageSource; if (isFontIconURI(iconSource)) { + isFontIcon = true; const fontIconCode = iconSource.split("//")[1]; is = ImageSource.fromFontIconCodeSync(fontIconCode, font, color); } else { @@ -968,7 +800,11 @@ export class Tabs extends TabsBase { image = this.getFixedSizeIcon(image); } - const originalRenderedImage = image.imageWithRenderingMode(this.getIconRenderingMode()); + let renderingMode: UIImageRenderingMode = UIImageRenderingMode.AlwaysOriginal; + if (!isFontIcon) { + renderingMode = this.getIconRenderingMode(); + } + const originalRenderedImage = image.imageWithRenderingMode(renderingMode); this._iconsCache[iconTag] = originalRenderedImage; image = originalRenderedImage; } else { @@ -997,26 +833,6 @@ export class Tabs extends TabsBase { return resultImage; } - // private _updateIOSTabBarColorsAndFonts(): void { - // if (!this.tabStrip || !this.tabStrip.items || !this.tabStrip.items.length) { - // return; - // } - - // const tabBar = this.ios.tabBar; - // const states = getTitleAttributesForStates(this); - // for (let i = 0; i < tabBar.items.count; i++) { - // applyStatesToItem(tabBar.items[i], states); - // } - // } - - // TODO: Move this to TabStripItem - // [fontInternalProperty.getDefault](): Font { - // return null; - // } - // [fontInternalProperty.setNative](value: Font) { - // this._updateIOSTabBarColorsAndFonts(); - // } - public getTabBarBackgroundColor(): UIColor { return this._ios.tabBar.barTintColor; } @@ -1025,6 +841,69 @@ export class Tabs extends TabsBase { this._ios.tabBar.barTintColor = value instanceof Color ? value.ios : value; } + public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): void { + tabStripItem.nativeView.title = value; + } + + private equalUIColor(first: UIColor, second: UIColor): boolean { + if (!first && !second) { + return true; + } + if (!first || !second) { + return false; + } + const firstComponents = CGColorGetComponents(first.CGColor); + const secondComponents = CGColorGetComponents(second.CGColor); + + return firstComponents[0] === secondComponents[0] + && firstComponents[1] === secondComponents[1] + && firstComponents[2] === secondComponents[2] + && firstComponents[3] === secondComponents[3]; + } + + private isSelectedAndHightlightedItem(tabStripItem: TabStripItem): boolean { + return (tabStripItem._index === this.selectedIndex && tabStripItem["_visualState"] === "highlighted"); + } + + public setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: UIColor | Color): void { + if (!this.tabStrip || !tabStripItem) { + return; + } + + let newColor = value instanceof Color ? value.ios : value; + const itemSelectedAndHighlighted = this.isSelectedAndHightlightedItem(tabStripItem); + + if (!this._defaultItemBackgroundColor && !itemSelectedAndHighlighted) { + this._defaultItemBackgroundColor = newColor; + } + + if (this.viewController.tabBar.alignment !== MDCTabBarAlignment.Justified && itemSelectedAndHighlighted + && !this.equalUIColor(this._defaultItemBackgroundColor, newColor)) { + if (!this._backgroundIndicatorColor) { + this._backgroundIndicatorColor = newColor; + this._ios.tabBar.selectionIndicatorTemplate = new BackgroundIndicatorTemplate(); + this._ios.tabBar.tintColor = newColor; + } + } else { + updateBackgroundPositions(this.tabStrip, tabStripItem, newColor); + } + } + + public setTabBarItemColor(tabStripItem: TabStripItem, value: UIColor | Color): void { + setViewTextAttributes(this._ios.tabBar, tabStripItem.label); + } + + public setTabBarIconColor(tabStripItem: TabStripItem, value: UIColor | Color): void { + const image = this.getIcon(tabStripItem); + + tabStripItem.nativeView.image = image; + tabStripItem.nativeView.selectedImage = image; + } + + public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void { + setViewTextAttributes(this._ios.tabBar, tabStripItem.label); + } + public getTabBarFontInternal(): UIFont { return this._ios.tabBar.unselectedItemTitleFont; } @@ -1039,7 +918,15 @@ export class Tabs extends TabsBase { } public getTabBarTextTransform(): TextTransform { - return null; + switch (this._ios.tabBar.titleTextTransform) { + case MDCTabBarTextTransform.None: + return "none"; + case MDCTabBarTextTransform.Automatic: + return "initial"; + case MDCTabBarTextTransform.Uppercase: + default: + return "uppercase"; + } } public setTabBarTextTransform(value: TextTransform): void { @@ -1047,11 +934,13 @@ export class Tabs extends TabsBase { this._ios.tabBar.titleTextTransform = MDCTabBarTextTransform.None; } else if (value === "uppercase") { this._ios.tabBar.titleTextTransform = MDCTabBarTextTransform.Uppercase; + } else if (value === "initial") { + this._ios.tabBar.titleTextTransform = MDCTabBarTextTransform.Automatic; } } public getTabBarColor(): UIColor { - return this._ios.tabBar.titleColorForState(MDCTabBarItemState.Normal); + return this._ios.tabBar.titleColorForState(MDCTabBarItemState.Normal); } public setTabBarColor(value: UIColor | Color): void { @@ -1134,6 +1023,12 @@ export class Tabs extends TabsBase { return null; } [itemsProperty.setNative](value: TabContentItem[]) { + if (value) { + value.forEach((item: TabContentItem, i) => { + (item).index = i; + }); + } + this.setViewControllers(value); selectedIndexProperty.coerce(this); } @@ -1141,16 +1036,74 @@ export class Tabs extends TabsBase { [tabStripProperty.getDefault](): TabStrip { return null; } + [tabStripProperty.setNative](value: TabStrip) { - this.setTabStripItems(value.items); + this.setViewControllers(this.items); + selectedIndexProperty.coerce(this); } [swipeEnabledProperty.getDefault](): boolean { return true; } + [swipeEnabledProperty.setNative](value: boolean) { if (this.viewController && this.viewController.scrollView) { this.viewController.scrollView.scrollEnabled = value; } } + + [iOSTabBarItemsAlignmentProperty.getDefault](): IOSTabBarItemsAlignment { + if (!this.viewController || !this.viewController.tabBar) { + return "justified"; + } + + let alignment = this.viewController.tabBar.alignment.toString(); + + return (alignment.charAt(0).toLowerCase() + alignment.substring(1)); + } + + [iOSTabBarItemsAlignmentProperty.setNative](value: IOSTabBarItemsAlignment) { + if (!this.viewController || !this.viewController.tabBar) { + return; + } + + let alignment = MDCTabBarAlignment.Justified; + switch (value) { + case "leading": + alignment = MDCTabBarAlignment.Leading; + break; + case "center": + alignment = MDCTabBarAlignment.Center; + break; + case "centerSelected": + alignment = MDCTabBarAlignment.CenterSelected; + break; + } + + this.viewController.tabBar.alignment = alignment; + } } + +function setViewTextAttributes(tabBar: MDCTabBar, view: View, setSelected: boolean = false): any { + if (!view) { + return null; + } + + const defaultTabItemFontSize = 10; + const tabItemFontSize = view.style.fontSize || defaultTabItemFontSize; + const font: UIFont = view.style.fontInternal.getUIFont(UIFont.systemFontOfSize(tabItemFontSize)); + + tabBar.unselectedItemTitleFont = font; + tabBar.selectedItemTitleFont = font; + + const tabItemTextColor = view.style.color; + const textColor = tabItemTextColor instanceof Color ? tabItemTextColor.ios : null; + if (textColor) { + tabBar.setTitleColorForState(textColor, MDCTabBarItemState.Normal); + if (setSelected) { + tabBar.setTitleColorForState(textColor, MDCTabBarItemState.Selected); + } + } + + tabBar.inkColor = UIColor.clearColor; +} \ No newline at end of file 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 4e2cd7eeb..13760a683 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 @@ -19,6 +19,7 @@ package org.nativescript.widgets; import android.content.Context; import android.graphics.Typeface; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.util.SparseArray; @@ -54,6 +55,7 @@ public class BottomNavigationBar extends LinearLayout { private SparseArray mContentDescriptions = new SparseArray(); private final TabStrip mTabStrip; + private int mMaxImageHeight; public BottomNavigationBar(Context context) { this(context, null); @@ -108,6 +110,7 @@ public class BottomNavigationBar extends LinearLayout { public void setItems(TabItemSpec[] items) { mTabStrip.removeAllViews(); mTabItems = items; + setImageHeights(); populateTabStrip(); } @@ -120,25 +123,25 @@ public class BottomNavigationBar extends LinearLayout { TextView textView = (TextView)ll.getChildAt(1); this.setupItem(ll, textView, imgView, tabItem); } - + /** * Gets the TextView for tab item at index */ public TextView getTextViewForItemAt(int index){ LinearLayout ll = this.getViewForItemAt(index); - return (ll != null) ? (TextView)ll.getChildAt(1) : null; + return (ll != null) ? (TextView)ll.getChildAt(1) : null; } - + /** * Gets the LinearLayout container for tab item at index */ public LinearLayout getViewForItemAt(int index){ LinearLayout result = null; - + if(this.mTabStrip.getChildCount() > index){ result = (LinearLayout)this.mTabStrip.getChildAt(index); } - + return result; } @@ -155,39 +158,40 @@ public class BottomNavigationBar extends LinearLayout { protected View createDefaultTabView(Context context, TabItemSpec tabItem) { float density = getResources().getDisplayMetrics().density; - LinearLayout ll = new LinearLayout(context); - ll.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - ll.setGravity(Gravity.CENTER); - ll.setOrientation(LinearLayout.VERTICAL); - TypedValue outValue = new TypedValue(); - getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); - ll.setBackgroundResource(outValue.resourceId); + LinearLayout tabItemLayout = new LinearLayout(context); + tabItemLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + tabItemLayout.setGravity(Gravity.CENTER); + tabItemLayout.setOrientation(LinearLayout.VERTICAL); + TypedValue backgroundOutValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, backgroundOutValue, true); + tabItemLayout.setBackgroundResource(backgroundOutValue.resourceId); - ImageView imgView = new ImageView(context); - imgView.setScaleType(ScaleType.FIT_CENTER); - LinearLayout.LayoutParams imgLP = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - imgLP.gravity = Gravity.CENTER; - imgView.setLayoutParams(imgLP); + ImageView iconImageView = new ImageView(context); + iconImageView.setScaleType(ScaleType.FIT_CENTER); + int iconImageHeight = this.mMaxImageHeight > 0 ? this.mMaxImageHeight : ViewGroup.LayoutParams.WRAP_CONTENT; + int iconImageWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + LinearLayout.LayoutParams iconImageLayoutParams = new LinearLayout.LayoutParams(iconImageWidth, iconImageHeight); + iconImageLayoutParams.gravity = Gravity.CENTER; + iconImageView.setLayoutParams(iconImageLayoutParams); - TextView textView = new TextView(context); - textView.setGravity(Gravity.CENTER); - textView.setMaxWidth((int) (ITEM_TEXT_MAX_WIDTH * density)); - textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, ITEM_TEXT_SIZE_SP); - textView.setTypeface(Typeface.DEFAULT_BOLD); - textView.setEllipsize(TextUtils.TruncateAt.END); - textView.setMaxLines(1); - textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + TextView titleTextView = new TextView(context); + titleTextView.setGravity(Gravity.CENTER); + titleTextView.setMaxWidth((int) (ITEM_TEXT_MAX_WIDTH * density)); + titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, ITEM_TEXT_SIZE_SP); + titleTextView.setTypeface(Typeface.DEFAULT_BOLD); + titleTextView.setEllipsize(TextUtils.TruncateAt.END); + titleTextView.setMaxLines(1); + titleTextView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - this.setupItem(ll, textView, imgView, tabItem); + this.setupItem(tabItemLayout, titleTextView, iconImageView, tabItem); - ll.addView(imgView); - ll.addView(textView); - return ll; + tabItemLayout.addView(iconImageView); + tabItemLayout.addView(titleTextView); + return tabItemLayout; } - + private void setupItem(LinearLayout ll, TextView textView,ImageView imgView, TabItemSpec tabItem){ float density = getResources().getDisplayMetrics().density; - if (tabItem.iconId != 0) { imgView.setImageResource(tabItem.iconId); imgView.setVisibility(VISIBLE); @@ -205,14 +209,14 @@ public class BottomNavigationBar extends LinearLayout { if (tabItem.typeFace != null) { textView.setTypeface(tabItem.typeFace); } - + if (tabItem.fontSize != 0) { textView.setTextSize(tabItem.fontSize); } - + if (tabItem.color != 0) { textView.setTextColor(tabItem.color); - mTabStrip.setShouldUpdateTabsTextColor(false); + mTabStrip.setShouldUpdateTabsTextColor(false); } } else { textView.setVisibility(GONE); @@ -238,17 +242,29 @@ public class BottomNavigationBar extends LinearLayout { // to be overridden in JS } + private void setImageHeights(){ + if (this.mTabItems != null) { + for (TabItemSpec tabItem : this.mTabItems) { + if(tabItem.imageHeight == 0 && tabItem.iconId != 0) { + Drawable drawable = getResources().getDrawable(tabItem.iconId); + tabItem.imageHeight = drawable.getIntrinsicHeight(); + } + if(tabItem.imageHeight > this.mMaxImageHeight) { + this.mMaxImageHeight = tabItem.imageHeight; + } + } + } + } + private void populateTabStrip() { final OnClickListener tabClickListener = new TabClickListener(); if (this.mTabItems != null) { int count = this.mTabItems.length < 5 ? this.mTabItems.length : 5; for (int i = 0; i < count; i++) { - View tabView = null; - TabItemSpec tabItem; tabItem = this.mTabItems[i]; - tabView = createDefaultTabView(getContext(), tabItem); + View tabView = createDefaultTabView(getContext(), tabItem); tabView.setOnClickListener(tabClickListener); String desc = mContentDescriptions.get(i, null); diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java index 1494e407f..43299710d 100644 --- a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java @@ -9,6 +9,7 @@ public class TabItemSpec { public Typeface typeFace; public int iconId; public Drawable iconDrawable; + public int imageHeight; public int backgroundColor; public int color; } \ No newline at end of file diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index 264abe4b1..35da58adb 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -468,6 +468,7 @@ typeFace: android.graphics.Typeface; iconId: number; iconDrawable: android.graphics.drawable.Drawable; + imageHeight: number; backgroundColor: number; color: number; }