Skip to content

Commit

Permalink
feat: add "imageIsMask" property to Ti.UI.Button (#13125)
Browse files Browse the repository at this point in the history
* feat: add "imageIsMask" property to Ti.UI.Button

Fixes TIMOB-28558

* docs(android): indicate Ti.UI.Button "image" can be set to resource ID

Fixes TIMOB-28559

Co-authored-by: Gary Mathews <contact@garymathews.com>
  • Loading branch information
jquick-axway and garymathews committed Oct 19, 2021
1 parent c76ee86 commit 0b82834
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
TiC.PROPERTY_ENABLED,
TiC.PROPERTY_FONT,
TiC.PROPERTY_IMAGE,
TiC.PROPERTY_IMAGE_IS_MASK,
TiC.PROPERTY_TEXT_ALIGN,
TiC.PROPERTY_VERTICAL_ALIGN,
TiC.PROPERTY_SHADOW_OFFSET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.R;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiBlob;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.proxy.TiViewProxy;
import org.appcelerator.titanium.util.TiConvert;
Expand Down Expand Up @@ -140,20 +139,7 @@ public void processProperties(KrollDict d)
boolean needShadow = false;

AppCompatButton btn = (AppCompatButton) getNativeView();
if (d.containsKey(TiC.PROPERTY_IMAGE)) {
Object value = d.get(TiC.PROPERTY_IMAGE);
TiDrawableReference drawableRef = null;
if (value instanceof String) {
drawableRef = TiDrawableReference.fromUrl(proxy, (String) value);
} else if (value instanceof TiBlob) {
drawableRef = TiDrawableReference.fromBlob(proxy.getActivity(), (TiBlob) value);
}

if (drawableRef != null) {
Drawable image = drawableRef.getDensityScaledDrawable();
btn.setCompoundDrawablesWithIntrinsicBounds(image, null, null, null);
}
} else if (d.containsKey(TiC.PROPERTY_BACKGROUND_COLOR)) {
if (!d.containsKey(TiC.PROPERTY_IMAGE) && d.containsKey(TiC.PROPERTY_BACKGROUND_COLOR)) {
// Reset the padding here if the background color is set. By default the padding will be calculated
// for the button, but if we set a background color, it will not look centered unless we reset the padding.
btn.setPadding(8, 0, 8, 0);
Expand All @@ -179,13 +165,12 @@ public void processProperties(KrollDict d)
setAttributedStringText((AttributedStringProxy) attributedString);
}
}
if (d.containsKey(TiC.PROPERTY_COLOR)) {
Object color = d.get(TiC.PROPERTY_COLOR);
if (color == null) {
btn.setTextColor(defaultColor);
} else {
btn.setTextColor(TiConvert.toColor(d, TiC.PROPERTY_COLOR));
if (d.containsKey(TiC.PROPERTY_COLOR) || d.containsKey(TiC.PROPERTY_TINT_COLOR)) {
String colorString = TiConvert.toString(d.get(TiC.PROPERTY_COLOR));
if (colorString == null) {
colorString = TiConvert.toString(d.get(TiC.PROPERTY_TINT_COLOR));
}
btn.setTextColor((colorString != null) ? TiConvert.toColor(colorString) : this.defaultColor);
}
if (d.containsKey(TiC.PROPERTY_FONT)) {
TiUIHelper.styleText(btn, d.getKrollDict(TiC.PROPERTY_FONT));
Expand Down Expand Up @@ -215,17 +200,10 @@ public void processProperties(KrollDict d)
needShadow = true;
shadowColor = TiConvert.toColor(d, TiC.PROPERTY_SHADOW_COLOR);
}
if (d.containsKey(TiC.PROPERTY_TINT_COLOR)) {
Object color = d.get(TiC.PROPERTY_TINT_COLOR);
if (color == null) {
btn.getBackground().clearColorFilter();
} else {
btn.getBackground().setColorFilter(TiConvert.toColor(d, TiC.PROPERTY_TINT_COLOR), Mode.MULTIPLY);
}
}
if (needShadow) {
btn.setShadowLayer(shadowRadius, shadowX, shadowY, shadowColor);
}
updateButtonImage();
btn.invalidate();
}

Expand All @@ -242,7 +220,8 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP
} else if (key.equals(TiC.PROPERTY_ATTRIBUTED_STRING) && newValue instanceof AttributedStringProxy) {
setAttributedStringText((AttributedStringProxy) newValue);
} else if (key.equals(TiC.PROPERTY_COLOR)) {
btn.setTextColor(TiConvert.toColor(TiConvert.toString(newValue)));
String colorString = TiConvert.toString(newValue);
btn.setTextColor((colorString != null) ? TiConvert.toColor(colorString) : this.defaultColor);
} else if (key.equals(TiC.PROPERTY_FONT)) {
TiUIHelper.styleText(btn, (HashMap) newValue);
} else if (key.equals(TiC.PROPERTY_TEXT_ALIGN)) {
Expand All @@ -251,17 +230,14 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP
} else if (key.equals(TiC.PROPERTY_VERTICAL_ALIGN)) {
TiUIHelper.setAlignment(btn, null, TiConvert.toString(newValue));
btn.requestLayout();
} else if (key.equals(TiC.PROPERTY_IMAGE)) {
TiDrawableReference drawableRef = null;
if (newValue instanceof String) {
drawableRef = TiDrawableReference.fromUrl(proxy, (String) newValue);
} else if (newValue instanceof TiBlob) {
drawableRef = TiDrawableReference.fromBlob(proxy.getActivity(), (TiBlob) newValue);
}
if (drawableRef != null) {
Drawable image = drawableRef.getDrawable();
btn.setCompoundDrawablesWithIntrinsicBounds(image, null, null, null);
} else if (key.equals(TiC.PROPERTY_IMAGE)
|| key.equals(TiC.PROPERTY_IMAGE_IS_MASK)
|| key.equals(TiC.PROPERTY_TINT_COLOR)) {
if (!proxy.hasProperty(TiC.PROPERTY_COLOR) && key.equals(TiC.PROPERTY_TINT_COLOR)) {
String colorString = TiConvert.toString(newValue);
btn.setTextColor((colorString != null) ? TiConvert.toColor(colorString) : this.defaultColor);
}
updateButtonImage();
} else if ((btn instanceof MaterialButton)
&& (key.equals(TiC.PROPERTY_TOUCH_FEEDBACK) || key.equals(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR))) {
// Only override MaterialButton's native ripple effect if "touchFeedback" property is defined.
Expand Down Expand Up @@ -290,12 +266,6 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP
} else if (key.equals(TiC.PROPERTY_SHADOW_COLOR)) {
shadowColor = TiConvert.toColor(TiConvert.toString(newValue));
btn.setShadowLayer(shadowRadius, shadowX, shadowY, shadowColor);
} else if (key.equals(TiC.PROPERTY_TINT_COLOR)) {
if (newValue == null) {
btn.getBackground().clearColorFilter();
} else {
btn.getBackground().setColorFilter(TiConvert.toColor(TiConvert.toString(newValue)), Mode.MULTIPLY);
}
} else {
super.propertyChanged(key, oldValue, newValue, proxy);
}
Expand Down Expand Up @@ -326,4 +296,51 @@ private void setAttributedStringText(AttributedStringProxy attrString)

btn.setText(text);
}

private void updateButtonImage()
{
// Do not continue if proxy has been released.
if (this.proxy == null) {
return;
}

// Fetch the button view.
AppCompatButton button = (AppCompatButton) getNativeView();
if (button == null) {
return;
}

// Fetch the image.
TiDrawableReference drawableRef = null;
Object imageObject = this.proxy.getProperty(TiC.PROPERTY_IMAGE);
if (imageObject != null) {
drawableRef = TiDrawableReference.fromObject(this.proxy.getActivity(), imageObject);
}

// Update button's image/icon.
if (drawableRef != null) {
boolean imageIsMask = TiConvert.toBoolean(this.proxy.getProperty(TiC.PROPERTY_IMAGE_IS_MASK), true);
String colorString = TiConvert.toString(this.proxy.getProperty(TiC.PROPERTY_TINT_COLOR));
int colorValue = (colorString != null) ? TiConvert.toColor(colorString) : this.defaultColor;
Drawable drawable = drawableRef.getDensityScaledDrawable();
if (button instanceof MaterialButton) {
MaterialButton materialButton = (MaterialButton) button;
materialButton.setIcon(drawable);
materialButton.setIconTintMode(imageIsMask ? Mode.SRC_IN : Mode.DST);
materialButton.setIconTint(ColorStateList.valueOf(colorValue));
} else {
if (imageIsMask) {
drawable = drawable.mutate();
drawable.setColorFilter(colorValue, Mode.SRC_IN);
}
button.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null);
}
} else {
if (button instanceof MaterialButton) {
((MaterialButton) button).setIcon(null);
} else {
button.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ public class TiC
public static final String PROPERTY_ICONIFIED_BY_DEFAULT = "iconifiedByDefault";
public static final String PROPERTY_ID = "id";
public static final String PROPERTY_IMAGE = "image";
public static final String PROPERTY_IMAGE_IS_MASK = "imageIsMask";
public static final String PROPERTY_IMAGE_TOUCH_FEEDBACK = "imageTouchFeedback";
public static final String PROPERTY_IMAGE_TOUCH_FEEDBACK_COLOR = "imageTouchFeedbackColor";
public static final String PROPERTY_IMAGES = "images";
Expand Down
26 changes: 20 additions & 6 deletions apidoc/Titanium/UI/Button.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,21 @@ properties:
type: Font

- name: image
summary: Image to display on the button, specified as a local
path, URL or a `Blob`.
summary: Image to display next to the button title.
description: |
The image is displayed to the left of the title.
As of Titanium 10.2.0, the [imageIsMask](Titanium.UI.Button.imageIsMask) property determines how
this image is displayed. If set `true`, this image will be tinted using the color assigned to the
[tintColor](Titanium.UI.Button.tintColor) property. If set `false`, the image is displayed as-is.
Support for using <Titanium.Blob> for this property is only available on Android and iOS.
type: [String, Titanium.Blob]
On Android, you can set this to a numeric resource drawable ID via `Ti.App.Android.R.drawable.*`
which lets you use native vector drawbles that are commonly used as icons.
type: [String, Number, Titanium.Blob]

- name: imageIsMask
summary: Set true to tint the button image. Set false to show the image as-is.
type: Boolean
default: true
since: "10.2.0"

- name: selectedColor
summary: Button text color used to indicate the selected state, as a color name or hex triplet.
Expand Down Expand Up @@ -325,7 +333,13 @@ properties:
default: <Titanium.UI.TEXT_ALIGNMENT_CENTER>

- name: tintColor
summary: Button tint color.
summary: Color applied to button's image and title.
description: |
Color to be applied to the button's [image](Titanium.UI.Button.image), but only if the
[imageIsMask](Titanium.UI.Button.imageIsMask) property is set `true`.
This tint color is also applied to the button's [title](Titanium.UI.Button.title)
unless the [color](Titanium.UI.Button.color) property is set.
type: String
platforms: [android,iphone,ipad, macos]
osver: {ios: {min: "7.0"}}
Expand Down
22 changes: 22 additions & 0 deletions iphone/Classes/TiUIButton.m
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,35 @@ - (void)setImage_:(id)value
{
UIImage *image = value == nil ? nil : [TiUtils image:value proxy:(TiProxy *)self.proxy];
if (image != nil) {
if (![TiUtils boolValue:[[self proxy] valueForKey:@"imageIsMask"] def:YES]) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}
[[self button] setImage:image forState:UIControlStateNormal];
[(TiViewProxy *)[self proxy] contentsWillChange];
} else {
[[self button] setImage:nil forState:UIControlStateNormal];
}
}

- (void)setImageIsMask_:(id)value
{
UIImage *image = [self button].currentImage;
if (!image) {
return;
}

BOOL imageIsMask = [TiUtils boolValue:value];
if (imageIsMask && (image.renderingMode != UIImageRenderingModeAlwaysTemplate)) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
} else if (!imageIsMask && (image.renderingMode != UIImageRenderingModeAlwaysOriginal)) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}
if (image != [self button].currentImage) {
[[self button] setImage:image forState:UIControlStateNormal];
[(TiViewProxy *)[self proxy] contentsWillChange];
}
}

