diff --git a/android/modules/android/src/java/ti/modules/titanium/android/AndroidModule.java b/android/modules/android/src/java/ti/modules/titanium/android/AndroidModule.java index fdc5c89431a..991380b9eb2 100644 --- a/android/modules/android/src/java/ti/modules/titanium/android/AndroidModule.java +++ b/android/modules/android/src/java/ti/modules/titanium/android/AndroidModule.java @@ -363,6 +363,9 @@ public class AndroidModule extends KrollModule @Kroll.constant public static final int FLAG_UPDATE_CURRENT = PendingIntent.FLAG_UPDATE_CURRENT; + @Kroll.constant + public static final int STATUS_BAR_LIGHT = 8192; + @Kroll.constant public static final int RESULT_OK = Activity.RESULT_OK; @Kroll.constant diff --git a/android/modules/ui/res/layout/titanium_ui_bottom_navigation.xml b/android/modules/ui/res/layout/titanium_ui_bottom_navigation.xml new file mode 100644 index 00000000000..9a50ac07b42 --- /dev/null +++ b/android/modules/ui/res/layout/titanium_ui_bottom_navigation.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java index 0c76925eb2e..faa4abc14e6 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java @@ -6,10 +6,14 @@ */ package ti.modules.titanium.ui; +import static ti.modules.titanium.android.AndroidModule.STATUS_BAR_LIGHT; + import android.app.Activity; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; @@ -29,6 +33,7 @@ import org.appcelerator.titanium.TiRootActivity; import org.appcelerator.titanium.proxy.ActivityProxy; import org.appcelerator.titanium.proxy.TiWindowProxy; +import org.appcelerator.titanium.util.TiColorHelper; import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.util.TiRHelper; import org.appcelerator.titanium.util.TiUIHelper; @@ -39,6 +44,7 @@ import ti.modules.titanium.ui.android.AndroidModule; import ti.modules.titanium.ui.widget.tabgroup.TiUIAbstractTabGroup; +import ti.modules.titanium.ui.widget.tabgroup.TiUIBottomNavigation; import ti.modules.titanium.ui.widget.tabgroup.TiUIBottomNavigationTabGroup; import ti.modules.titanium.ui.widget.tabgroup.TiUITabLayoutTabGroup; @@ -49,7 +55,8 @@ TiC.PROPERTY_SWIPEABLE, TiC.PROPERTY_AUTO_TAB_TITLE, TiC.PROPERTY_EXIT_ON_CLOSE, - TiC.PROPERTY_SMOOTH_SCROLL_ON_TAB_CLICK + TiC.PROPERTY_SMOOTH_SCROLL_ON_TAB_CLICK, + TiC.PROPERTY_INDICATOR_COLOR }) public class TabGroupProxy extends TiWindowProxy implements TiActivityWindow { @@ -69,6 +76,7 @@ public class TabGroupProxy extends TiWindowProxy implements TiActivityWindow private Object selectedTab; // NOTE: Can be TabProxy or Number private String tabGroupTitle = null; private boolean autoTabTitle = false; + private boolean tabEnabled = true; public TabGroupProxy() { @@ -189,6 +197,22 @@ public void setActiveTab(Object tabOrIndex) } } + @Kroll.setProperty + public void setEnabled(Boolean enabled) + { + tabEnabled = enabled; + TiUIAbstractTabGroup tabGroup = (TiUIAbstractTabGroup) view; + if (tabGroup != null) { + tabGroup.setEnabled(enabled); + } + } + + @Kroll.getProperty + public Boolean getEnabled() + { + return tabEnabled; + } + private TabProxy getActiveTabProxy() { Object activeTab = getActiveTab(); @@ -279,6 +303,9 @@ public void handleCreationDict(KrollDict options) if (options.containsKeyAndNotNull(TiC.PROPERTY_ACTIVE_TAB)) { setActiveTab(options.get(TiC.PROPERTY_ACTIVE_TAB)); } + if (options.containsKeyAndNotNull(TiC.PROPERTY_ENABLED)) { + setEnabled(options.getBoolean(TiC.PROPERTY_ENABLED)); + } } @Kroll.getProperty @@ -323,6 +350,21 @@ protected void handleOpen(KrollDict options) if (topActivity == null || topActivity.isFinishing()) { return; } + + // set theme for XML layout + if (hasProperty(TiC.PROPERTY_STYLE) + && ((Integer) getProperty(TiC.PROPERTY_STYLE)) == AndroidModule.TABS_STYLE_BOTTOM_NAVIGATION + && getProperty(TiC.PROPERTY_THEME) != null) { + try { + String themeName = getProperty(TiC.PROPERTY_THEME).toString(); + int theme = TiRHelper.getResource("style." + + themeName.replaceAll("[^A-Za-z0-9_]", "_")); + topActivity.setTheme(theme); + topActivity.getApplicationContext().setTheme(theme); + } catch (Exception e) { + } + } + Intent intent = new Intent(topActivity, TiActivity.class); fillIntent(topActivity, intent); @@ -367,7 +409,11 @@ public void windowCreated(TiBaseActivity activity, Bundle savedInstanceState) ((TiUITabLayoutTabGroup) view).setTabMode((Integer) getProperty(TiC.PROPERTY_TAB_MODE)); } } else { - view = new TiUIBottomNavigationTabGroup(this, activity); + if (TiConvert.toBoolean(getProperty("experimental"), false)) { + view = new TiUIBottomNavigation(this, activity); + } else { + view = new TiUIBottomNavigationTabGroup(this, activity); + } } // If we have set a title before the creation of the native view, set it now. if (this.tabGroupTitle != null) { @@ -405,6 +451,22 @@ public void windowCreated(TiBaseActivity activity, Bundle savedInstanceState) // Need to handle the cached activity proxy properties in the JS side. callPropertySync(PROPERTY_POST_TAB_GROUP_CREATED, null); + + if (getActivity() != null) { + if (hasPropertyAndNotNull(TiC.PROPERTY_FLAGS)) { + if (TiConvert.toInt(getProperty(TiC.PROPERTY_FLAGS)) == STATUS_BAR_LIGHT + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + getActivity().getWindow().getDecorView() + .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + } + if (hasPropertyAndNotNull(TiC.PROPERTY_STATUS_BAR_COLOR)) { + int colorInt = TiColorHelper.parseColor( + TiConvert.toString(getProperty(TiC.PROPERTY_STATUS_BAR_COLOR)), + TiApplication.getAppRootOrCurrentActivity()); + getActivity().getWindow().setStatusBarColor(colorInt); + } + } } @Override diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java index 1782c67e97d..c9690a84647 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java @@ -7,6 +7,8 @@ package ti.modules.titanium.ui; +import static ti.modules.titanium.android.AndroidModule.STATUS_BAR_LIGHT; + import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; @@ -14,6 +16,7 @@ import android.graphics.PixelFormat; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.os.Message; import android.text.Spannable; @@ -333,6 +336,13 @@ public void windowCreated(TiBaseActivity activity, Bundle savedInstanceState) win.getDecorView().setSystemUiVisibility(TiConvert.toInt(getProperty(TiC.PROPERTY_UI_FLAGS))); } + if (hasProperty(TiC.PROPERTY_WINDOW_FLAGS)) { + if ((TiConvert.toInt(getProperty(TiC.PROPERTY_WINDOW_FLAGS)) & STATUS_BAR_LIGHT) != 0 + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + win.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + } + // Handle titleAttributes property. if (hasProperty(TiC.PROPERTY_TITLE_ATTRIBUTES)) { KrollDict innerAttributes = getProperties().getKrollDict(TiC.PROPERTY_TITLE_ATTRIBUTES); diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIAbstractTabGroup.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIAbstractTabGroup.java index 57d27a4ebab..883daac91ef 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIAbstractTabGroup.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIAbstractTabGroup.java @@ -40,6 +40,7 @@ import org.appcelerator.titanium.proxy.TiWindowProxy; import org.appcelerator.titanium.util.TiColorHelper; import org.appcelerator.titanium.util.TiConvert; +import org.appcelerator.titanium.util.TiIconDrawable; import org.appcelerator.titanium.util.TiUIHelper; import org.appcelerator.titanium.view.TiInsetsProvider; import org.appcelerator.titanium.view.TiUIView; @@ -115,6 +116,13 @@ public abstract class TiUIAbstractTabGroup extends TiUIView */ public abstract void updateTabBackgroundDrawable(int index); + /** + * Material 3 active indicator color + * + * @param color color + */ + public abstract void updateActiveIndicatorColor(int color); + /** * Update the tab's title to the proper text. * @@ -144,6 +152,13 @@ public abstract class TiUIAbstractTabGroup extends TiUIView */ public abstract String getTabTitle(int index); + /** + * Enables/disables tab menu + * + * @param enabled value + */ + public abstract void setEnabled(Boolean enabled); + // region protected fields protected final static String TAG = "TiUIAbstractTabGroup"; protected static final String WARNING_LAYOUT_MESSAGE = @@ -454,7 +469,7 @@ public void onPageScrollStateChanged(int i) // Set action bar color. if (proxy != null) { final ActionBar actionBar = ((AppCompatActivity) proxy.getActivity()).getSupportActionBar(); - if (actionBar != null) { + if (actionBar != null && !this.tabs.isEmpty()) { final TiWindowProxy windowProxy = ((TabProxy) this.tabs.get(tabIndex).getProxy()).getWindow(); final KrollDict windowProperties = windowProxy.getProperties(); final KrollDict properties = getProxy().getProperties(); @@ -495,6 +510,9 @@ public void processProperties(KrollDict d) } else { setBackgroundColor(getDefaultBackgroundColor()); } + if (d.containsKeyAndNotNull(TiC.PROPERTY_INDICATOR_COLOR)) { + updateActiveIndicatorColor(TiConvert.toColor(d, TiC.PROPERTY_INDICATOR_COLOR, proxy.getActivity())); + } super.processProperties(d); } @@ -516,6 +534,8 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP for (TiUITab tabView : tabs) { updateTabBackgroundDrawable(tabs.indexOf(tabView)); } + } else if (key.equals(TiC.PROPERTY_INDICATOR_COLOR)) { + updateActiveIndicatorColor(TiColorHelper.parseColor(newValue.toString(), proxy.getActivity())); } else { super.propertyChanged(key, oldValue, newValue, proxy); } @@ -552,7 +572,12 @@ public Drawable updateIconTint(TiViewProxy tabProxy, Drawable drawable, boolean } // Clone existing drawable so color filter applies correctly. - drawable = drawable.getConstantState().newDrawable(); + if (drawable.getConstantState() == null && drawable.getClass() == TiIconDrawable.class) { + // TiIconDrawable + drawable = drawable.mutate(); + } else { + drawable = drawable.getConstantState().newDrawable(); + } final KrollDict tabProperties = tabProxy.getProperties(); final KrollDict properties = getProxy().getProperties(); diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigation.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigation.java new file mode 100644 index 00000000000..8a7d3105c50 --- /dev/null +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigation.java @@ -0,0 +1,553 @@ +/** + * Titanium SDK + * Copyright TiDev, Inc. 04/07/2022-Present. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +package ti.modules.titanium.ui.widget.tabgroup; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.core.graphics.ColorUtils; + +import com.google.android.material.badge.BadgeDrawable; +import com.google.android.material.bottomnavigation.BottomNavigationMenuView; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.navigation.NavigationBarView; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.MaterialShapeDrawable; +import com.google.android.material.shape.ShapeAppearanceModel; + +import org.appcelerator.titanium.TiApplication; +import org.appcelerator.titanium.TiBaseActivity; +import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.TiDimension; +import org.appcelerator.titanium.proxy.TiViewProxy; +import org.appcelerator.titanium.util.TiConvert; +import org.appcelerator.titanium.util.TiIconDrawable; +import org.appcelerator.titanium.util.TiRHelper; +import org.appcelerator.titanium.util.TiUIHelper; +import org.appcelerator.titanium.view.TiUIView; + +import java.util.ArrayList; + +import ti.modules.titanium.ui.TabGroupProxy; +import ti.modules.titanium.ui.TabProxy; + +/** + * TabGroup implementation using BottomNavigationView as a controller. + */ +public class TiUIBottomNavigation extends TiUIAbstractTabGroup implements BottomNavigationView.OnItemSelectedListener +{ + + protected final static String TAG = "TiUIBottomNavigation"; + static int id_layout = 0; + static int id_content = 0; + static int id_bottomNavigation = 0; + private int currentlySelectedIndex = -1; + private ArrayList mMenuItemsArray = new ArrayList(); + private RelativeLayout layout = null; + private FrameLayout centerView; + private BottomNavigationView bottomNavigation; + private ArrayList tabsArray = new ArrayList(); + + public TiUIBottomNavigation(TabGroupProxy proxy, TiBaseActivity activity) + { + super(proxy, activity); + } + + // Overriding addTab method to provide a proper guard for trying to add more tabs than the limit + // for BottomNavigationView class. + @Override + public void addTab(TabProxy tabProxy) + { + if (this.bottomNavigation == null) { + return; + } + final int MAX_TABS = this.bottomNavigation.getMaxItemCount(); + if (this.tabs.size() < MAX_TABS) { + super.addTab(tabProxy); + } else { + Log.w(TAG, "Bottom style TabGroup cannot have more than " + MAX_TABS + " tabs."); + } + } + + private Drawable setIcon(Object icon, Object iconFamily) + { + Drawable drawable = null; + + if (icon instanceof Number) { + drawable = TiUIHelper.getResourceDrawable((int) icon); + } else if (iconFamily != null) { + drawable = new TiIconDrawable(TiConvert.toString(icon), TiConvert.toString(iconFamily)); + } else { + drawable = TiUIHelper.getResourceDrawable(icon); + } + return drawable; + } + + @Override + public void addViews(TiBaseActivity activity) + { + mMenuItemsArray = new ArrayList<>(); + if (tabsArray == null) { + tabsArray = new ArrayList(); + } + + try { + id_layout = TiRHelper.getResource("layout.titanium_ui_bottom_navigation"); + id_content = TiRHelper.getResource("id.bottomNavBar_content"); + id_bottomNavigation = TiRHelper.getResource("id.bottomNavBar"); + + LayoutInflater inflater = LayoutInflater.from(TiApplication.getAppRootOrCurrentActivity()); + layout = (RelativeLayout) inflater.inflate(id_layout, null, false); + bottomNavigation = layout.findViewById(id_bottomNavigation); + centerView = layout.findViewById(id_content); + + bottomNavigation.setOnItemSelectedListener(this); + activity.setLayout(layout); + + if (proxy.hasPropertyAndNotNull(TiC.PROPERTY_PADDING_LEFT) + || proxy.hasPropertyAndNotNull(TiC.PROPERTY_PADDING_RIGHT) + || proxy.hasPropertyAndNotNull(TiC.PROPERTY_PADDING_BOTTOM)) { + // Floating Bottom Navigation + // Fetch padding properties. If at least 1 property is non-zero, then show a floating tab bar. + final TiDimension paddingLeft = TiConvert.toTiDimension( + proxy.getProperty(TiC.PROPERTY_PADDING_LEFT), TiDimension.TYPE_LEFT); + final TiDimension paddingRight = TiConvert.toTiDimension( + proxy.getProperty(TiC.PROPERTY_PADDING_RIGHT), TiDimension.TYPE_RIGHT); + final TiDimension paddingBottom = TiConvert.toTiDimension( + proxy.getProperty(TiC.PROPERTY_PADDING_BOTTOM), TiDimension.TYPE_BOTTOM); + final boolean isFloating + = ((paddingLeft != null) && (paddingLeft.getValue() > 0)) + || ((paddingRight != null) && (paddingRight.getValue() > 0)) + || ((paddingBottom != null) && (paddingBottom.getValue() > 0)); + + if (isFloating) { + // Set up tab bar to look like a floating toolbar with rounded corners. + int pLeft = (paddingLeft != null) ? paddingLeft.getAsPixels(bottomNavigation) : 0; + int pRight = (paddingRight != null) ? paddingRight.getAsPixels(bottomNavigation) : 0; + int pBottom = (paddingBottom != null) ? paddingBottom.getAsPixels(bottomNavigation) : 0; + + MaterialShapeDrawable shapeDrawable = null; + Drawable background = bottomNavigation.getBackground(); + if (background instanceof MaterialShapeDrawable) { + shapeDrawable = (MaterialShapeDrawable) background; + } else { + shapeDrawable = new MaterialShapeDrawable(); + background = shapeDrawable; + bottomNavigation.setBackground(shapeDrawable); + } + ShapeAppearanceModel model = shapeDrawable.getShapeAppearanceModel(); + float radius = (new TiDimension("17dp", TiDimension.TYPE_LEFT)).getAsPixels(bottomNavigation); + model = model.toBuilder().setAllCorners(CornerFamily.ROUNDED, radius).build(); + shapeDrawable.setShapeAppearanceModel(model); + bottomNavigation.setPadding((int) (radius * 0.75), 0, (int) (radius * 0.75), 0); + bottomNavigation.setElevation( + (new TiDimension("8dp", TiDimension.TYPE_BOTTOM)).getAsPixels(bottomNavigation)); + RelativeLayout.LayoutParams params + = (RelativeLayout.LayoutParams) bottomNavigation.getLayoutParams(); + params.setMargins(pLeft, 0, pRight, pBottom); + + RelativeLayout.LayoutParams paramsCenter + = (RelativeLayout.LayoutParams) centerView.getLayoutParams(); + paramsCenter.removeRule(RelativeLayout.ABOVE); + } + } + + } catch (Exception ex) { + Log.e(TAG, "XML resources could not be found!!!" + ex.getMessage()); + } + } + + /** + * Handle the removing of the controller from the UI layout when tab navigation is disabled. + * + * @param disable + */ + @Override + public void disableTabNavigation(boolean disable) + { + super.disableTabNavigation(disable); + } + + @Override + public void addTabItemInController(TiViewProxy tabProxy) + { + // Guard for the limit of tabs in the BottomNavigationView. + if (this.mMenuItemsArray.size() == 5) { + Log.e(TAG, "Trying to add more than five tabs in a TabGroup with TABS_STYLE_BOTTOM_NAVIGATION style."); + return; + } + + tabsArray.add(tabProxy); + MenuItem menuItem = bottomNavigation.getMenu().add(0, mMenuItemsArray.size(), 0, ""); + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_TITLE)) { + menuItem.setTitle(tabProxy.getProperty(TiC.PROPERTY_TITLE).toString()); + } + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_ICON)) { + menuItem.setIcon(setIcon(tabProxy.getProperty(TiC.PROPERTY_ICON), tabProxy.getProperty("iconFamily"))); + } + + mMenuItemsArray.add(menuItem); + int index = this.mMenuItemsArray.size() - 1; + updateDrawablesAfterNewItem(index); + + final int shiftMode = proxy.getProperties().optInt(TiC.PROPERTY_SHIFT_MODE, 1); + switch (shiftMode) { + case 0: + this.bottomNavigation.setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_LABELED); + break; + case 1: + this.bottomNavigation.setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_AUTO); + break; + case 2: + // NOTE: Undocumented for now, will create new property that has parity with iOS. + this.bottomNavigation.setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_UNLABELED); + break; + } + } + + /** + * Remove an item from the BottomNavigationView for a specific index. + * + * @param position the position of the removed item. + */ + @Override + public void removeTabItemFromController(int position) + { + this.bottomNavigation.getMenu().clear(); + this.mMenuItemsArray.clear(); + for (TiUITab tabView : tabs) { + addTabItemInController(tabView.getProxy()); + } + } + + /** + * Select an item from the BottomNavigationView with a specific position. + * + * @param position the position of the item to be selected. + */ + @Override + public void selectTabItemInController(int position) + { + // select first tab and fire event + currentlySelectedIndex = position; + ((TabGroupProxy) getProxy()).onTabSelected(position); + } + + private void updateDrawablesAfterNewItem(int index) + { + updateTabTitle(index); + updateTabIcon(index); + updateBadge(index); + updateBadgeColor(index); + + for (int i = 0; i < this.bottomNavigation.getMenu().size(); i++) { + // Set the title text color. + updateTabTitleColor(i); + // Set the background drawable. + updateTabBackgroundDrawable(i); + } + } + + @Override + public void setBackgroundColor(int colorInt) + { + if (bottomNavigation == null) { + return; + } + // Update tab bar's background color. + Drawable drawable = bottomNavigation.getBackground(); + if (drawable instanceof MaterialShapeDrawable shapeDrawable) { + shapeDrawable.setFillColor(ColorStateList.valueOf(colorInt)); + shapeDrawable.setElevation(0); // Drawable will tint the fill color if elevation is non-zero. + } else { + bottomNavigation.setBackgroundColor(colorInt); + } + + // Apply given color to bottom navigation bar if using a "solid" theme. + if (isUsingSolidTitaniumTheme() && (Build.VERSION.SDK_INT >= 27)) { + Activity activity = (this.proxy != null) ? this.proxy.getActivity() : null; + Window window = (activity != null) ? activity.getWindow() : null; + View decorView = (window != null) ? window.getDecorView() : null; + if ((window != null) && (decorView != null)) { + int uiFlags = decorView.getSystemUiVisibility(); + if (ColorUtils.calculateLuminance(colorInt) > 0.5) { + uiFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } else { + uiFlags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + decorView.setSystemUiVisibility(uiFlags); + window.setNavigationBarColor(colorInt); + } + } + } + + @Override + @SuppressLint("RestrictedApi") + public void updateTabBackgroundDrawable(int index) + { + try { + // BottomNavigationMenuView rebuilds itself after adding a new item, so we need to reset the colors each time. + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + boolean hasTouchFeedback = TiConvert.toBoolean(tabProxy.getProperty(TiC.PROPERTY_TOUCH_FEEDBACK), true); + boolean hasTouchFeedbackColor = tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR); + if (hasCustomBackground(tabProxy) || hasCustomIconTint(tabProxy) || hasTouchFeedbackColor) { + BottomNavigationMenuView bottomMenuView = + ((BottomNavigationMenuView) this.bottomNavigation.getChildAt(0)); + Drawable drawable = createBackgroundDrawableForState(tabProxy, android.R.attr.state_checked); + int color = getActiveColor(tabProxy); + if (hasTouchFeedbackColor) { + color = TiConvert.toColor(tabProxy.getProperty(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR), + tabProxy.getActivity()); + } + drawable = new RippleDrawable(createRippleColorStateListFrom(color), drawable, null); + bottomMenuView.getChildAt(index).setBackground(drawable); + } + + if (!hasTouchFeedback) { + Drawable drawable = new RippleDrawable(ColorStateList.valueOf(Color.TRANSPARENT), null, null); + this.bottomNavigation.getChildAt(0).setBackground(drawable); + } + + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) { + BottomNavigationMenuView bottomMenuView = + ((BottomNavigationMenuView) this.bottomNavigation.getChildAt(0)); + bottomMenuView.getChildAt(index).setBackgroundColor(TiConvert.toColor( + tabProxy.getProperty(TiC.PROPERTY_BACKGROUND_COLOR), TiApplication.getAppRootOrCurrentActivity() + )); + } + } catch (Exception e) { + Log.w(TAG, WARNING_LAYOUT_MESSAGE); + } + } + + @Override + public void updateTabTitle(int index) + { + if ((index < 0) || (index >= this.tabs.size()) || tabsArray == null || tabsArray.isEmpty()) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + if (tabProxy == null) { + return; + } + + String title = TiConvert.toString(tabProxy.getProperty(TiC.PROPERTY_TITLE)); + this.bottomNavigation.getMenu().getItem(index).setTitle(title); + } + + @SuppressLint("RestrictedApi") + @Override + public void updateBadge(int index) + { + if ((index < 0) || (index >= tabsArray.size())) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + if (tabProxy == null) { + return; + } + + Object badgeValue = tabProxy.getProperty(TiC.PROPERTY_BADGE); + if ((badgeValue == null) && !TiUIHelper.isUsingMaterialTheme(bottomNavigation.getContext())) { + return; + } + + int menuItemId = bottomNavigation.getMenu().getItem(index).getItemId(); + BadgeDrawable badgeDrawable = bottomNavigation.getOrCreateBadge(menuItemId); + if (badgeValue != null) { + badgeDrawable.setVisible(true); + badgeDrawable.setNumber(TiConvert.toInt(badgeValue, 0)); + } else { + badgeDrawable.setVisible(false); + } + } + + @Override + public void updateBadgeColor(int index) + { + if ((index < 0) || (index >= this.tabs.size())) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + if (tabProxy == null) { + return; + } + + // TODO: reset to default value when property is null + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_BADGE_COLOR)) { + Log.w(TAG, "badgeColor is deprecated. Use badgeBackgroundColor instead."); + int menuItemId = this.bottomNavigation.getMenu().getItem(index).getItemId(); + BadgeDrawable badgeDrawable = this.bottomNavigation.getOrCreateBadge(menuItemId); + badgeDrawable.setBackgroundColor( + TiConvert.toColor(tabProxy.getProperty(TiC.PROPERTY_BADGE_COLOR), tabProxy.getActivity())); + } + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_BADGE_BACKGROUND_COLOR)) { + int menuItemId = this.bottomNavigation.getMenu().getItem(index).getItemId(); + BadgeDrawable badgeDrawable = this.bottomNavigation.getOrCreateBadge(menuItemId); + badgeDrawable.setBackgroundColor( + TiConvert.toColor(tabProxy.getProperty(TiC.PROPERTY_BADGE_BACKGROUND_COLOR), tabProxy.getActivity())); + } + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_BADGE_TEXT_COLOR)) { + int menuItemId = this.bottomNavigation.getMenu().getItem(index).getItemId(); + BadgeDrawable badgeDrawable = this.bottomNavigation.getOrCreateBadge(menuItemId); + badgeDrawable.setBadgeTextColor( + TiConvert.toColor(tabProxy.getProperty(TiC.PROPERTY_BADGE_TEXT_COLOR), tabProxy.getActivity())); + } + } + + @Override + @SuppressLint("RestrictedApi") + public void updateTabTitleColor(int index) + { + try { + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + if (hasCustomTextColor(tabProxy)) { + this.bottomNavigation.setItemTextColor(textColorStateList(tabProxy, android.R.attr.state_checked)); + } + } catch (Exception e) { + Log.w(TAG, WARNING_LAYOUT_MESSAGE); + } + } + + @SuppressLint("RestrictedApi") + public void updateActiveIndicatorColor(int color) + { + try { + // BottomNavigationMenuView rebuilds itself after adding a new item, so we need to reset the colors each time. + + int[][] states = new int[][] { + new int[] { android.R.attr.state_enabled }, // enabled + new int[] { -android.R.attr.state_enabled }, // disabled + new int[] { -android.R.attr.state_checked }, // unchecked + new int[] { android.R.attr.state_pressed } // pressed + }; + + int[] colors = new int[] { + color, + color, + color, + color + }; + + ColorStateList myList = new ColorStateList(states, colors); + + bottomNavigation.setItemActiveIndicatorColor(myList); + } catch (Exception e) { + Log.w(TAG, WARNING_LAYOUT_MESSAGE); + } + } + + @Override + public void updateTabIcon(int index) + { + if ((index < 0) || (index >= this.tabs.size()) || tabsArray == null || tabsArray.isEmpty()) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(index)); + if (tabProxy == null) { + return; + } + + if (tabProxy.hasPropertyAndNotNull(TiC.PROPERTY_ICON)) { + this.bottomNavigation.getMenu().getItem(index).setIcon(setIcon( + tabProxy.getProperty(TiC.PROPERTY_ICON), + tabProxy.getProperty("iconFamily") + )); + } + updateIconTint(); + } + + private void updateIconTint() + { + for (int i = 0; i < this.bottomNavigation.getMenu().size(); i++) { + if (i < tabsArray.size()) { + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(i)); + if (hasCustomIconTint(tabProxy)) { + final boolean selected = i == currentlySelectedIndex; + Drawable drawable = this.bottomNavigation.getMenu().getItem(i).getIcon(); + drawable = updateIconTint(tabProxy, drawable, selected); + this.bottomNavigation.getMenu().getItem(i).setIcon(drawable); + } + } + } + } + + @Override + public String getTabTitle(int index) + { + // Validate index. + if (index < 0 || index > tabs.size() - 1) { + return null; + } + return this.bottomNavigation.getMenu().getItem(index).getTitle().toString(); + } + + @Override + public void setEnabled(Boolean enabled) + { + for (int i = 0; i < this.bottomNavigation.getMenu().size(); i++) { + this.bottomNavigation.getMenu().getItem(i).setEnabled(enabled); + } + } + + @Override + public void selectTab(int tabIndex) + { + super.selectTab(tabIndex); + if (tabsArray == null || tabsArray.isEmpty()) { + return; + } + + // unselected event + if ((currentlySelectedIndex >= 0) && (tabIndex != currentlySelectedIndex) + && (currentlySelectedIndex < this.tabs.size()) && (getProxy() != null)) { + TiViewProxy tabProxy = ((TabProxy) tabsArray.get(currentlySelectedIndex)); + if (tabProxy != null) { + tabProxy.fireEvent(TiC.EVENT_UNSELECTED, null, false); + } + } + currentlySelectedIndex = tabIndex; + + TabProxy tp = ((TabProxy) tabsArray.get(tabIndex)); + if (tp != null) { + TiUITab abstractTab = new TiUITab(tp); + + centerView.removeAllViews(); + TiUIView view = abstractTab.getWindowProxy().getOrCreateView(); + if (view != null) { + centerView.addView(view.getOuterView()); + } + } + updateIconTint(); + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) + { + item.setChecked(true); + selectTab(item.getItemId()); + ((TabGroupProxy) getProxy()).onTabSelected(item.getItemId()); + return true; + } +} diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigationTabGroup.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigationTabGroup.java index 1ead9f66615..5ad0beecea9 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigationTabGroup.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigationTabGroup.java @@ -362,6 +362,12 @@ public void updateTabBackgroundDrawable(int index) } } + @Override + public void updateActiveIndicatorColor(int color) + { + + } + @Override public void updateTabTitle(int index) { @@ -514,6 +520,14 @@ public String getTabTitle(int index) return this.mBottomNavigationView.getMenu().getItem(index).getTitle().toString(); } + @Override + public void setEnabled(Boolean enabled) + { + for (int i = 0; i < this.mBottomNavigationView.getMenu().size(); i++) { + this.mBottomNavigationView.getMenu().getItem(i).setEnabled(enabled); + } + } + /** * After a menu item is clicked this method sends the proper index to the ViewPager to a select * a page. Also takes care of sending SELECTED/UNSELECTED events from the proper tabs. diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUITabLayoutTabGroup.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUITabLayoutTabGroup.java index 97d81da263e..15dd572bcdf 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUITabLayoutTabGroup.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUITabLayoutTabGroup.java @@ -235,6 +235,12 @@ public void updateTabBackgroundDrawable(int index) this.mTabLayout.setBackground(backgroundDrawable); } + @Override + public void updateActiveIndicatorColor(int color) + { + + } + @Override public void updateTabTitle(int index) { @@ -376,6 +382,12 @@ public String getTabTitle(int index) return this.mTabLayout.getTabAt(index).getText().toString(); } + @Override + public void setEnabled(Boolean enabled) + { + + } + /** * After a tab is selected send the index for the ViewPager to select the proper page. * diff --git a/android/titanium/src/java/org/appcelerator/titanium/util/TiIconDrawable.java b/android/titanium/src/java/org/appcelerator/titanium/util/TiIconDrawable.java new file mode 100644 index 00000000000..e8cbc6d96d4 --- /dev/null +++ b/android/titanium/src/java/org/appcelerator/titanium/util/TiIconDrawable.java @@ -0,0 +1,115 @@ +package org.appcelerator.titanium.util; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; + +import androidx.annotation.NonNull; + +import org.appcelerator.titanium.TiApplication; + +public class TiIconDrawable extends Drawable +{ + + Paint paint; + String icon; + int size = 64; + private int alpha = 255; + + public TiIconDrawable(String icon, String fontFamily) + { + this.icon = icon; + paint = new TextPaint(); + + Typeface typeface = TiUIHelper.toTypeface(TiApplication.getAppRootOrCurrentActivity(), fontFamily); + paint.setTypeface(typeface); + paint.setStyle(Paint.Style.FILL); + paint.setTextAlign(Paint.Align.CENTER); + paint.setUnderlineText(false); + paint.setColor(Color.WHITE); + paint.setAntiAlias(true); + } + + public static boolean isEnabled(int[] stateSet) + { + for (int state : stateSet) + if (state == android.R.attr.state_selected) + return true; + return false; + } + + @Override + public void draw(@NonNull Canvas canvas) + { + paint.setTextSize(getBounds().height()); + Rect textBounds = new Rect(); + String textValue = icon; + paint.getTextBounds(textValue, 0, 1, textBounds); + float textBottom = (getBounds().height() - textBounds.height()) / 2f + textBounds.height() - textBounds.bottom; + + canvas.drawText(textValue, getBounds().width() / 2f, textBottom, paint); + } + + @Override + public int getIntrinsicHeight() + { + return size; + } + + @Override + public int getIntrinsicWidth() + { + return size; + } + + @Override + public int getOpacity() + { + return PixelFormat.OPAQUE; + } + + @Override + public boolean isStateful() + { + return true; + } + + @Override + public void setAlpha(int alpha) + { + this.alpha = alpha; + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) + { + paint.setColorFilter(cf); + } + + @Override + public void clearColorFilter() + { + paint.setColorFilter(null); + } + + @Override + public boolean setState(int[] stateSet) + { + int oldValue = paint.getAlpha(); + int newValue = isEnabled(stateSet) ? alpha : alpha / 2; + paint.setAlpha(newValue); + return oldValue != newValue; + } + + public void setStyle(Paint.Style style) + { + paint.setStyle(style); + } +} diff --git a/apidoc/Titanium/UI/Android/Android.yml b/apidoc/Titanium/UI/Android/Android.yml index ef1be7069d2..b741f08e256 100644 --- a/apidoc/Titanium/UI/Android/Android.yml +++ b/apidoc/Titanium/UI/Android/Android.yml @@ -674,6 +674,12 @@ properties: type: Number permission: read-only + - name: STATUS_BAR_LIGHT + summary: Sets the color of the status bar to light mode. Needs Android API level 23. + type: Number + permission: read-only + since: "12.6.0" + - name: SWITCH_STYLE_CHECKBOX summary: Display a checkbox. description: Use with the property. diff --git a/apidoc/Titanium/UI/Tab.yml b/apidoc/Titanium/UI/Tab.yml index 82b2d91f613..d347601c856 100644 --- a/apidoc/Titanium/UI/Tab.yml +++ b/apidoc/Titanium/UI/Tab.yml @@ -253,7 +253,26 @@ properties: (https://developer.apple.com/ios/human-interface-guidelines/icons-and-images/image-size-and-resolution/), this image will be scaled to fit if used with activeIcon, and cropped at the bottom otherwise. + + #### Android Tab Icons + Can be used in combination with `iconFamily` in a BottomNavigation setup with `experimental:true`. + type: String + + - name: iconFamily + summary: Specifies the font family or specific font to use. + description: | + When using a BottomNavigation setup with `experimental:true` you can use this property in combination + with an `icon` to use an icon-font for the tab icons: + + ``` js + '#tab1': { + icon: "\ueac1", + iconFamily: "tabler-icons" + } + ``` + since: "12.7.0" type: String + platforms: [android] - name: iconInsets summary: The icon inset or outset for each edge. diff --git a/apidoc/Titanium/UI/TabGroup.yml b/apidoc/Titanium/UI/TabGroup.yml index 492f203906d..dd96d1305eb 100644 --- a/apidoc/Titanium/UI/TabGroup.yml +++ b/apidoc/Titanium/UI/TabGroup.yml @@ -26,14 +26,7 @@ description: | one to be opened later, but the root of the application must be a heavyweight window to prevent it exiting. Tabs cannot be removed from the tab group once added, and tabs cannot be reordered. - * If you use a BottomNavigation on Android with a Material 3 theme you'll have to create a file - `/app/platform/android/res/values/dimens.xml` and add to fix the height of the menu: - ```xml - - - 80dp - - ``` + * If you use a BottomNavigation on Android with a Material 3 theme you should set `experimental:true`. * On iOS, more than one tab group may exist, and may be opened and closed as required. Each tab can contain a stack of windows, and the user can switch between them by tapping the @@ -61,7 +54,7 @@ excludes: anchorPoint,animatedCenter,backgroundDisabledColor,backgroundDisabledImage, backgroundFocusedColor,backgroundFocusedImage,backgroundGradient,backgroundImage,backgroundLeftCap, backgroundRepeat,backgroundSelectedColor,backgroundSelectedImage,backgroundTopCap, - borderColor,borderRadius,borderWidth,bottom,children,enabled,focusable,height, + borderColor,borderRadius,borderWidth,bottom,children,focusable,height, horizontalWrap,layout,left,opacity,right,softKeyboardOnFocus, top,transform,width,zIndex] events: [click, dblclick, doubletap, keypressed, longclick, longpress, pinch, postlayout, @@ -296,6 +289,13 @@ properties: type: [String, Titanium.UI.Color] platforms: [iphone, ipad, android, macos] + - name: enabled + summary: | + Enables or disables the menu in a BottomNavigation. If disabled you can't change to a different tab. + type: Number + default: true + since: { android: "12.7.0" } + - name: paddingLeft summary: Left padding of bottom navigation description: | @@ -436,6 +436,12 @@ properties: since: "8.0.0" platforms: [android] + - name: statusBarColor + summary: The color of the status bar (top bar) for this window. + type: [Number] + platforms: [android] + since: {android: "12.7.0"} + - name: style summary: Property defining which style for the TabGroup to be used. description: | @@ -529,6 +535,15 @@ properties: default: true platforms: [iphone, ipad, macos] + - name: flags + summary: Additional flags to set on the TabGroup. + description: | + Use to set the status bar text color to light mode. + platforms: [android] + type: Number + since: "12.7.0" + availability: creation + - name: activeTintColor summary: The activeTintColor to apply to tabs. description: | @@ -628,6 +643,17 @@ properties: since: "12.5.0" availability: creation + - name: experimental + summary: | + Only used for a BottomNavigation setup. If set to `true` it will use an optimized BottomNavigation + setup with fixes for Material 3 layouts and new properties: `indicatorColor` and `iconFamily`. + The new BottomNavigation will only load the active Activity and doesn't support `swipeable`. + type: Boolean + default: false + platforms: [android] + since: "12.7.0" + availability: creation + examples: - title: Alloy XML Markup example: | @@ -712,6 +738,40 @@ examples: }); tabGroup.open(); ``` + + - title: BottomNavigation Material3 example + example: | + Create a BottomNavigation TabGroup with a Material3 theme using the new layout. + + ``` js + const win1 = Ti.UI.createWindow(); + const win2 = Ti.UI.createWindow(); + const tab1 = Ti.UI.createTab({ + window: win1, + title: 'Tab 1', + icon: '/images/appicon.png' + }); + const tab2 = Ti.UI.createTab({ + window: win2, + title: 'Tab 2', + icon: '/images/appicon.png' + }); + + win1.add(Ti.UI.createLabel({text: "Tab 1 - click to set badge"})); + win2.add(Ti.UI.createLabel({text: "Tab 2"})); + win1.addEventListener("click", function() { + tab1.badge = 20; + }); + + const bottomNav = Ti.UI.createTabGroup({ + tabs: [tab1, tab2], + theme: "Theme.Titanium.Material3.DayNight", + experimental: true, + style: Ti.UI.Android.TABS_STYLE_BOTTOM_NAVIGATION + }); + + bottomNav.open(); + ``` --- name: disableTabOptions summary: Dictionary of options for the method. diff --git a/apidoc/Titanium/UI/Window.yml b/apidoc/Titanium/UI/Window.yml index b17a8f5a30b..d1dc15d67da 100644 --- a/apidoc/Titanium/UI/Window.yml +++ b/apidoc/Titanium/UI/Window.yml @@ -1532,6 +1532,8 @@ properties: Setting to `true` automatically sets the [WindowManager.LayoutParams.FLAG_FULLSCREEN](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN) flag. Setting to true automatically sets the [WindowManager.LayoutParams.FLAG_SECURE](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE) flag. + + Use to set the status bar text color to light mode. platforms: [android] type: Number since: "3.3.0" diff --git a/tests/Resources/ti.ui.tabgroup.test.js b/tests/Resources/ti.ui.tabgroup.test.js index b71763356ab..099effdf585 100644 --- a/tests/Resources/ti.ui.tabgroup.test.js +++ b/tests/Resources/ti.ui.tabgroup.test.js @@ -652,6 +652,26 @@ describe('Titanium.UI.TabGroup', function () { tabGroup.open(); }); + it.android('icon-only tabs - android bottom style (experimental layout)', finish => { + this.timeout(5000); + tabGroup = Ti.UI.createTabGroup({ + style: Ti.UI.Android.TABS_STYLE_BOTTOM_NAVIGATION, + experimental: true, + tabs: [ + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 1' }) + }), + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 2' }) + }), + ] + }); + tabGroup.addEventListener('open', () => finish()); + tabGroup.open(); + }); + // Android only feature where setting the "padding*" properties on the bottom tab bar style // makes it look like a floating toolbar with rounded corners. describe('floating tab bar', () => { @@ -703,6 +723,57 @@ describe('Titanium.UI.TabGroup', function () { }); }); + describe('floating tab bar (experimental layout)', () => { + it.android('extendSafeArea - false (experimental layout)', finish => { + this.timeout(5000); + tabGroup = Ti.UI.createTabGroup({ + extendSafeArea: false, + paddingLeft: 15, + paddingRight: 15, + paddingBottom: 15, + experimental: true, + style: Ti.UI.Android.TABS_STYLE_BOTTOM_NAVIGATION, + tabs: [ + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 1' }) + }), + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 2' }) + }), + ] + }); + tabGroup.addEventListener('open', () => finish()); + tabGroup.open(); + }); + + it.android('extendSafeArea - true (experimental layout)', finish => { + this.timeout(5000); + tabGroup = Ti.UI.createTabGroup({ + extendSafeArea: true, + windowFlags: Ti.UI.Android.FLAG_TRANSLUCENT_STATUS | Ti.UI.Android.FLAG_TRANSLUCENT_NAVIGATION, + paddingLeft: 15, + paddingRight: 15, + paddingBottom: 15, + experimental: true, + style: Ti.UI.Android.TABS_STYLE_BOTTOM_NAVIGATION, + tabs: [ + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 1' }) + }), + Ti.UI.createTab({ + icon: '/SmallLogo.png', + window: Ti.UI.createWindow({ title: 'Tab 2' }) + }), + ] + }); + tabGroup.addEventListener('open', () => finish()); + tabGroup.open(); + }); + }); + describe('closed/focused', () => { beforeEach(() => { tabGroup = Ti.UI.createTabGroup();