diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java index 6c58a363907..2ef8902b31f 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java @@ -42,9 +42,9 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.ShapeDrawable; import android.os.Build; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import android.text.TextUtils; import android.util.Pair; @@ -813,8 +813,6 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP boolean hasColorState = hasColorState(d); boolean hasBorder = hasBorder(d); boolean hasGradient = hasGradient(d); - boolean nativeViewNull = (nativeView == null); - boolean requiresCustomBackground = hasImage || hasColorState || hasBorder || hasGradient; // PROPERTY_BACKGROUND_REPEAT is implicitly passed as false though not used in JS. So check the truth value and proceed. @@ -830,16 +828,11 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP background = null; } - if (d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) { - Integer bgColor = TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR); - if (!nativeViewNull) { - if (canApplyTouchFeedback(d)) { - applyTouchFeedback(bgColor, d.containsKey(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR) - ? TiConvert.toColor(d, TiC.PROPERTY_TOUCH_FEEDBACK_COLOR) - : null); - } else { - nativeView.setBackgroundColor(bgColor); - } + if (this.nativeView != null) { + if (d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) { + this.nativeView.setBackgroundColor(TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR)); + } else { + this.nativeView.setBackground(null); } } } else { @@ -896,11 +889,15 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP applyCustomBackground(); } + if (canApplyTouchFeedback(d)) { + String colorString = TiConvert.toString(d.get(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR)); + applyTouchFeedback((colorString != null) ? TiConvert.toColor(colorString) : null); + } if (key.equals(TiC.PROPERTY_OPACITY)) { setOpacity(TiConvert.toFloat(newValue, 1f)); } - if (!nativeViewNull) { - nativeView.postInvalidate(); + if (this.nativeView != null) { + this.nativeView.postInvalidate(); } } else if (key.equals(TiC.PROPERTY_SOFT_KEYBOARD_ON_FOCUS)) { Log.w(TAG, @@ -1008,26 +1005,22 @@ public void processProperties(KrollDict d) handleBackgroundImage(d); } else if (d.containsKey(TiC.PROPERTY_BACKGROUND_COLOR) && !nativeViewNull) { + // Set the background color on the view directly only if there is no border. + // If border is present, then we must use the TiBackgroundDrawable. bgColor = TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR); - - if (canApplyTouchFeedback(d)) { - applyTouchFeedback(bgColor, d.containsKey(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR) - ? TiConvert.toColor(d, TiC.PROPERTY_TOUCH_FEEDBACK_COLOR) - : null); - } else { - // Set the background color on the view directly only - // if there is no border. If a border is present we must - // use the TiBackgroundDrawable. - if (hasBorder(d)) { - if (background == null) { - applyCustomBackground(false); - } - background.setBackgroundColor(bgColor); - } else { - nativeView.setBackgroundColor(bgColor); + if (hasBorder(d)) { + if (background == null) { + applyCustomBackground(false); } + background.setBackgroundColor(bgColor); + } else { + nativeView.setBackgroundColor(bgColor); } } + if (canApplyTouchFeedback(d)) { + String colorString = TiConvert.toString(d.get(TiC.PROPERTY_TOUCH_FEEDBACK_COLOR)); + applyTouchFeedback((colorString != null) ? TiConvert.toColor(colorString) : null); + } if (d.containsKey(TiC.PROPERTY_HIDDEN_BEHAVIOR) && !nativeViewNull) { Object hidden = d.get(TiC.PROPERTY_HIDDEN_BEHAVIOR); @@ -1188,23 +1181,51 @@ protected boolean canApplyTouchFeedback(@NonNull KrollDict props) /** * Applies touch feedback. Should check canApplyTouchFeedback() before calling this. - * @param backgroundColor The background color of the view. - * @param rippleColor The ripple color. + * @param rippleColor The ripple color to use. Set to null to use system's default ripple color. */ - private void applyTouchFeedback(@NonNull Integer backgroundColor, @Nullable Integer rippleColor) + private void applyTouchFeedback(Integer rippleColor) { + // Do not continue if there is no view to modify. + if (this.nativeView == null) { + return; + } + + // Fetch default ripple color if given null. if (rippleColor == null) { Context context = proxy.getActivity(); TypedValue attribute = new TypedValue(); if (context.getTheme().resolveAttribute(android.R.attr.colorControlHighlight, attribute, true)) { rippleColor = attribute.data; - } else { - throw new RuntimeException("android.R.attr.colorControlHighlight cannot be resolved into Drawable"); + } + if (rippleColor == null) { + Log.e(TAG, "android.R.attr.colorControlHighlight cannot be resolved into Drawable"); + return; } } - RippleDrawable rippleDrawable = - new RippleDrawable(ColorStateList.valueOf(rippleColor), new ColorDrawable(backgroundColor), null); - nativeView.setBackground(rippleDrawable); + + // Fetch the background drawable that we'll be applying the ripple effect to. + Drawable backgroundDrawable = this.background; + if (backgroundDrawable == null) { + backgroundDrawable = this.nativeView.getBackground(); + } + + // Create a mask if a background doesn't exist or if it's completely transparent. + // Note: Ripple effect won't work unless it has something opaque to draw to. Use mask as a fallback. + ShapeDrawable maskDrawable = null; + boolean isVisible = (backgroundDrawable != null); + if (backgroundDrawable instanceof ColorDrawable) { + int colorValue = ((ColorDrawable) backgroundDrawable).getColor(); + if (Color.alpha(colorValue) <= 0) { + isVisible = false; + } + } + if (!isVisible) { + maskDrawable = new ShapeDrawable(); + } + + // Replace view's existing background with ripple effect wrapping the old drawable. + nativeView.setBackground( + new RippleDrawable(ColorStateList.valueOf(rippleColor), backgroundDrawable, maskDrawable)); } @Override diff --git a/apidoc/Titanium/UI/View.yml b/apidoc/Titanium/UI/View.yml index 792d6679bc1..fdd0f4ad225 100644 --- a/apidoc/Titanium/UI/View.yml +++ b/apidoc/Titanium/UI/View.yml @@ -1769,8 +1769,11 @@ properties: - name: touchFeedback summary: A material design visual construct that provides an instantaneous visual confirmation of touch point. description: | - This is an opt-in feature available from Android Lollipop. - Touch feedback is applied only if the backgroundColor is a solid color. + Touch feedback is only applied to a view's background. It is never applied to the view's foreground content + such as a 's image. + + For Titanium versions older than 9.1.0, touch feedback only works if you set the + property to a non-transparent color. type: Boolean default: false platforms: [android] diff --git a/tests/Resources/ti.ui.view.addontest.js b/tests/Resources/ti.ui.view.addontest.js index 69f43073e4a..2b11045e600 100644 --- a/tests/Resources/ti.ui.view.addontest.js +++ b/tests/Resources/ti.ui.view.addontest.js @@ -1,6 +1,6 @@ /* * Appcelerator Titanium Mobile - * Copyright (c) 2015-Present by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2015-Present 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. */ @@ -174,4 +174,78 @@ describe('Titanium.UI.View', function () { win.open(); }); }); + + it.android('touchFeedback', finish => { + win = Ti.UI.createWindow({ layout: 'horizontal' }); + win.add(Ti.UI.createLabel({ + text: 'View 1', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 2', + backgroundColor: 'gray', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 3', + backgroundImage: '/Logo.png', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 4', + backgroundGradient: { + type: 'linear', + startPoint: { x: '0%', y: '50%' }, + endPoint: { x: '100%', y: '50%' }, + colors: [ { color: 'red', offset: 0.0 }, { color: 'blue', offset: 1.0 } ] + }, + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 5', + borderRadius: 20, + borderColor: 'red', + borderWidth: '8dp', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 6', + backgroundColor: 'gray', + borderRadius: 20, + borderColor: 'red', + borderWidth: '8dp', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 7', + backgroundImage: '/Logo.png', + borderRadius: 20, + borderColor: 'red', + borderWidth: '8dp', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.add(Ti.UI.createLabel({ + text: 'View 8', + backgroundGradient: { + type: 'linear', + startPoint: { x: '0%', y: '50%' }, + endPoint: { x: '100%', y: '50%' }, + colors: [ { color: 'red', offset: 0.0 }, { color: 'blue', offset: 1.0 } ] + }, + borderRadius: 20, + borderColor: 'red', + borderWidth: '8dp', + touchFeedback: true, + touchFeedbackColor: 'yellow' + })); + win.addEventListener('open', () => finish()); + win.open(); + }); });