react-native: View bounds and touchable area incorrect after a rotate or a scale transform

Environment

React Native Environment Info: System: OS: macOS High Sierra 10.13.5 CPU: x64 Intel® Core™ i7-6660U CPU @ 2.40GHz Memory: 4.38 GB / 16.00 GB Shell: 5.3 - /bin/zsh Binaries: Node: 10.5.0 - /usr/local/bin/node Yarn: 1.7.0 - ~/.yarn/bin/yarn npm: 6.1.0 - /usr/local/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman SDKs: iOS SDK: Platforms: iOS 11.4, macOS 10.13, tvOS 11.4, watchOS 4.3 Android SDK: Build Tools: 23.0.1, 25.0.0, 25.0.1, 25.0.2, 26.0.0, 26.0.1, 26.0.2, 26.0.3 API Levels: 19, 21, 22, 23, 24, 25, 26 IDEs: Android Studio: 3.0 AI-171.4443003 Xcode: 9.4.1/9F2000 - /usr/bin/xcodebuild npmPackages: react: 16.4.1 => 16.4.1 react-native: 0.56.0 => 0.56.0 npmGlobalPackages: create-react-native-app: 1.0.0 react-native-cli: 2.0.1 react-native-rename: 2.2.2

Description

The bounds of transformed views, as reported by the inspector or as detected by the Touchable components, do not match what you seen on the screen. The touch area is not correctly transformed with the view.

Screenshot of a { scale: 2 } transform The real view (scaled) is in pink, the incorrect touchable area is in blue.

Screenshot of a { rotateZ: “-45deg” } transform The real view (rotated) is in pink, the incorrect touchable area is in blue.

This issue has already been reported in 2016 but has been closed because it was missing reproduction steps. This bug has been here for a long time.

Steps to Reproduce

  • Use a real device (I haven’t been able to reproduce the bug on emulator, I have on a real Galaxy A5 2017) UPDATE: See this comment for an emulator reproduction.
  • Use the inspector to display the view bounds
  • See that the reported bounds (blue square) do not match the real view (pink square)
  • Without the inspector, press somewhere on the real view where the blue square didn’t overlap: the press is not correctly detected. onPressOut is triggered immediately after onPressIn, without onPress being triggered. onPressOut is immediately triggered even if you don’t release the touch. Sometimes on the very first press, onPress is logged but it doesn’t wait for touch release.

Demo code with on-screen logging I’ve included some on-screen logging code to easily see the press events. This example shows the bug with the scale transform but it can be reproduced with a rotate transform as well.

import React from "react";
import { AppRegistry, View, TouchableOpacity, Text } from "react-native";

let i = 1;

class Demo extends React.Component {

  state = {
    logs: []
  };

  log(str) {
    this.setState(state => ({
      logs: [...state.logs.slice(-5), `${i++}  - ${str}`]
    }));
  }

  render() {
    return (
      <View style={{ flex: 1 }}>

        <View style={{
          position: "absolute",
          top: 100,
          left: 100,
          height: 100,
          width: 100,
          transform: [{
            scale: 2 // you can also try rotateZ: "-45deg"
          }]
        }}>
          <TouchableOpacity
            onPress={() => this.log("pressed")}
            onPressIn={() => this.log("pressed in")}
            onPressOut={() => this.log("pressed out")}
            style={{
              flex: 1,
              backgroundColor: "pink"
            }}
          />
        </View>

        <View style={{ position: "absolute", bottom: 0, height: 200 }}>
          <Text style={{ fontWeight: "bold", fontSize: 20 }}>Logs</Text>
          <Text>{this.state.logs.join("\n")}</Text>
        </View>

      </View>
    );
  }
}

AppRegistry.registerComponent("Demo", () => Demo);

Minimal example without on-screen logging

import React from "react";
import { AppRegistry, View, TouchableOpacity } from "react-native";

class Demo extends React.Component {

  render() {
    return (
      <View style={{ flex: 1 }}>

        <View style={{
          position: "absolute",
          top: 100,
          left: 100,
          height: 100,
          width: 100,
          transform: [{
            scale: 2 // you can also try rotateZ: "-45deg"
          }]
        }}>
          <TouchableOpacity
            onPress={() => alert("pressed")}
            style={{
              flex: 1,
              backgroundColor: "pink"
            }}
          />
        </View>

      </View>
    );
  }
}

