From e3af429629ce8ccbd49dfc342408bf675f149d17 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Fri, 15 Jan 2021 23:04:48 -0800 Subject: [PATCH 1/2] feat(android): add Ti.UI.ButtonBar Fixes TIMOB-25954 --- .../modules/titanium/ui/ButtonBarProxy.java | 32 +++ .../titanium/ui/widget/TiUIButtonBar.java | 186 ++++++++++++++++++ apidoc/Titanium/UI/ButtonBar.yml | 45 ++--- tests/Resources/app.js | 1 + tests/Resources/ti.ui.buttonbar.test.js | 147 ++++++++++++++ 5 files changed, 382 insertions(+), 29 deletions(-) create mode 100644 android/modules/ui/src/java/ti/modules/titanium/ui/ButtonBarProxy.java create mode 100644 android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java create mode 100644 tests/Resources/ti.ui.buttonbar.test.js diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/ButtonBarProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/ButtonBarProxy.java new file mode 100644 index 00000000000..a289be6e868 --- /dev/null +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/ButtonBarProxy.java @@ -0,0 +1,32 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2021 by Axway, Inc. 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; + +import android.app.Activity; +import org.appcelerator.kroll.annotations.Kroll; +import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.proxy.TiViewProxy; +import org.appcelerator.titanium.view.TiUIView; +import ti.modules.titanium.ui.widget.TiUIButtonBar; + +@Kroll.proxy(creatableInModule = UIModule.class, propertyAccessors = { + TiC.PROPERTY_LABELS, +}) +public class ButtonBarProxy extends TiViewProxy +{ + @Override + public TiUIView createView(Activity activity) + { + return new TiUIButtonBar(this); + } + + @Override + public String getApiName() + { + return "Ti.UI.ButtonBar"; + } +} diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java new file mode 100644 index 00000000000..8bb5a4a58b7 --- /dev/null +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java @@ -0,0 +1,186 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2021 by Axway, Inc. 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; + +import android.graphics.drawable.Drawable; +import android.view.View; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.button.MaterialButtonToggleGroup; +import java.util.HashMap; +import org.appcelerator.kroll.KrollDict; +import org.appcelerator.kroll.KrollProxy; +import org.appcelerator.titanium.R; +import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.proxy.TiViewProxy; +import org.appcelerator.titanium.util.TiConvert; +import org.appcelerator.titanium.util.TiUIHelper; +import org.appcelerator.titanium.view.TiUIView; + +public class TiUIButtonBar extends TiUIView +{ + private static final String TAG = "TiUIButtonBar"; + + public TiUIButtonBar(TiViewProxy proxy) + { + super(proxy); + + // Create view group used to host buttons. + MaterialButtonToggleGroup buttonGroup = new MaterialButtonToggleGroup(proxy.getActivity()); + buttonGroup.setSelectionRequired(false); + buttonGroup.setSingleSelection(false); + buttonGroup.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange( + View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) + { + TiUIHelper.firePostLayoutEvent(getProxy()); + } + }); + setNativeView(buttonGroup); + } + + @Override + public void processProperties(KrollDict properties) + { + // Validate. + if (properties == null) { + return; + } + + // Apply given properties to view. + if (properties.containsKey(TiC.PROPERTY_LABELS)) { + processLabels(properties.get(TiC.PROPERTY_LABELS)); + } + + // Let base class handle all other view property settings. + super.processProperties(properties); + } + + @Override + public void propertyChanged(String key, Object oldValue, Object newValue, KrollProxy proxy) + { + // Validate. + if (key == null) { + return; + } + + // Handle property change. + if (key.equals(TiC.PROPERTY_LABELS)) { + processLabels(newValue); + } else { + super.propertyChanged(key, oldValue, newValue, proxy); + } + } + + private void processLabels(Object labels) + { + // Do not continue if proxy has been released. + if (this.proxy == null) { + return; + } + + // Fetch the button group view. + MaterialButtonToggleGroup buttonGroup = getButtonGroup(); + if (buttonGroup == null) { + return; + } + + // Clear the previously assigned buttons. + buttonGroup.removeAllViews(); + + // Fetch "labels" property and validate it. + if ((labels == null) || !labels.getClass().isArray()) { + return; + } + Object[] objectArray = (Object[]) labels; + if (objectArray.length <= 0) { + return; + } + + // Process the labels object. + if (objectArray[0] instanceof String) { + // We were given an array of button titles. + for (Object title : objectArray) { + addButton(TiConvert.toString(title, ""), null); + } + } else if (objectArray[0] instanceof HashMap) { + // We were given an array of Titanium "BarItemType" dictionaries. + for (Object nextObject : objectArray) { + // Make sure next element is a dictionary. + if ((nextObject instanceof HashMap) == false) { + continue; + } + HashMap hashMap = (HashMap) nextObject; + + // Fetch the optional "title" property. + String title = TiConvert.toString(hashMap.get(TiC.PROPERTY_TITLE), ""); + + // Fetch the optional "image" property and load it as a drawable. + Drawable imageDrawable = null; + Object imageObject = hashMap.get(TiC.PROPERTY_IMAGE); + if (imageObject != null) { + imageDrawable = TiUIHelper.getResourceDrawable(imageObject); + } + + // Add the button. + addButton(title, imageDrawable); + } + } + } + + private void addButton(String title, Drawable imageDrawable) + { + // Fetch the button group view. + MaterialButtonToggleGroup buttonGroup = getButtonGroup(); + if (buttonGroup == null) { + return; + } + + // Title must be non-null. + if (title == null) { + title = ""; + } + + // Create a button with given settings and add it to view group. + MaterialButton button = new MaterialButton(buttonGroup.getContext(), null, R.attr.materialButtonOutlinedStyle); + button.setText(title); + if (imageDrawable != null) { + button.setIcon(imageDrawable); + if (title.isEmpty()) { + button.setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START); + button.setIconPadding(0); + button.setPadding(0, 0, 0, 0); + } + } + buttonGroup.addView(button); + button.setCheckable(false); + button.setOnClickListener((view) -> { + if (this.proxy == null) { + return; + } + for (int index = 0; index < buttonGroup.getChildCount(); index++) { + View childView = buttonGroup.getChildAt(index); + if (childView == view) { + KrollDict data = new KrollDict(); + data.put(TiC.PROPERTY_INDEX, index); + this.proxy.fireEvent(TiC.EVENT_CLICK, data); + return; + } + } + }); + } + + private MaterialButtonToggleGroup getButtonGroup() + { + View view = getNativeView(); + if (view instanceof MaterialButtonToggleGroup) { + return (MaterialButtonToggleGroup) view; + } + return null; + } +} diff --git a/apidoc/Titanium/UI/ButtonBar.yml b/apidoc/Titanium/UI/ButtonBar.yml index cb8cf807c5d..d3d5335549d 100644 --- a/apidoc/Titanium/UI/ButtonBar.yml +++ b/apidoc/Titanium/UI/ButtonBar.yml @@ -2,30 +2,18 @@ name: Titanium.UI.ButtonBar summary: An iOS button bar component. description: | - The button bar is a set of buttons joined into a single control. Each button in a - button bar can have a text label or an icon, but not both. + The button bar is a set of buttons joined into a single control. + On iOS, you can set up the buttons with either a title or image, but not both. + On Android, you can set up the buttons with a title, image, or both. Use the method or **``** Alloy element to create a button bar. The [TabbedBar](Titanium.UI.iOS.TabbedBar) control is a button bar where the last selected button mantains a pressed or selected state. The following discussion applies to both button bar and tabbed bar. - - The buttons share a common style, defined by the `style` property. This can be - set to one of the constants defined in - [Titanium.UI.iOS.SystemButtonStyle](Titanium.UI.iOS.SystemButtonStyle): - - * `PLAIN`. Default style for `ButtonBar` and `TabbedBar`. - * `BORDERED`. Creates a bar like the `PLAIN` bar, but with a heavier border. - - Note that you cannot style individual buttons in a button bar. If you want to give a - distinct visual style to individual buttons, to use an icon and text on the same button, - or to use a button-bar type component on a platform other than iOS, you can use a set - of individual [Button](Titanium.UI.Button) controls wrapped in a - [View](Titanium.UI.View) to create the appearance of a button bar. extends: Titanium.UI.View -since: "0.8" -platforms: [iphone, ipad, macos] +since: {android: "10.0.0", iphone: "0.8", ipad: "0.8", macos: "9.2.0"} +platforms: [android, iphone, ipad, macos] excludes: events: [ 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'dblclick', 'doubletap', 'longclick', 'singletap', 'swipe', 'twofingertap' ] @@ -37,7 +25,7 @@ events: summary: Fired when a button is clicked. properties: - name: index - summary: Index of the clicked button. + summary: Index of the clicked button. type: Number properties: - name: index @@ -60,7 +48,7 @@ properties: ``` xml - + @@ -106,15 +94,15 @@ examples: - title: Simple 3 button button bar example: | ``` js - var bb1 = Titanium.UI.createButtonBar({ - labels:['One', 'Two', 'Three'], - backgroundColor:'#336699', - top:50, - style:Titanium.UI.iOS.SystemButtonStyle.PLAIN, - height:25, - width:200 + const win = Ti.UI.createWindow(); + const buttonBar = Titanium.UI.createButtonBar({ + labels:['One', 'Two', 'Three'] + }); + buttonBar.addEventListener('click', (e) => { + console.log(`Clicked on button index: ${e.index}`); }); - win.add(bb1); + win.add(buttonBar); + win.open(); ``` - title: Alloy XML Markup @@ -124,7 +112,7 @@ examples: ``` xml - + @@ -133,7 +121,6 @@ examples: - diff --git a/tests/Resources/app.js b/tests/Resources/app.js index aef24c0acec..2cb6e7adc58 100644 --- a/tests/Resources/app.js +++ b/tests/Resources/app.js @@ -151,6 +151,7 @@ function loadTests() { } require('./ti.ui.attributedstring.test'); require('./ti.ui.button.test'); + require('./ti.ui.buttonbar.test'); require('./ti.ui.clipboard.test'); require('./ti.ui.constants.test'); require('./ti.ui.emaildialog.test'); diff --git a/tests/Resources/ti.ui.buttonbar.test.js b/tests/Resources/ti.ui.buttonbar.test.js new file mode 100644 index 00000000000..4dbb9d18304 --- /dev/null +++ b/tests/Resources/ti.ui.buttonbar.test.js @@ -0,0 +1,147 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2021-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +const should = require('./utilities/assertions'); + +describe.windowsMissing('Titanium.UI.ButtonBar', function () { + let win; + this.timeout(5000); + + beforeEach(() => { + win = Ti.UI.createWindow(); + }); + + afterEach(done => { // fires after every test in sub-suites too... + if (win && !win.closed) { + win.addEventListener('close', function listener () { + win.removeEventListener('close', listener); + win = null; + done(); + }); + win.close(); + } else { + win = null; + done(); + } + }); + + describe('properties', () => { + describe('.apiName', () => { + it('is a read-only String', () => { + const buttonBar = Ti.UI.createButtonBar(); + should(buttonBar).have.readOnlyProperty('apiName').which.is.a.String(); + }); + + it('equals Ti.UI.ButtonBar', () => { + const buttonBar = Ti.UI.createButtonBar(); + should(buttonBar.apiName).be.eql('Ti.UI.ButtonBar'); + }); + }); + + describe('.labels', () => { + it('from string[]', finish => { + const buttonBar = Ti.UI.createButtonBar({ + labels: [ 'A', 'B', 'C' ] + }); + function postlayout() { + buttonBar.removeEventListener('postlayout', postlayout); + finish(); + } + buttonBar.addEventListener('postlayout', postlayout); + win.add(buttonBar); + win.open(); + }); + + it('from BarItemType[] with titles', finish => { + const buttonBar = Ti.UI.createButtonBar({ + labels: [ + { title: 'A' }, + { title: 'B' }, + { title: 'C' } + ] + }); + function postlayout() { + buttonBar.removeEventListener('postlayout', postlayout); + finish(); + } + buttonBar.addEventListener('postlayout', postlayout); + win.add(buttonBar); + win.open(); + }); + + it('from BarItemType[] with images', finish => { + const buttonBar = Ti.UI.createButtonBar({ + labels: [ + { image: '/SmallLogo.png' }, + { image: '/SmallLogo.png' }, + { image: '/SmallLogo.png' } + ] + }); + function postlayout() { + buttonBar.removeEventListener('postlayout', postlayout); + finish(); + } + buttonBar.addEventListener('postlayout', postlayout); + win.add(buttonBar); + win.open(); + }); + + it('from BarItemType[] with images and titles', finish => { + const buttonBar = Ti.UI.createButtonBar({ + labels: [ + { title: 'A', image: '/SmallLogo.png' }, + { title: 'B', image: '/SmallLogo.png' }, + { title: 'C', image: '/SmallLogo.png' } + ] + }); + function postlayout() { + buttonBar.removeEventListener('postlayout', postlayout); + finish(); + } + buttonBar.addEventListener('postlayout', postlayout); + win.add(buttonBar); + win.open(); + }); + + it('update', finish => { + const buttonBar = Ti.UI.createButtonBar({ + labels: [ 'A', 'B', 'C' ] + }); + function postlayout() { + buttonBar.removeEventListener('postlayout', postlayout); + try { + buttonBar.labels = [ 'D', 'E', 'F' ]; + should(buttonBar.labels[1]).be.eql('E'); + } catch (err) { + return finish(err); + } + finish(); + } + buttonBar.addEventListener('postlayout', postlayout); + win.add(buttonBar); + win.open(); + }); + + it('update - before window.open()', finish => { + const buttonBar = Ti.UI.createButtonBar(); + buttonBar.labels = [ 'A', 'B', 'C' ]; + win.add(buttonBar); + win.addEventListener('open', () => { + try { + should(buttonBar.labels[1]).be.eql('B'); + } catch (err) { + return finish(err); + } + finish(); + }); + win.open(); + }); + }); + }); +}); From 59ab40a9baffec4f5dfa1e13012a775abc3b01e1 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Thu, 18 Feb 2021 22:48:30 -0800 Subject: [PATCH 2/2] chore(android): improve Ti.UI.ButtonBar icon-only style --- ...anium_ui_outlinedbutton_icononly_style.xml | 13 ++++++++ .../titanium/ui/widget/TiUIButtonBar.java | 31 +++++++++++++------ 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 android/modules/ui/res/values/titanium_ui_outlinedbutton_icononly_style.xml diff --git a/android/modules/ui/res/values/titanium_ui_outlinedbutton_icononly_style.xml b/android/modules/ui/res/values/titanium_ui_outlinedbutton_icononly_style.xml new file mode 100644 index 00000000000..141754c2abc --- /dev/null +++ b/android/modules/ui/res/values/titanium_ui_outlinedbutton_icononly_style.xml @@ -0,0 +1,13 @@ + + + + diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java index 8bb5a4a58b7..444d0f78934 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButtonBar.java @@ -6,8 +6,10 @@ */ package ti.modules.titanium.ui.widget; +import android.content.Context; import android.graphics.drawable.Drawable; import android.view.View; +import androidx.appcompat.view.ContextThemeWrapper; import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButtonToggleGroup; import java.util.HashMap; @@ -106,7 +108,7 @@ private void processLabels(Object labels) if (objectArray[0] instanceof String) { // We were given an array of button titles. for (Object title : objectArray) { - addButton(TiConvert.toString(title, ""), null); + addButton(TiConvert.toString(title, ""), null, null, true); } } else if (objectArray[0] instanceof HashMap) { // We were given an array of Titanium "BarItemType" dictionaries. @@ -120,6 +122,9 @@ private void processLabels(Object labels) // Fetch the optional "title" property. String title = TiConvert.toString(hashMap.get(TiC.PROPERTY_TITLE), ""); + // Fetch the optional "accessibilityLabel" property. + String accessibilityLabel = TiConvert.toString(hashMap.get(TiC.PROPERTY_ACCESSIBILITY_LABEL), null); + // Fetch the optional "image" property and load it as a drawable. Drawable imageDrawable = null; Object imageObject = hashMap.get(TiC.PROPERTY_IMAGE); @@ -127,13 +132,16 @@ private void processLabels(Object labels) imageDrawable = TiUIHelper.getResourceDrawable(imageObject); } + // Fetch the optional "enabled" flag. + boolean isEnabled = TiConvert.toBoolean(hashMap.get(TiC.PROPERTY_ENABLED), true); + // Add the button. - addButton(title, imageDrawable); + addButton(title, accessibilityLabel, imageDrawable, isEnabled); } } } - private void addButton(String title, Drawable imageDrawable) + private void addButton(String title, String accessibilityLabel, Drawable imageDrawable, boolean isEnabled) { // Fetch the button group view. MaterialButtonToggleGroup buttonGroup = getButtonGroup(); @@ -147,18 +155,23 @@ private void addButton(String title, Drawable imageDrawable) } // Create a button with given settings and add it to view group. - MaterialButton button = new MaterialButton(buttonGroup.getContext(), null, R.attr.materialButtonOutlinedStyle); + Context context = buttonGroup.getContext(); + int attributeId = R.attr.materialButtonOutlinedStyle; + if (title.isEmpty() && (imageDrawable != null)) { + context = new ContextThemeWrapper(context, R.style.Widget_Titanium_OutlinedButton_IconOnly); + attributeId = R.attr.materialButtonToggleGroupStyle; + } + MaterialButton button = new MaterialButton(context, null, attributeId); button.setText(title); + if ((accessibilityLabel != null) && !accessibilityLabel.isEmpty()) { + button.setContentDescription(accessibilityLabel); + } if (imageDrawable != null) { button.setIcon(imageDrawable); - if (title.isEmpty()) { - button.setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START); - button.setIconPadding(0); - button.setPadding(0, 0, 0, 0); - } } buttonGroup.addView(button); button.setCheckable(false); + button.setEnabled(isEnabled); button.setOnClickListener((view) -> { if (this.proxy == null) { return;