mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: Scoped Packages (#7911)
* chore: move tns-core-modules to nativescript-core * chore: preparing compat generate script * chore: add missing definitions * chore: no need for http-request to be private * chore: packages chore * test: generate tests for tns-core-modules * chore: add anroid module for consistency * chore: add .npmignore * chore: added privateModulesWhitelist * chore(webpack): added bundle-entry-points * chore: scripts * chore: tests changed to use @ns/core * test: add scoped-packages test project * test: fix types * test: update test project * chore: build scripts * chore: update build script * chore: npm scripts cleanup * chore: make the compat pgk work with old wp config * test: generate diff friendly tests * chore: create barrel exports * chore: move files after rebase * chore: typedoc config * chore: compat mode * chore: review of barrels * chore: remove tns-core-modules import after rebase * chore: dev workflow setup * chore: update developer-workflow * docs: experiment with API extractor * chore: api-extractor and barrel exports * chore: api-extractor configs * chore: generate d.ts rollup with api-extractor * refactor: move methods inside Frame * chore: fic tests to use Frame static methods * refactor: create Builder class * refactor: use Builder class in tests * refactor: include Style in ui barrel * chore: separate compat build script * chore: fix tslint errors * chore: update NATIVESCRIPT_CORE_ARGS * chore: fix compat pack * chore: fix ui-test-app build with linked modules * chore: Application, ApplicationSettings, Connectivity and Http * chore: export Trace, Profiling and Utils * refactor: Static create methods for ImageSource * chore: fix deprecated usages of ImageSource * chore: move Span and FormattedString to ui * chore: add events-args and ImageSource to index files * chore: check for CLI >= 6.2 when building for IOS * chore: update travis build * chore: copy Pod file to compat package * chore: update error msg ui-tests-app * refactor: Apply suggestions from code review Co-Authored-By: Martin Yankov <m.i.yankov@gmail.com> * chore: typings and refs * chore: add missing d.ts files for public API * chore: adress code review FB * chore: update api-report * chore: dev-workflow for other apps * chore: api update * chore: update api-report
This commit is contained in:
committed by
GitHub
parent
6c7139477e
commit
cc97a16800
@@ -0,0 +1,766 @@
|
||||
// Types
|
||||
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 { 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";
|
||||
|
||||
// TODO: Impl trace
|
||||
// import { isEnabled as traceEnabled, write as traceWrite } from "../../../trace";
|
||||
|
||||
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";
|
||||
|
||||
const PRIMARY_COLOR = "colorPrimary";
|
||||
const DEFAULT_ELEVATION = 8;
|
||||
|
||||
const TABID = "_tabId";
|
||||
const INDEX = "_index";
|
||||
const ownerSymbol = Symbol("_owner");
|
||||
|
||||
let TabFragment: any;
|
||||
let BottomNavigationBar: any;
|
||||
let AttachStateChangeListener: any;
|
||||
let appResources: android.content.res.Resources;
|
||||
|
||||
function makeFragmentName(viewId: number, id: number): string {
|
||||
return "android:bottomnavigation:" + viewId + ":" + id;
|
||||
}
|
||||
|
||||
function getTabById(id: number): BottomNavigation {
|
||||
const ref = tabs.find(ref => {
|
||||
const tab = ref.get();
|
||||
|
||||
return tab && tab._domId === id;
|
||||
});
|
||||
|
||||
return ref && ref.get();
|
||||
}
|
||||
|
||||
function initializeNativeClasses() {
|
||||
if (BottomNavigationBar) {
|
||||
return;
|
||||
}
|
||||
|
||||
class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase {
|
||||
private owner: BottomNavigation;
|
||||
private index: number;
|
||||
private backgroundBitmap: android.graphics.Bitmap = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
return global.__native(this);
|
||||
}
|
||||
|
||||
static newInstance(tabId: number, index: number): TabFragmentImplementation {
|
||||
const args = new android.os.Bundle();
|
||||
args.putInt(TABID, tabId);
|
||||
args.putInt(INDEX, index);
|
||||
const fragment = new TabFragmentImplementation();
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public onCreate(savedInstanceState: android.os.Bundle): void {
|
||||
super.onCreate(savedInstanceState);
|
||||
const args = this.getArguments();
|
||||
this.owner = getTabById(args.getInt(TABID));
|
||||
this.index = args.getInt(INDEX);
|
||||
if (!this.owner) {
|
||||
throw new Error(`Cannot find BottomNavigation`);
|
||||
}
|
||||
}
|
||||
|
||||
public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View {
|
||||
const tabItem = this.owner.items[this.index];
|
||||
|
||||
return tabItem.nativeViewProtected;
|
||||
}
|
||||
|
||||
public onDestroyView() {
|
||||
const hasRemovingParent = this.getRemovingParentFragment();
|
||||
|
||||
// Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments.
|
||||
// TODO: Consider removing it when update to androidx.fragment:1.2.0
|
||||
if (hasRemovingParent && this.owner.selectedIndex === this.index) {
|
||||
const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(appResources, this.backgroundBitmap);
|
||||
this.owner._originalBackground = this.owner.backgroundColor || new Color("White");
|
||||
this.owner.nativeViewProtected.setBackgroundDrawable(bitmapDrawable);
|
||||
this.backgroundBitmap = null;
|
||||
}
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public onPause(): void {
|
||||
const hasRemovingParent = this.getRemovingParentFragment();
|
||||
|
||||
// Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments.
|
||||
// TODO: Consider removing it when update to androidx.fragment:1.2.0
|
||||
if (hasRemovingParent && this.owner.selectedIndex === this.index) {
|
||||
this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected);
|
||||
}
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap {
|
||||
// Another way to get view bitmap. Test performance vs setDrawingCacheEnabled
|
||||
// const width = view.getWidth();
|
||||
// const height = view.getHeight();
|
||||
// const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888);
|
||||
// const canvas = new android.graphics.Canvas(bitmap);
|
||||
// view.layout(0, 0, width, height);
|
||||
// view.draw(canvas);
|
||||
|
||||
view.setDrawingCacheEnabled(true);
|
||||
const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache());
|
||||
view.setDrawingCacheEnabled(false);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavigationBarImplementation extends org.nativescript.widgets.BottomNavigationBar {
|
||||
|
||||
constructor(context: android.content.Context, public owner: BottomNavigation) {
|
||||
super(context);
|
||||
|
||||
return global.__native(this);
|
||||
}
|
||||
|
||||
public onSelectedPositionChange(position: number, prevPosition: number): void {
|
||||
const owner = this.owner;
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
owner.changeTab(position);
|
||||
|
||||
const tabStripItems = owner.tabStrip && owner.tabStrip.items;
|
||||
|
||||
if (position >= 0 && tabStripItems && tabStripItems[position]) {
|
||||
tabStripItems[position]._emit(TabStripItem.selectEvent);
|
||||
}
|
||||
|
||||
if (prevPosition >= 0 && tabStripItems && tabStripItems[prevPosition]) {
|
||||
tabStripItems[prevPosition]._emit(TabStripItem.unselectEvent);
|
||||
}
|
||||
|
||||
owner.selectedIndex = position;
|
||||
}
|
||||
|
||||
public onTap(position: number): boolean {
|
||||
const owner = this.owner;
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tabStrip = owner.tabStrip;
|
||||
const tabStripItems = tabStrip && tabStrip.items;
|
||||
|
||||
if (position >= 0 && tabStripItems[position]) {
|
||||
tabStripItems[position]._emit(TabStripItem.tapEvent);
|
||||
tabStrip.notify({ eventName: TabStrip.itemTapEvent, object: tabStrip, index: position });
|
||||
}
|
||||
|
||||
if (!owner.items[position]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Interfaces([android.view.View.OnAttachStateChangeListener])
|
||||
class AttachListener extends java.lang.Object implements android.view.View.OnAttachStateChangeListener {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
return global.__native(this);
|
||||
}
|
||||
|
||||
onViewAttachedToWindow(view: android.view.View): void {
|
||||
const owner: View = view[ownerSymbol];
|
||||
if (owner) {
|
||||
owner._onAttachedToWindow();
|
||||
}
|
||||
}
|
||||
|
||||
onViewDetachedFromWindow(view: android.view.View): void {
|
||||
const owner: View = view[ownerSymbol];
|
||||
if (owner) {
|
||||
owner._onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TabFragment = TabFragmentImplementation;
|
||||
BottomNavigationBar = BottomNavigationBarImplementation;
|
||||
AttachStateChangeListener = new AttachListener();
|
||||
appResources = application.android.context.getResources();
|
||||
}
|
||||
|
||||
function setElevation(bottomNavigationBar: org.nativescript.widgets.BottomNavigationBar) {
|
||||
const compat = <any>androidx.core.view.ViewCompat;
|
||||
if (compat.setElevation) {
|
||||
const val = DEFAULT_ELEVATION * layout.getDisplayDensity();
|
||||
compat.setElevation(bottomNavigationBar, val);
|
||||
}
|
||||
}
|
||||
|
||||
export const tabs = new Array<WeakRef<BottomNavigation>>();
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
tabs.push(new WeakRef(this));
|
||||
}
|
||||
|
||||
get _hasFragments(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onItemsChanged(oldItems: TabContentItem[], newItems: TabContentItem[]): void {
|
||||
super.onItemsChanged(oldItems, newItems);
|
||||
|
||||
if (oldItems) {
|
||||
oldItems.forEach((item: TabContentItem, i, arr) => {
|
||||
(<any>item).index = 0;
|
||||
(<any>item).tabItemSpec = null;
|
||||
item.setNativeView(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
nativeView.addRow(new org.nativescript.widgets.ItemSpec(1, org.nativescript.widgets.GridUnitType.star));
|
||||
nativeView.addRow(new org.nativescript.widgets.ItemSpec(1, org.nativescript.widgets.GridUnitType.auto));
|
||||
|
||||
// CONTENT VIEW
|
||||
const contentView = new org.nativescript.widgets.ContentLayout(this._context);
|
||||
const contentViewLayoutParams = new org.nativescript.widgets.CommonLayoutParams();
|
||||
contentViewLayoutParams.row = 0;
|
||||
contentView.setLayoutParams(contentViewLayoutParams);
|
||||
nativeView.addView(contentView);
|
||||
(<any>nativeView).contentView = contentView;
|
||||
|
||||
// TABSTRIP
|
||||
const bottomNavigationBar = new BottomNavigationBar(context, this);
|
||||
const bottomNavigationBarLayoutParams = new org.nativescript.widgets.CommonLayoutParams();
|
||||
bottomNavigationBarLayoutParams.row = 1;
|
||||
bottomNavigationBar.setLayoutParams(bottomNavigationBarLayoutParams);
|
||||
nativeView.addView(bottomNavigationBar);
|
||||
(<any>nativeView).bottomNavigationBar = bottomNavigationBar;
|
||||
|
||||
setElevation(bottomNavigationBar);
|
||||
|
||||
const primaryColor = ad.resources.getPaletteColor(PRIMARY_COLOR, context);
|
||||
if (primaryColor) {
|
||||
bottomNavigationBar.setBackgroundColor(primaryColor);
|
||||
}
|
||||
|
||||
return nativeView;
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
super.initNativeView();
|
||||
|
||||
if (this._contentViewId < 0) {
|
||||
this._contentViewId = android.view.View.generateViewId();
|
||||
}
|
||||
|
||||
const nativeView: any = this.nativeViewProtected;
|
||||
|
||||
nativeView.addOnAttachStateChangeListener(AttachStateChangeListener);
|
||||
nativeView[ownerSymbol] = this;
|
||||
|
||||
this._contentView = (<any>nativeView).contentView;
|
||||
this._contentView.setId(this._contentViewId);
|
||||
|
||||
this._bottomNavigationBar = (<any>nativeView).bottomNavigationBar;
|
||||
(<any>this._bottomNavigationBar).owner = this;
|
||||
|
||||
if (this.tabStrip) {
|
||||
this.tabStrip.setNativeView(this._bottomNavigationBar);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (this._attachedToWindow) {
|
||||
this.changeTab(this.selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
_onAttachedToWindow(): void {
|
||||
super._onAttachedToWindow();
|
||||
this._attachedToWindow = true;
|
||||
|
||||
// _onAttachedToWindow called from OS again after it was detach
|
||||
// TODO: Consider testing and removing it when update to androidx.fragment:1.2.0
|
||||
if (this._manager && this._manager.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeTab(this.selectedIndex);
|
||||
}
|
||||
|
||||
_onDetachedFromWindow(): void {
|
||||
super._onDetachedFromWindow();
|
||||
|
||||
this._attachedToWindow = false;
|
||||
}
|
||||
|
||||
public onUnloaded(): void {
|
||||
super.onUnloaded();
|
||||
|
||||
if (this.tabStrip) {
|
||||
this.setTabStripItems(null);
|
||||
}
|
||||
|
||||
const fragmentToDetach = this._currentFragment;
|
||||
if (fragmentToDetach) {
|
||||
this.destroyItem((<any>fragmentToDetach).index, fragmentToDetach);
|
||||
this.commitCurrentTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public disposeNativeView() {
|
||||
this._bottomNavigationBar.setItems(null);
|
||||
this._bottomNavigationBar = null;
|
||||
|
||||
this.nativeViewProtected.removeOnAttachStateChangeListener(AttachStateChangeListener);
|
||||
this.nativeViewProtected[ownerSymbol] = 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.disposeTabFragments();
|
||||
}
|
||||
|
||||
private disposeTabFragments(): void {
|
||||
const fragmentManager = this._getFragmentManager();
|
||||
const transaction = fragmentManager.beginTransaction();
|
||||
for (let fragment of (<Array<any>>fragmentManager.getFragments().toArray())) {
|
||||
transaction.remove(fragment);
|
||||
}
|
||||
|
||||
transaction.commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
private get currentTransaction(): androidx.fragment.app.FragmentTransaction {
|
||||
if (!this._currentTransaction) {
|
||||
const fragmentManager = this._getFragmentManager();
|
||||
this._currentTransaction = fragmentManager.beginTransaction();
|
||||
}
|
||||
|
||||
return this._currentTransaction;
|
||||
}
|
||||
|
||||
private commitCurrentTransaction(): void {
|
||||
if (this._currentTransaction) {
|
||||
this._currentTransaction.commitNowAllowingStateLoss();
|
||||
this._currentTransaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should we extract adapter-like class?
|
||||
// TODO: Rename this?
|
||||
public changeTab(index: number) {
|
||||
// this is the case when there are no items
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragmentToDetach = this._currentFragment;
|
||||
if (fragmentToDetach) {
|
||||
this.destroyItem((<any>fragmentToDetach).index, fragmentToDetach);
|
||||
}
|
||||
|
||||
const fragment = this.instantiateItem(this._contentView, index);
|
||||
this.setPrimaryItem(index, fragment);
|
||||
|
||||
this.commitCurrentTransaction();
|
||||
}
|
||||
|
||||
private instantiateItem(container: android.view.ViewGroup, position: number): androidx.fragment.app.Fragment {
|
||||
const name = makeFragmentName(container.getId(), position);
|
||||
|
||||
const fragmentManager = this._getFragmentManager();
|
||||
let fragment: androidx.fragment.app.Fragment = fragmentManager.findFragmentByTag(name);
|
||||
if (fragment != null) {
|
||||
this.currentTransaction.attach(fragment);
|
||||
} else {
|
||||
fragment = TabFragment.newInstance(this._domId, position);
|
||||
this.currentTransaction.add(container.getId(), fragment, name);
|
||||
}
|
||||
|
||||
if (fragment !== this._currentFragment) {
|
||||
fragment.setMenuVisibility(false);
|
||||
fragment.setUserVisibleHint(false);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private setPrimaryItem(position: number, fragment: androidx.fragment.app.Fragment): void {
|
||||
if (fragment !== this._currentFragment) {
|
||||
if (this._currentFragment != null) {
|
||||
this._currentFragment.setMenuVisibility(false);
|
||||
this._currentFragment.setUserVisibleHint(false);
|
||||
}
|
||||
|
||||
if (fragment != null) {
|
||||
fragment.setMenuVisibility(true);
|
||||
fragment.setUserVisibleHint(true);
|
||||
}
|
||||
|
||||
this._currentFragment = fragment;
|
||||
|
||||
const tabItems = this.items;
|
||||
const tabItem = tabItems ? tabItems[position] : null;
|
||||
if (tabItem) {
|
||||
tabItem.canBeLoaded = true;
|
||||
this._loadUnloadTabItems(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private destroyItem(position: number, fragment: androidx.fragment.app.Fragment): void {
|
||||
if (fragment) {
|
||||
this.currentTransaction.detach(fragment);
|
||||
if (this._currentFragment === fragment) {
|
||||
this._currentFragment = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.items && this.items[position]) {
|
||||
this.items[position].canBeLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
private setTabStripItems(items: Array<TabStripItem>) {
|
||||
if (!this.tabStrip || !items) {
|
||||
this._bottomNavigationBar.setItems(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tabItems = new Array<org.nativescript.widgets.TabItemSpec>();
|
||||
items.forEach((item, i, arr) => {
|
||||
(<any>item).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);
|
||||
});
|
||||
}
|
||||
|
||||
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 = titleLabel.style.textTransform;
|
||||
if (textTransform) {
|
||||
title = getTransformedText(title, textTransform);
|
||||
}
|
||||
tabItemSpec.title = title;
|
||||
|
||||
// BACKGROUND-COLOR
|
||||
const backgroundColor = tabStripItem.style.backgroundColor;
|
||||
if (backgroundColor) {
|
||||
tabItemSpec.backgroundColor = backgroundColor.android;
|
||||
}
|
||||
|
||||
// COLOR
|
||||
const color = titleLabel.style.color;
|
||||
if (color) {
|
||||
tabItemSpec.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 icon = this.getIcon(tabStripItem);
|
||||
|
||||
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): android.graphics.drawable.BitmapDrawable {
|
||||
const iconSource = tabStripItem.image && tabStripItem.image.src;
|
||||
|
||||
let is: ImageSource;
|
||||
if (isFontIconURI(iconSource)) {
|
||||
const fontIconCode = iconSource.split("//")[1];
|
||||
const target = tabStripItem.image ? tabStripItem.image : tabStripItem;
|
||||
const font = target.style.fontInternal;
|
||||
const 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(application.android.context.getResources(), 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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): 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 setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: android.graphics.drawable.Drawable | Color): 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 setTabBarItemColor(tabStripItem: TabStripItem, value: number | Color): void {
|
||||
if (typeof value === "number") {
|
||||
tabStripItem.nativeViewProtected.setTextColor(value);
|
||||
} else {
|
||||
tabStripItem.nativeViewProtected.setTextColor(value.android);
|
||||
}
|
||||
}
|
||||
|
||||
public setTabBarIconColor(tabStripItem: TabStripItem, value: number | Color): void {
|
||||
const index = (<any>tabStripItem).index;
|
||||
const tabBarItem = this._bottomNavigationBar.getViewForItemAt(index);
|
||||
const imgView = <android.widget.ImageView>tabBarItem.getChildAt(0);
|
||||
const drawable = this.getIcon(tabStripItem);
|
||||
|
||||
imgView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font): void {
|
||||
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);
|
||||
}
|
||||
|
||||
[selectedIndexProperty.setNative](value: number) {
|
||||
// const smoothScroll = false;
|
||||
|
||||
// if (traceEnabled()) {
|
||||
// traceWrite("TabView this._viewPager.setCurrentItem(" + value + ", " + smoothScroll + ");", traceCategory);
|
||||
// }
|
||||
|
||||
if (this.tabStrip) {
|
||||
this._bottomNavigationBar.setSelectedPosition(value);
|
||||
} else {
|
||||
this.changeTab(value);
|
||||
}
|
||||
}
|
||||
|
||||
[itemsProperty.getDefault](): TabContentItem[] {
|
||||
return null;
|
||||
}
|
||||
[itemsProperty.setNative](value: TabContentItem[]) {
|
||||
if (value) {
|
||||
value.forEach((item: TabContentItem, i) => {
|
||||
(<any>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;
|
||||
}
|
||||
81
nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts
vendored
Normal file
81
nativescript-core/ui/bottom-navigation/bottom-navigation.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Contains the BottomNavigation class, which represents a tab navigation widget with static tabs at the bottom.
|
||||
* @module "ui/tab-navigation/bottom-navigation"
|
||||
*/ /** */
|
||||
|
||||
import { TabNavigationBase } from "../tab-navigation-base/tab-navigation-base";
|
||||
import { TabContentItem } from "../tab-navigation-base/tab-content-item";
|
||||
import { TabStrip } from "../tab-navigation-base/tab-strip";
|
||||
import { Property, CoercibleProperty, EventData } from "../core/view";
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* Defines the data for the TabView.selectedIndexChanged event.
|
||||
*/
|
||||
export interface SelectedIndexChangedEventData extends EventData {
|
||||
/**
|
||||
* The old selected index.
|
||||
*/
|
||||
oldIndex: number;
|
||||
|
||||
/**
|
||||
* The new selected index.
|
||||
*/
|
||||
newIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tab navigation widget with static tabs at the bottom.
|
||||
*/
|
||||
export class BottomNavigation extends TabNavigationBase {
|
||||
/**
|
||||
* Gets or sets the items of the BottomNavigation.
|
||||
*/
|
||||
items: Array<TabContentItem>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
export const itemsProperty: Property<BottomNavigation, TabContentItem[]>;
|
||||
export const tabStripProperty: Property<BottomNavigation, TabStrip>
|
||||
export const selectedIndexProperty: CoercibleProperty<BottomNavigation, number>;
|
||||
716
nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts
Normal file
716
nativescript-core/ui/bottom-navigation/bottom-navigation.ios.ts
Normal file
@@ -0,0 +1,716 @@
|
||||
// Types
|
||||
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 { Color } from "../../color";
|
||||
import { ImageSource } from "../../image-source";
|
||||
import { device } from "../../platform";
|
||||
import { ios as iosUtils, isFontIconURI, layout } from "../../utils/utils";
|
||||
import { CSSType, ios as iosView, View } from "../core/view";
|
||||
import { Frame } 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";
|
||||
|
||||
// TODO:
|
||||
// import { profile } from "../../profiling";
|
||||
|
||||
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";
|
||||
|
||||
const maxTabsCount = 5;
|
||||
const majorVersion = iosUtils.MajorVersion;
|
||||
const isPhone = device.deviceType === "Phone";
|
||||
|
||||
class UITabBarControllerImpl extends UITabBarController {
|
||||
|
||||
private _owner: WeakRef<BottomNavigation>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<BottomNavigation>): UITabBarControllerImpl {
|
||||
let handler = <UITabBarControllerImpl>UITabBarControllerImpl.new();
|
||||
handler._owner = owner;
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// @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();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// @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:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mind implementation for other controllers
|
||||
public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void {
|
||||
super.traitCollectionDidChange(previousTraitCollection);
|
||||
|
||||
if (majorVersion >= 13) {
|
||||
const owner = this._owner.get();
|
||||
if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) {
|
||||
owner.notify({ eventName: iosView.traitCollectionColorAppearanceChangedEvent, object: owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UITabBarControllerDelegateImpl extends NSObject implements UITabBarControllerDelegate {
|
||||
public static ObjCProtocols = [UITabBarControllerDelegate];
|
||||
|
||||
private _owner: WeakRef<BottomNavigation>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<BottomNavigation>): UITabBarControllerDelegateImpl {
|
||||
let delegate = <UITabBarControllerDelegateImpl>UITabBarControllerDelegateImpl.new();
|
||||
delegate._owner = owner;
|
||||
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public tabBarControllerShouldSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): boolean {
|
||||
// TODO
|
||||
// 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.viewControllers) {
|
||||
const position = tabBarController.viewControllers.indexOfObject(viewController);
|
||||
if (position !== NSNotFound) {
|
||||
const tabStrip = owner.tabStrip;
|
||||
const tabStripItems = tabStrip && tabStrip.items;
|
||||
|
||||
if (tabStripItems && tabStripItems[position]) {
|
||||
tabStripItems[position]._emit(TabStripItem.tapEvent);
|
||||
tabStrip.notify({ eventName: TabStrip.itemTapEvent, object: tabStrip, index: position });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((<any>tabBarController).selectedViewController === viewController) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(<any>tabBarController)._willSelectViewController = viewController;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public tabBarControllerDidSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): void {
|
||||
// TODO
|
||||
// if (traceEnabled()) {
|
||||
// traceWrite("TabView.delegate.DID_select(" + tabBarController + ", " + viewController + ");", traceCategories.Debug);
|
||||
// }
|
||||
|
||||
const owner = this._owner.get();
|
||||
if (owner) {
|
||||
if (tabBarController.viewControllers) {
|
||||
const position = tabBarController.viewControllers.indexOfObject(viewController);
|
||||
if (position !== NSNotFound) {
|
||||
const prevPosition = owner.selectedIndex;
|
||||
const tabStripItems = owner.tabStrip && owner.tabStrip.items;
|
||||
if (tabStripItems) {
|
||||
if (tabStripItems[position]) {
|
||||
tabStripItems[position]._emit(TabStripItem.selectEvent);
|
||||
}
|
||||
|
||||
if (tabStripItems[prevPosition]) {
|
||||
tabStripItems[prevPosition]._emit(TabStripItem.unselectEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner._onViewControllerShown(viewController);
|
||||
}
|
||||
|
||||
(<any>tabBarController)._willSelectViewController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class UINavigationControllerDelegateImpl extends NSObject implements UINavigationControllerDelegate {
|
||||
public static ObjCProtocols = [UINavigationControllerDelegate];
|
||||
|
||||
private _owner: WeakRef<BottomNavigation>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<BottomNavigation>): UINavigationControllerDelegateImpl {
|
||||
let delegate = <UINavigationControllerDelegateImpl>UINavigationControllerDelegateImpl.new();
|
||||
delegate._owner = owner;
|
||||
|
||||
return delegate;
|
||||
}
|
||||
|
||||
navigationControllerWillShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void {
|
||||
// TODO
|
||||
// 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 {
|
||||
// TODO
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CSSType("BottomNavigation")
|
||||
export class BottomNavigation extends TabNavigationBase {
|
||||
public viewController: UITabBarControllerImpl;
|
||||
public items: TabContentItem[];
|
||||
public _ios: UITabBarControllerImpl;
|
||||
private _delegate: UITabBarControllerDelegateImpl;
|
||||
private _moreNavigationControllerDelegate: UINavigationControllerDelegateImpl;
|
||||
private _iconsCache = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.viewController = this._ios = UITabBarControllerImpl.initWithOwner(new WeakRef(this));
|
||||
this.nativeViewProtected = this._ios.view;
|
||||
}
|
||||
|
||||
initNativeView() {
|
||||
super.initNativeView();
|
||||
this._delegate = UITabBarControllerDelegateImpl.initWithOwner(new WeakRef(this));
|
||||
this._moreNavigationControllerDelegate = UINavigationControllerDelegateImpl.initWithOwner(new WeakRef(this));
|
||||
if (!this.tabStrip) {
|
||||
this.viewController.tabBar.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
disposeNativeView() {
|
||||
this._delegate = null;
|
||||
this._moreNavigationControllerDelegate = null;
|
||||
super.disposeNativeView();
|
||||
}
|
||||
|
||||
// TODO
|
||||
// @profile
|
||||
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();
|
||||
}
|
||||
|
||||
this._ios.delegate = this._delegate;
|
||||
}
|
||||
|
||||
public onUnloaded() {
|
||||
this._ios.delegate = null;
|
||||
this._ios.moreNavigationController.delegate = null;
|
||||
super.onUnloaded();
|
||||
}
|
||||
|
||||
get ios(): UITabBarController {
|
||||
return this._ios;
|
||||
}
|
||||
|
||||
public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
|
||||
//
|
||||
}
|
||||
|
||||
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
|
||||
//
|
||||
}
|
||||
|
||||
public onSelectedIndexChanged(oldIndex: number, newIndex: number): void {
|
||||
const items = this.items;
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
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].content;
|
||||
if (selectedView instanceof Frame) {
|
||||
selectedView._pushInFrameStackRecursive();
|
||||
}
|
||||
|
||||
newItem.canBeLoaded = true;
|
||||
newItem.loadView(newItem.content);
|
||||
}
|
||||
|
||||
super.onSelectedIndexChanged(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
public getTabBarBackgroundColor(): UIColor {
|
||||
return this._ios.tabBar.barTintColor;
|
||||
}
|
||||
|
||||
public setTabBarBackgroundColor(value: UIColor | Color): void {
|
||||
this._ios.tabBar.barTintColor = value instanceof Color ? value.ios : value;
|
||||
}
|
||||
|
||||
public setTabBarItemTitle(tabStripItem: TabStripItem, value: string): void {
|
||||
tabStripItem.nativeView.title = value;
|
||||
}
|
||||
|
||||
public setTabBarItemBackgroundColor(tabStripItem: TabStripItem, value: UIColor | Color): void {
|
||||
if (!this.tabStrip) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bgView = (<any>tabStripItem).bgView;
|
||||
if (!bgView) {
|
||||
const index = (<any>tabStripItem).index;
|
||||
const width = this.tabStrip.nativeView.frame.size.width / this.tabStrip.items.length;
|
||||
const frame = CGRectMake(width * index, 0, width, this.tabStrip.nativeView.frame.size.width);
|
||||
bgView = UIView.alloc().initWithFrame(frame);
|
||||
this.tabStrip.nativeView.insertSubviewAtIndex(bgView, 0);
|
||||
(<any>tabStripItem).bgView = bgView;
|
||||
}
|
||||
|
||||
bgView.backgroundColor = value instanceof Color ? value.ios : value;
|
||||
}
|
||||
|
||||
public setTabBarItemColor(tabStripItem: TabStripItem, value: UIColor | Color): void {
|
||||
const states = getTitleAttributesForStates(tabStripItem.label);
|
||||
applyStatesToItem(tabStripItem.nativeView, states);
|
||||
}
|
||||
|
||||
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 {
|
||||
const states = getTitleAttributesForStates(tabStripItem.label);
|
||||
applyStatesToItem(tabStripItem.nativeView, states);
|
||||
}
|
||||
|
||||
public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform): void {
|
||||
const title = getTransformedText(tabStripItem.label.text, value);
|
||||
tabStripItem.nativeView.title = title;
|
||||
}
|
||||
|
||||
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
||||
const width = layout.getMeasureSpecSize(widthMeasureSpec);
|
||||
const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
|
||||
|
||||
const height = layout.getMeasureSpecSize(heightMeasureSpec);
|
||||
const heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
|
||||
|
||||
const widthAndState = View.resolveSizeAndState(width, width, widthMode, 0);
|
||||
const heightAndState = View.resolveSizeAndState(height, height, heightMode, 0);
|
||||
|
||||
this.setMeasuredDimension(widthAndState, heightAndState);
|
||||
}
|
||||
|
||||
public _onViewControllerShown(viewController: UIViewController) {
|
||||
// This method could be called with the moreNavigationController or its list controller, so we have to check.
|
||||
// TODO
|
||||
// if (traceEnabled()) {
|
||||
// traceWrite("TabView._onViewControllerShown(" + viewController + ");", traceCategories.Debug);
|
||||
// }
|
||||
if (this._ios.viewControllers && this._ios.viewControllers.containsObject(viewController)) {
|
||||
this.selectedIndex = this._ios.viewControllers.indexOfObject(viewController);
|
||||
} else {
|
||||
// TODO
|
||||
// if (traceEnabled()) {
|
||||
// traceWrite("TabView._onViewControllerShown: viewController is not one of our viewControllers", traceCategories.Debug);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
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 || (<any>this)._selectedView.page || (<any>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;
|
||||
|
||||
if (newController) {
|
||||
(<any>item).setViewController(newController, newController.view);
|
||||
|
||||
return newController;
|
||||
}
|
||||
|
||||
if (item.content.ios instanceof UIViewController) {
|
||||
newController = item.content.ios;
|
||||
(<any>item).setViewController(newController, newController.view);
|
||||
} else if (item.content.ios && item.content.ios.controller instanceof UIViewController) {
|
||||
newController = item.content.ios.controller;
|
||||
(<any>item).setViewController(newController, newController.view);
|
||||
} else {
|
||||
newController = iosView.UILayoutViewController.initWithOwner(new WeakRef(item.content)) as UIViewController;
|
||||
newController.view.addSubview(item.content.nativeViewProtected);
|
||||
item.content.viewController = newController;
|
||||
(<any>item).setViewController(newController, item.content.nativeViewProtected);
|
||||
}
|
||||
|
||||
return newController;
|
||||
}
|
||||
|
||||
private setViewControllers(items: TabContentItem[]) {
|
||||
const length = items ? items.length : 0;
|
||||
if (length === 0) {
|
||||
this._ios.viewControllers = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit both tabContentItems and tabStripItems to 5 in order to prevent iOS 'more' button
|
||||
items = items.slice(0, maxTabsCount);
|
||||
|
||||
const controllers = NSMutableArray.alloc<UIViewController>().initWithCapacity(length);
|
||||
|
||||
if (this.tabStrip) {
|
||||
this.tabStrip.setNativeView(this._ios.tabBar);
|
||||
}
|
||||
|
||||
items.forEach((item, i) => {
|
||||
const controller = this.getViewController(item);
|
||||
|
||||
if (this.tabStrip && this.tabStrip.items && this.tabStrip.items[i]) {
|
||||
const tabStripItem = <TabStripItem>this.tabStrip.items[i];
|
||||
const tabBarItem = this.createTabBarItem(tabStripItem, i);
|
||||
updateTitleAndIconPositions(tabStripItem, tabBarItem, controller);
|
||||
|
||||
const states = getTitleAttributesForStates(tabStripItem.label);
|
||||
applyStatesToItem(tabBarItem, states);
|
||||
|
||||
controller.tabBarItem = tabBarItem;
|
||||
(<any>tabStripItem).index = i;
|
||||
tabStripItem.setNativeView(tabBarItem);
|
||||
}
|
||||
|
||||
controllers.addObject(controller);
|
||||
});
|
||||
|
||||
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 createTabBarItem(item: TabStripItem, index: number): UITabBarItem {
|
||||
let image: UIImage;
|
||||
let title: string;
|
||||
|
||||
if (item.isLoaded) {
|
||||
image = this.getIcon(item);
|
||||
title = item.label.text;
|
||||
|
||||
const textTransform = item.label.style.textTransform;
|
||||
if (textTransform) {
|
||||
title = getTransformedText(title, textTransform);
|
||||
}
|
||||
}
|
||||
|
||||
const tabBarItem = UITabBarItem.alloc().initWithTitleImageTag(title, image, index);
|
||||
|
||||
return tabBarItem;
|
||||
}
|
||||
|
||||
private getIconRenderingMode(): UIImageRenderingMode {
|
||||
switch (this.tabStrip && this.tabStrip.iosIconRenderingMode) {
|
||||
case "alwaysOriginal":
|
||||
return UIImageRenderingMode.AlwaysOriginal;
|
||||
case "alwaysTemplate":
|
||||
return UIImageRenderingMode.AlwaysTemplate;
|
||||
case "automatic":
|
||||
default:
|
||||
return UIImageRenderingMode.Automatic;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const target = tabStripItem.image;
|
||||
const font = target.style.fontInternal;
|
||||
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 {
|
||||
is = ImageSource.fromFileOrResourceSync(iconSource);
|
||||
}
|
||||
|
||||
if (is && is.ios) {
|
||||
image = is.ios;
|
||||
|
||||
if (this.tabStrip && this.tabStrip.isIconSizeFixed) {
|
||||
image = this.getFixedSizeIcon(image);
|
||||
}
|
||||
|
||||
let renderingMode: UIImageRenderingMode = UIImageRenderingMode.AlwaysOriginal;
|
||||
if (!isFontIcon) {
|
||||
renderingMode = this.getIconRenderingMode();
|
||||
}
|
||||
const originalRenderedImage = image.imageWithRenderingMode(renderingMode);
|
||||
this._iconsCache[iconTag] = originalRenderedImage;
|
||||
image = originalRenderedImage;
|
||||
} else {
|
||||
// TODO
|
||||
// traceMissingIcon(iconSource);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private getFixedSizeIcon(image: UIImage): UIImage {
|
||||
const inWidth = image.size.width;
|
||||
const inHeight = image.size.height;
|
||||
|
||||
const iconSpecSize = getIconSpecSize({ width: inWidth, height: inHeight });
|
||||
|
||||
const widthPts = iconSpecSize.width;
|
||||
const heightPts = iconSpecSize.height;
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions({ width: widthPts, height: heightPts }, false, layout.getDisplayDensity());
|
||||
image.drawInRect(CGRectMake(0, 0, widthPts, heightPts));
|
||||
let resultImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return resultImage;
|
||||
}
|
||||
|
||||
// private _updateIOSTabBarColorsAndFonts(): void {
|
||||
// if (!this.tabStrip || !this.tabStrip.items || !this.tabStrip.items.length) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const tabBar = <UITabBar>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();
|
||||
// }
|
||||
|
||||
[selectedIndexProperty.setNative](value: number) {
|
||||
// TODO
|
||||
// if (traceEnabled()) {
|
||||
// traceWrite("TabView._onSelectedIndexPropertyChangedSetNativeValue(" + value + ")", traceCategories.Debug);
|
||||
// }
|
||||
|
||||
if (value > -1) {
|
||||
(<any>this._ios)._willSelectViewController = this._ios.viewControllers[value];
|
||||
this._ios.selectedIndex = value;
|
||||
}
|
||||
}
|
||||
|
||||
[itemsProperty.getDefault](): TabContentItem[] {
|
||||
return null;
|
||||
}
|
||||
[itemsProperty.setNative](value: TabContentItem[]) {
|
||||
if (value) {
|
||||
value.forEach((item: TabContentItem, i) => {
|
||||
(<any>item).index = i;
|
||||
});
|
||||
}
|
||||
|
||||
this.setViewControllers(value);
|
||||
selectedIndexProperty.coerce(this);
|
||||
}
|
||||
|
||||
[tabStripProperty.getDefault](): TabStrip {
|
||||
return null;
|
||||
}
|
||||
|
||||
[tabStripProperty.setNative](value: TabStrip) {
|
||||
this.setViewControllers(this.items);
|
||||
selectedIndexProperty.coerce(this);
|
||||
}
|
||||
}
|
||||
|
||||
interface TabStates {
|
||||
normalState?: any;
|
||||
selectedState?: any;
|
||||
}
|
||||
|
||||
function getTitleAttributesForStates(view: View): TabStates {
|
||||
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 };
|
||||
if (textColor) {
|
||||
result.normalState[UITextAttributeTextColor] = 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) {
|
||||
if (!states) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setTitleTextAttributesForState(states.normalState, UIControlState.Normal);
|
||||
item.setTitleTextAttributesForState(states.selectedState, UIControlState.Selected);
|
||||
}
|
||||
5
nativescript-core/ui/bottom-navigation/package.json
Normal file
5
nativescript-core/ui/bottom-navigation/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "bottom-navigation",
|
||||
"main": "bottom-navigation",
|
||||
"types": "bottom-navigation.d.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user