feat(bottom-navigation-android): add tabstripitem css support (#7458)

* wip: add background color placeholders for tabstripitem

* feat: add css for tabstripitem for bottom navigation android

* chore: update example

* fix: revert native default index

* clean up tabcontentitem

* update setTabBarItemTextTransform

* textTransform inherited css property now

* fix(android-bottom-navigation): fragment detach logic

* chore: fix tests

* fix(android-bottom-navigation): fragment lifecycle logic

* fix: revert text-transform inherited css property
This commit is contained in:
Martin Yankov
2019-07-09 13:33:32 +03:00
committed by Manol Donev
parent 6e1e0e843a
commit fab9c90007
24 changed files with 761 additions and 108 deletions

View File

@ -2,11 +2,14 @@
import { TabStrip } from "../tab-navigation-base/tab-strip";
import { TabStripItem } from "../tab-navigation-base/tab-strip-item";
import { TabContentItem } from "../tab-navigation-base/tab-content-item";
import { TextTransform } from "../text-base";
// Requires
import { TabNavigationBase, itemsProperty, selectedIndexProperty, tabStripProperty } from "../tab-navigation-base/tab-navigation-base";
import { Font } from "../styling/font";
import { getTransformedText } from "../text-base";
import { CSSType, Color } from "../core/view";
import { Frame } from "../frame";
import { Frame, View } from "../frame";
import { RESOURCE_PREFIX, ad, layout } from "../../utils/utils";
import { fromFileOrResource } from "../../image-source";
// TODO: Impl trace
@ -22,8 +25,11 @@ const DEFAULT_ELEVATION = 8;
const TABID = "_tabId";
const INDEX = "_index";
const ownerSymbol = Symbol("_owner");
let TabFragment: any;
let BottomNavigationBar: any;
let AttachStateChangeListener: any;
function makeFragmentName(viewId: number, id: number): string {
return "android:bottomnavigation:" + viewId + ":" + id;
@ -89,20 +95,77 @@ function initializeNativeClasses() {
return global.__native(this);
}
public onSelectedPositionChange(position: number): void {
this.owner.changeTab(position);
this.owner.selectedIndex = position;
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): void {
const owner = this.owner;
if (!owner) {
return;
}
const tabStripItems = owner.tabStrip && owner.tabStrip.items;
if (position >= 0 && tabStripItems[position]) {
tabStripItems[position]._emit(TabStripItem.tapEvent);
}
}
}
@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();
}
function createTabItemSpec(tabStripItem: TabStripItem): org.nativescript.widgets.TabItemSpec {
const result = new org.nativescript.widgets.TabItemSpec();
result.title = tabStripItem.title;
if (tabStripItem.backgroundColor instanceof Color) {
result.backgroundColor = tabStripItem.backgroundColor.android;
}
if (tabStripItem.iconSource) {
if (tabStripItem.iconSource.indexOf(RESOURCE_PREFIX) === 0) {
result.iconId = ad.resources.getDrawableId(tabStripItem.iconSource.substr(RESOURCE_PREFIX.length));
@ -151,6 +214,8 @@ export class BottomNavigation extends TabNavigationBase {
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;
constructor() {
super();
@ -187,17 +252,17 @@ export class BottomNavigation extends TabNavigationBase {
// CONTENT VIEW
const contentView = new org.nativescript.widgets.ContentLayout(this._context);
const contentViewLP = new org.nativescript.widgets.CommonLayoutParams();
contentViewLP.row = 0;
contentView.setLayoutParams(contentViewLP);
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 bottomNavigationBarLP = new org.nativescript.widgets.CommonLayoutParams();
bottomNavigationBarLP.row = 1;
bottomNavigationBar.setLayoutParams(bottomNavigationBarLP);
const bottomNavigationBarLayoutParams = new org.nativescript.widgets.CommonLayoutParams();
bottomNavigationBarLayoutParams.row = 1;
bottomNavigationBar.setLayoutParams(bottomNavigationBarLayoutParams);
nativeView.addView(bottomNavigationBar);
(<any>nativeView).bottomNavigationBar = bottomNavigationBar;
@ -213,15 +278,25 @@ export class BottomNavigation extends TabNavigationBase {
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) {
@ -265,19 +340,46 @@ export class BottomNavigation extends TabNavigationBase {
public onLoaded(): void {
super.onLoaded();
this.setTabStripItems();
const items = this.tabStrip ? this.tabStrip.items : null;
this.setTabStripItems(items);
if (this._attachedToWindow) {
this.changeTab(this.selectedIndex);
}
}
_onAttachedToWindow(): void {
super._onAttachedToWindow();
this._attachedToWindow = true;
this.changeTab(this.selectedIndex);
}
_onDetachedFromWindow(): void {
super._onDetachedFromWindow();
this._attachedToWindow = false;
}
public onUnloaded(): void {
super.onUnloaded();
this.setTabStripItems();
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();
}
@ -288,78 +390,132 @@ export class BottomNavigation extends TabNavigationBase {
// i.e. in a scenario with tab frames let the frames cleanup their fragments first, and then
// cleanup the tab fragments to avoid
// android.content.res.Resources$NotFoundException: Unable to find resource ID #0xfffffff6
this.disposeCurrentFragments();
this.disposeTabFragments();
}
private disposeCurrentFragments(): void {
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 containerView = this._contentView;
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();
const transaction = fragmentManager.beginTransaction();
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 (this._currentFragment) {
const fragment = this._currentFragment;
transaction.detach(fragment);
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;
}
}
const name = makeFragmentName(containerView.getId(), index);
let fragment: androidx.fragment.app.Fragment = fragmentManager.findFragmentByTag(name);
if (fragment != null) {
transaction.attach(fragment);
} else {
fragment = TabFragment.newInstance(this._domId, index);
transaction.add(containerView.getId(), fragment, name);
if (this.items && this.items[position]) {
this.items[position].canBeLoaded = false;
}
this._currentFragment = fragment;
const tabItems = this.items;
const tabItem = tabItems ? tabItems[index] : null;
if (tabItem) {
tabItem.canBeLoaded = true;
this._loadUnloadTabItems(index);
}
transaction.commitNowAllowingStateLoss();
}
private setTabStripItems() {
if (this.tabStrip && this.tabStrip.items) {
const tabItems = new Array<org.nativescript.widgets.TabItemSpec>();
this.tabStrip.items.forEach((item, i, arr) => {
if (this.tabStrip.items[i]) {
const tabItemSpec = createTabItemSpec(this.tabStrip.items[i]);
tabItems.push(tabItemSpec);
}
});
this._bottomNavigationBar.setItems(tabItems);
this.tabStrip.setNativeView(this._bottomNavigationBar);
this.tabStrip.items.forEach((item, i, arr) => {
const tv = this._bottomNavigationBar.getTextViewForItemAt(i);
item.setNativeView(tv);
});
} else {
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 = createTabItemSpec(items[i]);
tabItems.push(tabItemSpec);
}
});
this._bottomNavigationBar.setItems(tabItems);
items.forEach((item, i, arr) => {
const textView = this._bottomNavigationBar.getTextViewForItemAt(i);
item.setNativeView(textView);
});
}
public updateAndroidItemAt(index: number, spec: org.nativescript.widgets.TabItemSpec) {
@ -378,6 +534,80 @@ export class BottomNavigation extends TabNavigationBase {
}
}
public getTabBarColor(): number {
return this._bottomNavigationBar.getTabTextColor();
}
public setTabBarColor(value: number | Color): void {
if (value instanceof Color) {
this._bottomNavigationBar.setTabTextColor(value.android);
this._bottomNavigationBar.setSelectedTabTextColor(value.android);
} else {
this._bottomNavigationBar.setTabTextColor(value);
this._bottomNavigationBar.setSelectedTabTextColor(value);
}
}
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 = createTabItemSpec(tabStripItem);
this.updateAndroidItemAt(tabStripItemIndex, tabItemSpec);
}
public getTabBarItemColor(tabStripItem: TabStripItem): number {
return tabStripItem.nativeViewProtected.getCurrentTextColor();
}
public setTabBarItemColor(tabStripItem: TabStripItem, value: number | Color): void {
if (typeof value === "number") {
tabStripItem.nativeViewProtected.setTextColor(value);
} else {
tabStripItem.nativeViewProtected.setTextColor(value.android);
}
}
public getTabBarItemFontSize(tabStripItem: TabStripItem): { nativeSize: number } {
return { nativeSize: tabStripItem.nativeViewProtected.getTextSize() };
}
public setTabBarItemFontSize(tabStripItem: TabStripItem, value: number | { nativeSize: number }): void {
if (typeof value === "number") {
tabStripItem.nativeViewProtected.setTextSize(value);
} else {
tabStripItem.nativeViewProtected.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize);
}
}
public getTabBarItemFontInternal(tabStripItem: TabStripItem): android.graphics.Typeface {
return tabStripItem.nativeViewProtected.getTypeface();
}
public setTabBarItemFontInternal(tabStripItem: TabStripItem, value: Font | android.graphics.Typeface): void {
tabStripItem.nativeViewProtected.setTypeface(value instanceof Font ? value.getAndroidTypeface() : value);
}
private _defaultTransformationMethod: android.text.method.TransformationMethod;
public getTabBarItemTextTransform(tabStripItem: TabStripItem): "default" {
return "default";
}
public setTabBarItemTextTransform(tabStripItem: TabStripItem, value: TextTransform | "default"): void {
const tv = tabStripItem.nativeViewProtected;
this._defaultTransformationMethod = this._defaultTransformationMethod || tv.getTransformationMethod();
if (value === "default") {
tv.setTransformationMethod(this._defaultTransformationMethod);
tv.setText(tabStripItem.title);
} else {
const result = getTransformedText(tabStripItem.title, value);
tv.setText(result);
tv.setTransformationMethod(null);
}
}
[selectedIndexProperty.setNative](value: number) {
// const smoothScroll = false;
@ -405,7 +635,8 @@ export class BottomNavigation extends TabNavigationBase {
return null;
}
[tabStripProperty.setNative](value: TabStrip) {
this.setTabStripItems();
const items = this.tabStrip ? this.tabStrip.items : null;
this.setTabStripItems(items);
}
}