Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svg with TouchableOpacity #645

Closed
Naeoth opened this issue Apr 10, 2018 · 13 comments
Closed

Svg with TouchableOpacity #645

Naeoth opened this issue Apr 10, 2018 · 13 comments
Labels
Close when stale This issue is going to be closed when there is no activity for a while Missing repro This issue need minimum repro scenario

Comments

@Naeoth
Copy link

Naeoth commented Apr 10, 2018

Hi,

I would like to create my image buttons with SVG using TouchableOpacity element. But my problem is I don't have the opacity event triggered on press button.

This is my render :
render() { return ( <TouchableOpacity onPress={this.props.onPress}> <Svg width={100} height={100} viewBox="0 0 104 104" > .... </Svg> </TouchableOpacity> ); }

I would like to know how to create a button with SVG image (if it is possible).

@msand
Copy link
Collaborator

msand commented Apr 21, 2018

Seems to work fine: https://snack.expo.io/By94nj_3z

import React, { Component } from 'react';
import { View, StyleSheet, TouchableHighlight, TouchableOpacity } from 'react-native';
import { Constants, Svg } from 'expo';

export default class App extends Component {
  state = { toggle: false };
  toggle = () => this.setState(({ toggle }) => ({ toggle: !toggle }));
  render() {
    const { toggle } = this.state;
    return (
      <View style={styles.container}>
        <TouchableHighlight style={styles.button} onPress={this.toggle}>
          <Svg height={100} width={100}>
            <Svg.Circle
              cx={50}
              cy={50}
              r={45}
              strokeWidth={2.5}
              stroke="#e74c3c"
              fill="#f1c40f"
            />
            <Svg.Rect
              x={15}
              y={15}
              width={70}
              height={70}
              strokeWidth={2}
              stroke="#9b59b6"
              fill={toggle ? '#3498db' : '#9b59b6'}
            />
          </Svg>
        </TouchableHighlight>
        <TouchableOpacity style={styles.button} onPress={this.toggle}>
          <Svg height={100} width={100}>
            <Svg.Circle
              cx={50}
              cy={50}
              r={45}
              strokeWidth={2.5}
              stroke="#e74c3c"
              fill="#f1c40f"
            />
            <Svg.Rect
              x={15}
              y={15}
              width={70}
              height={70}
              strokeWidth={2}
              stroke="#9b59b6"
              fill={toggle ? '#3498db' : '#9b59b6'}
            />
          </Svg>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0ff',
  },
  button: {
    alignItems: 'center',
    backgroundColor: '#DDDDDD',
    padding: 10,
  },
});

@Naeoth Naeoth closed this as completed Apr 26, 2018
@Naeoth Naeoth reopened this Apr 26, 2018
@Naeoth Naeoth closed this as completed Apr 26, 2018
@slorber
Copy link

slorber commented Jul 11, 2018

@msand this works but unfortunately it would be more convenient if the svg shapes highlighted when touched. Currently you can press the whole SVG to highlight it. What I want is to highlight only the shapes (is the TouchableOpacity could be "nested" inside the root Svg tag)

As far as I know it's not possible

@pedrosimao
Copy link

I am facing the same issue.
It would be a great feature to be added to this Library.

@Naeoth
Copy link
Author

Naeoth commented Jan 11, 2019

Finaly it was a mistake from my code. I have the correct opacity behaviour on press.

Can you share your code to help you on your issue ?a

@msand
Copy link
Collaborator

msand commented Jan 11, 2019

@slorber @Naeoth I have an experiment for using TouchableOpacity inside svg content here: https://snack.expo.io/@msand/touchableopacityg no changes needed inside the library.

@msand
Copy link
Collaborator

msand commented Jan 11, 2019

With the latest version, it works better than in the version bundled in Expo.

Example:

import React from "react";
import Svg, {
    Rect,
} from "react-native-svg";

import TouchableOpacityG from './TouchableOpacityG';

export default () => (
    <Svg width="200" height="200" viewBox="0 0 100 100">
        <Rect
            x="0"
            y="0"
            width="100"
            height="50"
            fill="red"
            onPress={e => {
                console.log('press1', e);
            }}
        />
        <TouchableOpacityG
            onPress={e => {
                console.log('press2', e);
            }}>
            <Rect x="0" y="50" width="100" height="50" fill="blue" />
        </TouchableOpacityG>
    </Svg>
);

Can use this in plain react-native:
TouchableOpacityG.js

import React from 'react';
import {
    Animated,
    Easing,
    Platform,
    Touchable,
    TouchableWithoutFeedback,
    NativeMethodsMixin,
} from 'react-native';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import invariant from 'invariant';
import Svg, {
    G,
} from "react-native-svg";