AppRegistry.registerComponent("Demo", () => Demo);

Expected Behavior

The bounds should match the view even when transformed. The touch area should match what you see.

Actual Behavior

The bounds are not correctly transformed. The touch area is not correctly transformed and press behaviour is broken where the real view and the incorrect bounds do not overlap.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 33
  • Comments: 32 (11 by maintainers)

Commits related to this issue

Most upvoted comments

When I opened this issue 1 year ago, I was used to see PRs opened for a very long time with little “official” response. I was afraid of working for nothing, that’s why I asked if a PR would be considered (without response).

Last months’ open source efforts (OSS Roadmap, March update, June update ) show that the core team now gives a lot more consideration to contributions. That’s very motivating and I might restart working on that PR soon.

Still exist in Android on 0.59.1

Confirmed, using any transform on a touchable reproduces the bug on 0.58.

I have this same issue on RN 59.10 and as I can’t still move to next version I came up with following workaround (use view instead button) hope it can help someone:

<View style={[styles.styleWithTransform]}
           onStartShouldSetResponder={() => {}}>
        <Image />
</View>

Would a PR implementing the fix suggested in https://github.com/facebook/react-native/issues/19637#issuecomment-396065914 be considered?

Still experiencing this issue in RN 0.62

as temporarily solution just add hitSlop={{ top: 0, bottom: 0, left: 0, right: 0 }} to your TouchableOpacity

i am facing the same issues on android, any news about it?

You could try fixing this by using prop rendertohardwaretextureandroid with value of true

https://reactnative.dev/docs/view#rendertohardwaretextureandroid

Whether this View should render itself (and all of its children) into a single hardware texture on the GPU.

On Android, this is useful for animations and interactions that only modify opacity, rotation, translation, and/or scale: in those cases, the view doesn’t have to be redrawn and display lists don’t need to be re-executed. The texture can be re-used and re-composited with different parameters. The downside is that this can use up limited video memory, so this prop should be set back to false at the end of the interaction/animation.

My pr would just enable hardware accelleration on that view after applying the border radius, but you could solve this issue by enabling hardware accelleration on the view in Javascript for Android API N till P

https://github.com/facebook/react-native/blob/e6658a203db20e7950648de55cb80f14961470e1/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java#L267-L270

the prop allows you to call setLayerType(HARDWARE) on each React View.

https://github.com/facebook/react-native/blob/1465c8f3874cdee8c325ab4a4916fda0b3e43bdb/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java#L120-L122

https://github.com/facebook/react-native/issues/29312#issuecomment-892405146

Hi @makovkastar, I’ve actually written the code a few days ago but haven’t found the time to write up a proper PR to go with it. I’ll try to take that time tomorrow, no promises.

As a workaround for scaling, you can use the prop hitslop in your TouchableOpacity. There you can define the size of your touchable area dependant on your scaling. But it would be better if this bug would get a fix in the next update of react-native @hushicai

I managed to partially reproduce the bug in the emulator:

  • the bounds are not transformed in the inspector
  • the onPress detection only works if I do not move (which is probably why I can’t do that on a real phone)
  • BUT you can also press in the view and release in the reported bounds and onPress will be triggered!

In the following GIF we can see this. Here’s what I did

  1. Inspect the pink view to show the reported bounds (blue).
  2. Press the pink view without moving between press in and press out: it’s detected (alert).
  3. Press in the pink view, move a bit, and release in the pink view: it’s incorrectly not detected.
  4. Press in the pink view, move a outside the pink view, and release: it’s correctly not detected.
  5. Press in the pink view, move a outside the pink view but inside the reported bounds, and release: it’s incorrectly detected (alert).

HI guys

I am not sur but I think this problem does not only concern the TouchableOpacity component. When we wrap a TextInput component in a Animated View which use a transform animation, we can’t focus the input…

                <Animated.View
                    style={[
                        styles.inputContainer,
                        {
                            transform: [
                                {
                                    translateY: animatedTranslateYInputContainer,
                                },
                            ],
                        },
                    ]}>
                    <TextInput
                        style={styles.input}
                        onChangeText={onChangeText}
                        value={value}
                    />
                </Animated.View>

The issue is still here in RN 0.56, I’ve updated the environment section.