- (void)setEnabled_:(id)value
{
[[self button] setEnabled:[TiUtils boolValue:value]];
Expand Down
69 changes: 69 additions & 0 deletions tests/Resources/ti.ui.button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,75 @@ describe('Titanium.UI.Button', function () {
win.open();
});

describe('imageIsMask', () => {
it('true', function (finish) {
this.slow(1000);
this.timeout(5000);

win = Ti.UI.createWindow();
const button = Ti.UI.createButton({
title: 'Button',
image: 'Logo.png',
imageIsMask: true,
});
win.add(button);
win.addEventListener('open', () => {
try {
should(button.imageIsMask).be.true();
finish();
} catch (err) {
finish(err);
}
});
win.open();
});

it('false', function (finish) {
this.slow(1000);
this.timeout(5000);

win = Ti.UI.createWindow();
const button = Ti.UI.createButton({
title: 'Button',
image: 'Logo.png',
imageIsMask: false,
});
win.add(button);
win.addEventListener('open', () => {
try {
should(button.imageIsMask).be.false();
finish();
} catch (err) {
finish(err);
}
});
win.open();
});
});

it('tintColor', function (finish) {
this.slow(1000);
this.timeout(5000);

win = Ti.UI.createWindow();
const button = Ti.UI.createButton({
title: 'Button',
image: 'Logo.png',
imageIsMask: true,
tintColor: 'red',
});
win.add(button);
win.addEventListener('open', () => {
try {
should(button.tintColor).be.eql('red');
finish();
} catch (err) {
finish(err);
}
});
win.open();
});

// FIXME Get working on iOS and Android. borderColor defaults to undefined there, we're verifying it's a String
it.androidAndIosBroken('backgroundColor/Image', function (finish) {
var view;
Expand Down

0 comments on commit 0b82834

Please sign in to comment.