const AnimatedG = Animated.createAnimatedComponent(G);

const ensurePositiveDelayProps = function(props: any) {
    invariant(
        !(
            props.delayPressIn < 0 ||
            props.delayPressOut < 0 ||
            props.delayLongPress < 0
        ),
        'Touchable components cannot have negative delay properties'
    );
};

function flattenStyle(style) {
    if (style === null || typeof style !== 'object') {
        return undefined;
    }

    if (!Array.isArray(style)) {
        return style;
    }

    const result = {};
    for (let i = 0, styleLength = style.length; i < styleLength; ++i) {
        const computedStyle = flattenStyle(style[i]);
        if (computedStyle) {
            for (const key in computedStyle) {
                result[key] = computedStyle[key];
            }
        }
    }
    return result;
}

const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 };

/**
 * A wrapper for making views respond properly to touches.
 * On press down, the opacity of the wrapped view is decreased, dimming it.
 *
 * Opacity is controlled by wrapping the children in an Animated.G, which is
 * added to the view hiearchy.  Be aware that this can affect layout.
 *
 * Example:
 *
 * ```
 * renderButton: function() {
 *   return (
 *     <TouchableOpacity onPress={this._onPressButton}>
 *       <Image
 *         style={styles.button}
 *         source={require('./myButton.png')}
 *       />
 *     </TouchableOpacity>
 *   );
 * },
 * ```
 * ### Example
 *
 * ```ReactNativeWebPlayer
 * import React from "react";
 * import {
 *   AppRegistry,
 * } from 'react-native'
 * import Svg, {
 *     Rect,
 * } from "react-native-svg";
 * 
 * import TouchableOpacityG from './TouchableOpacityG';
 * 
 * const App = () => (
 *     <Svg width="200" height="200" viewBox="0 0 100 100">
 *         <Rect
 *             x="0"
 *             y="0"
 *             width="100"
 *             height="50"
 *             fill="red"
 *             onPress={e => {
 *                 console.log('press1', e);
 *             }}
 *         />
 *         <TouchableOpacityG
 *             onPress={e => {
 *                 console.log('press2', e);
 *             }}>
 *             <Rect x="0" y="50" width="100" height="50" fill="blue" />
 *         </TouchableOpacityG>
 *     </Svg>
 * );
 *
 * AppRegistry.registerComponent('App', () => App)
 * ```
 *
 */
