Modify TouchableNativeFeedback (Ripple) to follow borderRadius
Summary:Using TouchableNativeFeedback has been a problem for me because the ripples it makes don't follow the child view's border radii so the ripples stick out of the child view's rounded corners. This PR should fix this problem with a minor caveat: this only works for TouchableNativeFeedback.Ripple and not TouchableNativeFeedback.SelectableBackground. I searched how I could apply corner radius to selectableItemBackground and it doesn't seem to be possible (the prevalent advice is to create the ripple manually which is equivalent to using TNF.Ripple in our case), though I could be wrong. I added [an example to UIExplorer (TouchableExample)](http://i.imgur.com/CHY9xjW.png). This is my first PR to this repo so let me know if something's wrong. Cheers! Closes https://github.com/facebook/react-native/pull/6515 Differential Revision: D3126513 Pulled By: mkonicek fb-gh-sync-id: 1d3e92243abf9706132ae47c485d9e04a9b47d81 fbshipit-source-id: 1d3e92243abf9706132ae47c485d9e04a9b47d81
This commit is contained in:
parent
44d36f7c5f
commit
e438954938
|
@ -110,6 +110,14 @@ exports.examples = [
|
|||
render: function(): ReactElement {
|
||||
return <TouchableDisabled />;
|
||||
},
|
||||
}, {
|
||||
title: 'TouchableNativeFeedback with Border Radius',
|
||||
description: 'The ripples from <TouchableNativeFeedback> should follow borderRadius. ' +
|
||||
'This only works with a non-borderless background from TouchableNativeFeedback.Ripple',
|
||||
platform: 'android',
|
||||
render: function(): ReactElement {
|
||||
return <TouchableNativeFeedbackBorderRadius />;
|
||||
},
|
||||
}];
|
||||
|
||||
var TextOnPressBox = React.createClass({
|
||||
|
@ -368,6 +376,25 @@ var TouchableDisabled = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var TouchableNativeFeedbackBorderRadius = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<TouchableNativeFeedback
|
||||
style={[styles.row, styles.block]}
|
||||
onPress={() => console.log('TouchableNativeFeedback Ripple with Border Radius has been clicked')}
|
||||
background={TouchableNativeFeedback.Ripple('#ccc')}>
|
||||
<View style={[styles.tnfBorderRadiusView]}>
|
||||
<Text style={[styles.button, styles.nativeFeedbackButton]}>
|
||||
TouchableNativeFeedback.Ripple with Border Radius
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'};
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
@ -441,4 +468,10 @@ var styles = StyleSheet.create({
|
|||
fontWeight: '500',
|
||||
color: 'blue',
|
||||
},
|
||||
tnfBorderRadiusView: {
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 5,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 20
|
||||
}
|
||||
});
|
||||
|
|
|
@ -92,6 +92,9 @@ var TouchableNativeFeedback = React.createClass({
|
|||
/**
|
||||
* Creates an object that represents android theme's default background for
|
||||
* selectable elements (?android:attr/selectableItemBackground).
|
||||
*
|
||||
* The backgrounds generated by this method DO NOT follow the child view's
|
||||
* border radii styles. To clip the background with border radii, use Ripple.
|
||||
*/
|
||||
SelectableBackground: function() {
|
||||
return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'};
|
||||
|
@ -111,6 +114,9 @@ var TouchableNativeFeedback = React.createClass({
|
|||
* example of that behavior). This background type is available on Android
|
||||
* API level 21+.
|
||||
*
|
||||
* The non-borderless backgrounds generated by this method follows the child
|
||||
* view's border radii styles (e.g. the ripples are clipped by the border radii).
|
||||
*
|
||||
* @param color The ripple color
|
||||
* @param borderless If the ripple can render outside it's bounds
|
||||
*/
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
|
||||
package com.facebook.react.views.view;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.PaintDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build;
|
||||
|
@ -34,6 +36,13 @@ public class ReactDrawableHelper {
|
|||
public static Drawable createDrawableFromJSDescription(
|
||||
Context context,
|
||||
ReadableMap drawableDescriptionDict) {
|
||||
return createDrawableFromJSDescription(context, drawableDescriptionDict, null);
|
||||
}
|
||||
|
||||
public static Drawable createDrawableFromJSDescription(
|
||||
Context context,
|
||||
ReadableMap drawableDescriptionDict,
|
||||
@Nullable float[] cornerRadii) {
|
||||
String type = drawableDescriptionDict.getString("type");
|
||||
if ("ThemeAttrAndroid".equals(type)) {
|
||||
String attr = drawableDescriptionDict.getString("attribute");
|
||||
|
@ -75,11 +84,14 @@ public class ReactDrawableHelper {
|
|||
"couldn't be resolved into a drawable");
|
||||
}
|
||||
}
|
||||
Drawable mask = null;
|
||||
PaintDrawable mask = null;
|
||||
if (!drawableDescriptionDict.hasKey("borderless") ||
|
||||
drawableDescriptionDict.isNull("borderless") ||
|
||||
!drawableDescriptionDict.getBoolean("borderless")) {
|
||||
mask = new ColorDrawable(Color.WHITE);
|
||||
mask = new PaintDrawable(Color.WHITE);
|
||||
if (cornerRadii != null) {
|
||||
mask.setCornerRadii(cornerRadii);
|
||||
}
|
||||
}
|
||||
ColorStateList colorStateList = new ColorStateList(
|
||||
new int[][] {new int[]{}},
|
||||
|
|
|
@ -236,6 +236,25 @@ import com.facebook.csslayout.Spacing;
|
|||
}
|
||||
}
|
||||
|
||||
/* package */ float[] getBorderRadii() {
|
||||
float defaultBorderRadius = !CSSConstants.isUndefined(mBorderRadius) ? mBorderRadius : 0;
|
||||
float topLeftRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[0]) ? mBorderCornerRadii[0] : defaultBorderRadius;
|
||||
float topRightRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[1]) ? mBorderCornerRadii[1] : defaultBorderRadius;
|
||||
float bottomRightRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[2]) ? mBorderCornerRadii[2] : defaultBorderRadius;
|
||||
float bottomLeftRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[3]) ? mBorderCornerRadii[3] : defaultBorderRadius;
|
||||
|
||||
return new float[] {
|
||||
topLeftRadius,
|
||||
topLeftRadius,
|
||||
topRightRadius,
|
||||
topRightRadius,
|
||||
bottomRightRadius,
|
||||
bottomRightRadius,
|
||||
bottomLeftRadius,
|
||||
bottomLeftRadius
|
||||
};
|
||||
}
|
||||
|
||||
private void updatePath() {
|
||||
if (!mNeedUpdatePathForBorderRadius) {
|
||||
return;
|
||||
|
@ -258,25 +277,10 @@ import com.facebook.csslayout.Spacing;
|
|||
mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f);
|
||||
}
|
||||
|
||||
float defaultBorderRadius = !CSSConstants.isUndefined(mBorderRadius) ? mBorderRadius : 0;
|
||||
float topLeftRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[0]) ? mBorderCornerRadii[0] : defaultBorderRadius;
|
||||
float topRightRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[1]) ? mBorderCornerRadii[1] : defaultBorderRadius;
|
||||
float bottomRightRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[2]) ? mBorderCornerRadii[2] : defaultBorderRadius;
|
||||
float bottomLeftRadius = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[3]) ? mBorderCornerRadii[3] : defaultBorderRadius;
|
||||
|
||||
|
||||
float[] borderRadii = getBorderRadii();
|
||||
mPathForBorderRadius.addRoundRect(
|
||||
mTempRectForBorderRadius,
|
||||
new float[] {
|
||||
topLeftRadius,
|
||||
topLeftRadius,
|
||||
topRightRadius,
|
||||
topRightRadius,
|
||||
bottomRightRadius,
|
||||
bottomRightRadius,
|
||||
bottomLeftRadius,
|
||||
bottomLeftRadius
|
||||
},
|
||||
borderRadii,
|
||||
Path.Direction.CW);
|
||||
|
||||
float extraRadiusForOutline = 0;
|
||||
|
@ -288,14 +292,14 @@ import com.facebook.csslayout.Spacing;
|
|||
mPathForBorderRadiusOutline.addRoundRect(
|
||||
mTempRectForBorderRadiusOutline,
|
||||
new float[] {
|
||||
topLeftRadius + extraRadiusForOutline,
|
||||
topLeftRadius + extraRadiusForOutline,
|
||||
topRightRadius + extraRadiusForOutline,
|
||||
topRightRadius + extraRadiusForOutline,
|
||||
bottomRightRadius + extraRadiusForOutline,
|
||||
bottomRightRadius + extraRadiusForOutline,
|
||||
bottomLeftRadius + extraRadiusForOutline,
|
||||
bottomLeftRadius + extraRadiusForOutline
|
||||
borderRadii[0] + extraRadiusForOutline,
|
||||
borderRadii[1] + extraRadiusForOutline,
|
||||
borderRadii[2] + extraRadiusForOutline,
|
||||
borderRadii[3] + extraRadiusForOutline,
|
||||
borderRadii[4] + extraRadiusForOutline,
|
||||
borderRadii[5] + extraRadiusForOutline,
|
||||
borderRadii[6] + extraRadiusForOutline,
|
||||
borderRadii[7] + extraRadiusForOutline
|
||||
},
|
||||
Path.Direction.CW);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.touch.ReactHitSlopView;
|
||||
import com.facebook.react.touch.ReactInterceptingViewGroup;
|
||||
|
@ -94,6 +95,7 @@ public class ReactViewGroup extends ViewGroup implements
|
|||
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
|
||||
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
|
||||
private boolean mNeedsOffscreenAlphaCompositing = false;
|
||||
private @Nullable ReadableMap mNativeBackground;
|
||||
|
||||
public ReactViewGroup(Context context) {
|
||||
super(context);
|
||||
|
@ -134,7 +136,21 @@ public class ReactViewGroup extends ViewGroup implements
|
|||
"This method is not supported for ReactViewGroup instances");
|
||||
}
|
||||
|
||||
public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
|
||||
public void setNativeBackground(@Nullable ReadableMap nativeBackground) {
|
||||
mNativeBackground = nativeBackground;
|
||||
refreshTranslucentBackgroundDrawable();
|
||||
}
|
||||
|
||||
private void refreshTranslucentBackgroundDrawable() {
|
||||
Drawable background = null;
|
||||
if (mNativeBackground != null) {
|
||||
float[] cornerRadii = null;
|
||||
if (mReactBackgroundDrawable != null) {
|
||||
cornerRadii = mReactBackgroundDrawable.getBorderRadii();
|
||||
}
|
||||
background = ReactDrawableHelper.createDrawableFromJSDescription(getContext(), mNativeBackground, cornerRadii);
|
||||
}
|
||||
|
||||
// it's required to call setBackground to null, as in some of the cases we may set new
|
||||
// background to be a layer drawable that contains a drawable that has been previously setup
|
||||
// as a background previously. This will not work correctly as the drawable callback logic is
|
||||
|
@ -207,10 +223,12 @@ public class ReactViewGroup extends ViewGroup implements
|
|||
|
||||
public void setBorderRadius(float borderRadius) {
|
||||
getOrCreateReactViewBackground().setRadius(borderRadius);
|
||||
refreshTranslucentBackgroundDrawable();
|
||||
}
|
||||
|
||||
public void setBorderRadius(float borderRadius, int position) {
|
||||
getOrCreateReactViewBackground().setRadius(borderRadius, position);
|
||||
refreshTranslucentBackgroundDrawable();
|
||||
}
|
||||
|
||||
public void setBorderStyle(@Nullable String style) {
|
||||
|
|
|
@ -101,8 +101,7 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
|
|||
|
||||
@ReactProp(name = "nativeBackgroundAndroid")
|
||||
public void setNativeBackground(ReactViewGroup view, @Nullable ReadableMap bg) {
|
||||
view.setTranslucentBackgroundDrawable(bg == null ?
|
||||
null : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg));
|
||||
view.setNativeBackground(bg);
|
||||
}
|
||||
|
||||
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
||||
|
|
Loading…
Reference in New Issue