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..1dee4f9dbbc 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 @@ -39,6 +39,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 +50,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 { @@ -323,6 +325,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 +384,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("newLayout"), 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) { 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..fb4afb02cb3 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 @@ -115,6 +115,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. * @@ -454,7 +461,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 +502,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 +526,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); } @@ -550,7 +562,6 @@ public Drawable updateIconTint(TiViewProxy tabProxy, Drawable drawable, boolean if (drawable == null) { return null; } - // Clone existing drawable so color filter applies correctly. drawable = drawable.getConstantState().newDrawable(); 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..9407681bfe0 --- /dev/null +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tabgroup/TiUIBottomNavigation.java @@ -0,0 +1,437 @@ +/** + * 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.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.MaterialShapeDrawable; + +import org.appcelerator.titanium.TiApplication; +import org.appcelerator.titanium.TiBaseActivity; +import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.proxy.TiViewProxy; +import org.appcelerator.titanium.util.TiConvert; +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 +{ + + 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 Object[] tabsArray; + protected final static String TAG = "TiUIBottomNavigation"; + + 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."); + } + } + + public void setTabs(Object tabs) + { + if (tabs instanceof Object[] objArray) { + tabsArray = objArray; + for (Object tabView : tabsArray) { + if (tabView instanceof TabProxy tp) { + MenuItem menuItem = bottomNavigation.getMenu().add(0, mMenuItemsArray.size(), 0, ""); + menuItem.setTitle(tp.getProperty(TiC.PROPERTY_TITLE).toString()); + Drawable drawable = TiUIHelper.getResourceDrawable(tp.getProperty(TiC.PROPERTY_ICON)); + menuItem.setIcon(drawable); + mMenuItemsArray.add(menuItem); + int index = this.mMenuItemsArray.size() - 1; + updateDrawablesAfterNewItem(index); + } + } + } + + } + + @Override + public void addViews(TiBaseActivity activity) + { + mMenuItemsArray = 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.hasProperty(TiC.PROPERTY_TABS)) { + setTabs(proxy.getProperty(TiC.PROPERTY_TABS)); + selectTab(0); + } + + } 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) + { + 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) + { + + } + + private void updateDrawablesAfterNewItem(int index) + { + updateTabTitle(index); + updateTabIcon(index); + updateBadge(index); + updateBadgeColor(index); + updateTabTitleColor(index); + updateTabBackgroundDrawable(index); + } + + @Override + public void setBackgroundColor(int colorInt) + { + // 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[index]); + 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); + } + } catch (Exception e) { + Log.w(TAG, WARNING_LAYOUT_MESSAGE); + } + } + + @Override + public void updateTabTitle(int index) + { + if ((index < 0) || (index >= this.tabs.size())) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray[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.length)) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray[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[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[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())) { + return; + } + + TiViewProxy tabProxy = ((TabProxy) tabsArray[index]); + if (tabProxy == null) { + return; + } + + final Drawable drawable = TiUIHelper.getResourceDrawable(tabProxy.getProperty(TiC.PROPERTY_ICON)); + this.bottomNavigation.getMenu().getItem(index).setIcon(drawable); + updateIconTint(); + } + + private void updateIconTint() + { + for (int i = 0; i < this.bottomNavigation.getMenu().size(); i++) { + TiViewProxy tabProxy = ((TabProxy) tabsArray[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 selectTab(int tabIndex) + { + super.selectTab(tabIndex); + currentlySelectedIndex = tabIndex; + + TabProxy tp = ((TabProxy) tabsArray[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..6cb337083d3 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) { 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..5039a67ee36 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) {