const TouchableOpacityG = ((createReactClass({
    displayName: 'TouchableOpacityG',
    mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur, NativeMethodsMixin],

    propTypes: {
        /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
         * error found when Flow v0.89 was deployed. To see the error, delete this
         * comment and run Flow. */
        ...TouchableWithoutFeedback.propTypes,
        /**
         * Determines what the opacity of the wrapped view should be when touch is
         * active. Defaults to 0.2.
         */
        activeOpacity: PropTypes.number,
        /**
         * TV preferred focus (see documentation for the View component).
         */
        hasTVPreferredFocus: PropTypes.bool,
        /**
         * Apple TV parallax effects
         */
        tvParallaxProperties: PropTypes.object,
    },

    getDefaultProps: function() {
        return {
            activeOpacity: 0.2,
        };
    },

    getInitialState: function() {
        return {
            /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
             * error found when Flow v0.89 was deployed. To see the error, delete
             * this comment and run Flow. */
            //...this.touchableGetInitialState(),
            /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
             * error found when Flow v0.89 was deployed. To see the error, delete
             * this comment and run Flow. */
            anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
        };
    },

    componentDidMount: function() {
        ensurePositiveDelayProps(this.props);
    },

    UNSAFE_componentWillReceiveProps: function(nextProps) {
        ensurePositiveDelayProps(nextProps);
    },

    componentDidUpdate: function(prevProps, prevState) {
        if (this.props.disabled !== prevProps.disabled) {
            this._opacityInactive(250);
        }
    },

    /**
     * Animate the touchable to a new opacity.
     */
    setOpacityTo: function(value: number, duration: number) {
        console.log('setOpacityTo', value, duration);
        Animated.timing(this.state.anim, {
            toValue: value,
            duration: duration,
            easing: Easing.inOut(Easing.quad),
            useNativeDriver: true,
        }).start();
    },

    /**
     * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
     * defined on your component.
     */
    touchableHandleActivePressIn: function(e: PressEvent) {
        if (e.dispatchConfig.registrationName === 'onResponderGrant') {
            this._opacityActive(0);
        } else {
            this._opacityActive(150);
        }
        this.props.onPressIn && this.props.onPressIn(e);
    },

    touchableHandleActivePressOut: function(e: PressEvent) {
        this._opacityInactive(250);
        this.props.onPressOut && this.props.onPressOut(e);
    },

    touchableHandleFocus: function(e: Event) {
        if (Platform.isTV) {
            this._opacityActive(150);
        }
        this.props.onFocus && this.props.onFocus(e);
    },

    touchableHandleBlur: function(e: Event) {
        if (Platform.isTV) {
            this._opacityInactive(250);
        }
        this.props.onBlur && this.props.onBlur(e);
    },

    touchableHandlePress: function(e: PressEvent) {
        this.props.onPress && this.props.onPress(e);
    },

    touchableHandleLongPress: function(e: PressEvent) {
        this.props.onLongPress && this.props.onLongPress(e);
    },

    touchableGetPressRectOffset: function() {
        return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
    },

    touchableGetHitSlop: function() {
        return this.props.hitSlop;
    },

    touchableGetHighlightDelayMS: function() {
        return this.props.delayPressIn || 0;
    },

    touchableGetLongPressDelayMS: function() {
        return this.props.delayLongPress === 0
            ? 0
            : this.props.delayLongPress || 500;
    },

    touchableGetPressOutDelayMS: function() {
        return this.props.delayPressOut;
    },

    _opacityActive: function(duration: number) {
        console.log('_opacityActive', duration);
        this.setOpacityTo(this.props.activeOpacity, duration);
    },

    _opacityInactive: function(duration: number) {
        console.log('_opacityInactive', duration);
        /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
         * error found when Flow v0.89 was deployed. To see the error, delete this
         * comment and run Flow. */
        this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
    },

    _getChildStyleOpacityWithDefault: function() {
        const childStyle = flattenStyle(this.props.style) || {};
        return childStyle.opacity == null ? 1 : childStyle.opacity;
    },

    render: function() {
        return (
            <AnimatedG
                accessible={this.props.accessible !== false}
                accessibilityLabel={this.props.accessibilityLabel}
                accessibilityHint={this.props.accessibilityHint}
                accessibilityRole={this.props.accessibilityRole}
                accessibilityStates={this.props.accessibilityStates}
                opacity={this.state.anim}
                nativeID={this.props.nativeID}
                testID={this.props.testID}
                onLayout={this.props.onLayout}
                isTVSelectable={true}
                hasTVPreferredFocus={this.props.hasTVPreferredFocus}
                tvParallaxProperties={this.props.tvParallaxProperties}
                hitSlop={this.props.hitSlop}
                /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
                 * error found when Flow v0.89 was deployed. To see the error, delete
                 * this comment and run Flow. */
                onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
                onResponderTerminationRequest={
                    /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses
                     * an error found when Flow v0.89 was deployed. To see the error,
                     * delete this comment and run Flow. */
                    this.touchableHandleResponderTerminationRequest
                }
                /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
                 * error found when Flow v0.89 was deployed. To see the error, delete
                 * this comment and run Flow. */
                onResponderGrant={this.touchableHandleResponderGrant}
                /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
                 * error found when Flow v0.89 was deployed. To see the error, delete
                 * this comment and run Flow. */
                onResponderMove={this.touchableHandleResponderMove}
                /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
                 * error found when Flow v0.89 was deployed. To see the error, delete
                 * this comment and run Flow. */
                onResponderRelease={this.touchableHandleResponderRelease}
                /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
                 * error found when Flow v0.89 was deployed. To see the error, delete
                 * this comment and run Flow. */
                onResponderTerminate={this.touchableHandleResponderTerminate}
                onPress={this.touchableHandlePress}
                onPressIn={this.touchableHandleActivePressIn}
                onPressOut={this.touchableHandleActivePressOut}>
                {this.props.children}
            </AnimatedG>
        );
    },
}): any): React.ComponentType<Props>);

module.exports = TouchableOpacityG;

@msand
Copy link
Collaborator

msand commented Jan 11, 2019

Simplified into plain component:

import React, { Component } from 'react';
import { Animated, Easing } from 'react-native';
import { G } from 'react-native-svg';

const AnimatedG = Animated.createAnimatedComponent(G);

function flattenStyle(style) {
    if (style === null || typeof style !== 'object') {
        return undefined;
    }

    if (!Array.isArray(style)) {
        return style;
    }

    const result = {};
    for (let i = 0, styleLength = style.length; i < styleLength; ++i) {
        const computedStyle = flattenStyle(style[i]);
        if (computedStyle) {
            for (const key in computedStyle) {
                result[key] = computedStyle[key];
            }
        }
    }
    return result;
}

/**
 * A wrapper for making svg elements respond properly to touches.
 * On press down, the opacity of the wrapped element is decreased, dimming it.
 *
 * Opacity is controlled by wrapping the children in an Animated.G, which is
 * added to the view hierarchy.
 *
 * Example:
 *
 * ```
 * renderRect: function() {
 *   return (
 *     <TouchableOpacityG
 *       onPress={e => {
 *         console.log('press', e);
 *       }}>
 *       <Rect x="0" y="50" width="100" height="50" fill="blue" />
 *     </TouchableOpacityG>
 *   );
 * },
 * ```
 * ### Example
 *
 * ```ReactNativeWebPlayer
 * import React from "react";
 * import {
 *   AppRegistry,
 * } from 'react-native'
 * import Svg, {
 *     Rect,
 * } from "react-native-svg";
 *
 * import TouchableOpacityG from './TouchableOpacityG';
 *
 * const App = () => (
 *     <Svg width="200" height="200" viewBox="0 0 100 100">
 *         <Rect
 *             x="0"
 *             y="0"
 *             width="100"
 *             height="50"
 *             fill="red"
 *             onPress={e => {
 *                 console.log('press1', e);
 *             }}
 *         />
 *         <TouchableOpacityG
 *             onPress={e => {
 *                 console.log('press2', e);
 *             }}>
 *             <Rect x="0" y="50" width="100" height="50" fill="blue" />
 *         </TouchableOpacityG>
 *     </Svg>
 * );
 *
 * AppRegistry.registerComponent('App', () => App)
 * ```
 *
 */
export default class TouchableOpacityG extends Component {
    static defaultProps = {
        activeOpacity: 0.2,
    };

    componentDidUpdate = (prevProps, prevState) => {
        if (this.props.disabled !== prevProps.disabled) {
            this._opacityInactive(250);
        }
    };

    /**
     * Animate the touchable to a new opacity.
     */
    setOpacityTo = (value, duration) => {
        Animated.timing(this.state.anim, {
            toValue: value,
            duration: duration,
            easing: Easing.inOut(Easing.quad),
            useNativeDriver: true,
        }).start();
    };

    touchableHandleActivePressIn = e => {
        if (e.dispatchConfig.registrationName === 'onResponderGrant') {
            this._opacityActive(0);
        } else {
            this._opacityActive(150);
        }
        this.props.onPressIn && this.props.onPressIn(e);
    };

    touchableHandleActivePressOut = e => {
        this._opacityInactive(250);
        this.props.onPressOut && this.props.onPressOut(e);
    };

    touchableHandlePress = e => {
        this.props.onPress && this.props.onPress(e);
    };

    touchableHandleLongPress = e => {
        this.props.onLongPress && this.props.onLongPress(e);
    };

    _opacityActive = duration => {
        this.setOpacityTo(this.props.activeOpacity, duration);
    };

    _opacityInactive = duration => {
        this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
    };

    _getChildStyleOpacityWithDefault = () => {
        const childStyle = flattenStyle(this.props.style) || {};
        return childStyle.opacity == null ? 1 : childStyle.opacity;
    };

    state = {
        anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
    };

    render() {
        return (
            <AnimatedG
                opacity={this.state.anim}
                onPress={this.touchableHandlePress}
                onLongPress={this.touchableHandleLongPress}
                onPressIn={this.touchableHandleActivePressIn}
                onPressOut={this.touchableHandleActivePressOut}
            >
                {this.props.children}
            </AnimatedG>
        );
    }
}

@slorber
Copy link

slorber commented Jan 14, 2019

thanks @msand this is great to know ;) will keep this in mind the next time i need this

@Symyon
Copy link

Symyon commented Dec 29, 2021

The solution above doesn't work on all Android devices. e.g. Works on Essential PH-1 and it doesn't on LG G5 or Samsung S21 - the ripple effect of touching works, onPress is not called though.

@cauyyl
Copy link

cauyyl commented May 17, 2023

#1634
https://github.com/software-mansion/react-native-svg/issues/1634#issuecomment-1550717356
Use PanResponder can solve this question, android also works fine.

@anis-18
Copy link

anis-18 commented Jul 4, 2023

Can anyone please tell us how can we do it ?

@talal-tilted
Copy link

Did someone find a solution ?

@bohdanprog
Copy link
Member

Hello,
Could someone provide an example of how to reproduce that, please?
Thank you.

@bohdanprog bohdanprog added Missing repro This issue need minimum repro scenario Close when stale This issue is going to be closed when there is no activity for a while labels Jun 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Close when stale This issue is going to be closed when there is no activity for a while Missing repro This issue need minimum repro scenario
Projects
None yet
Development

No branches or pull requests

9 participants