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

zIndex does not work with dynamic components on Android #8968

Closed
madox2 opened this issue Jul 22, 2016 · 84 comments
Closed

zIndex does not work with dynamic components on Android #8968

madox2 opened this issue Jul 22, 2016 · 84 comments
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@madox2
Copy link

madox2 commented Jul 22, 2016

I am trying to render elements conditionally where each element has different zIndex style property.
Using folowing code in Android emulator with react-native 0.30.0.

import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class Demo extends Component {
  constructor(props, ctx) {
    super(props, ctx);
    this.state = {
      showGreen: true,
    };
  }
  render() {
    return (
      <View style={{flex: 1, padding: 20}}>
        <View style={[styles.item, {zIndex: 3, backgroundColor: 'red'}]}>
          <Text>zIndex: 3</Text>
        </View>
        {this.state.showGreen &&
        <View style={[styles.item, {zIndex: 2, backgroundColor: 'green'}]}>
          <Text>zIndex: 2</Text>
        </View>
        }
        <View style={[styles.item, {zIndex: 1, backgroundColor: 'blue'}]}>
          <Text>zIndex: 1</Text>
        </View>
        <View style={styles.button}>
          <Text onPress={() => this.setState({ showGreen: !this.state.showGreen })}>
            Toggle green
          </Text>
        </View>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  item: {
    marginTop: -20,
    height: 50,
    paddingTop: 22,
  },
  button: {
    backgroundColor: 'gray',
    marginTop: 30,
  }
});

Initial screen looks like expected:

zindex1

When I click 'Toggle green' button to dynamically show/hide green element it hides blue element instead and furthermore element's text is missing:

zindex2

When I click the button again, red and green elements remains visible and the toggle button jumps down.

@thedgbrt
Copy link

thedgbrt commented Aug 30, 2016

Still an issue in 0.32.
It seems that zindex breaks component unmounting.

@roysG
Copy link

roysG commented Sep 7, 2016

any news?

@tioback
Copy link

tioback commented Sep 19, 2016

Same thing happened to me, so I'll throw in some more details:
In iOS, returning null makes the element disappear.
In Android, you have to reduce the height to 0 and remove borders.
What is worse is that you can't stick to a single solution, because Android's workaround won't work for iOS. The element will just show up again.

Here's the code for a component that does this sort of thing:

import React from 'react';

import {
    Image,
    Platform,
    StyleSheet,
    Text,
    TouchableOpacity,
    View
} from 'react-native';

import { default as closeIcon } from "../img/closeIcon.png";

const ReadFullArticleFloatingBox = ({visible, opacity = 0, title, onPressRead, onPressClose}) => {
    let conditionalLayout = visible ? {} : styles.hidden;
    return Platform.OS === "ios" && !visible ? null : (
        <View style={[styles.container, conditionalLayout, { opacity }]}>
            <View style={styles.leftContainer}>
                <Text numberOfLines={1} style={styles.title}>{title}</Text>
                <TouchableOpacity onPress={onPressRead} style={styles.readButton}>
                    <Text style={styles.readButtonText}>READ FULL ARTICLE</Text>
                </TouchableOpacity>
            </View>
            <TouchableOpacity onPress={onPressClose} style={styles.icon}>
                <Image
                        resizeMode={Image.resizeMode.contain}
                        style={styles.iconImage}
                        source={closeIcon}
                    />
            </TouchableOpacity>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        height: 60,
        position: "absolute",
        zIndex: 2,
        left: 0,
        right: 0,
        marginTop: 20,
        paddingHorizontal: 10,
        borderTopWidth: 1,
        borderBottomWidth: 1,
        borderColor: "lightgrey",
        backgroundColor: "white",
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "center"
    },
    hidden: {
        zIndex: -2,
        height: 0,
        borderTopWidth: 0,
        borderBottomWidth: 0
    },
    leftContainer: {
        flex: 1,
        padding: 6
    },
    title: {
        fontSize: 13,
        fontFamily: "CrimsonText-Roman",
        color: "#46474C"
    },
    readButton: {
        paddingVertical: 6
    },
    readButtonText: {
        fontFamily: "Roboto",
        color: "teal",
        fontSize: 11
    },
    icon: {
        width: 40,
        height: 40,
        alignItems: "center",
        justifyContent: "center",
        padding: 10
    },
    iconImage: {
        width: 20,
        height: 20
    }
});

export default ReadFullArticleFloatingBox;

@dmitru
Copy link

dmitru commented Sep 30, 2016

+1, happened to me in a similar situation.

@liamfd
Copy link

liamfd commented Oct 7, 2016

If it's helpful to anyone, I created a Hideable component to handle this:

import React, { Component } from 'react';
import {
  View,
  StyleSheet,
  Platform
} from 'react-native';

const hideable_styles = StyleSheet.create({
  android_hidden: {
    height: 0,
    borderWidth: 0,
    paddingTop: 0,
    paddingBottom: 0,
    marginTop: 0,
    marginBottom: 0
  }
});
class Hideable extends Component {
  render(){
    const {hide, children, style = {}, ...rest_props} = this.props;

    // on ios, best way to hide is to return null
    if (hide && Platform.OS === 'ios') return null;

    // otherwise, if android, going to add in the special android hidden styles
    const conditional_layout = hide ? hideable_styles.android_hidden : {};
    const styles = [style, conditional_layout];

    return (
      <View {...rest_props} style={styles}>
        {children}
      </View>
    );
  }
}

export default Hideable;

Here's a simple use:

const simpleTestHideable = ({hide = true}) => {
  return (
    <Hideable hide={hide} style={{zIndex: 1, backgroundColor: 'red', height: 100}} >
      <Text>{hide ? 'You should not see this' : 'You should see this' }</Text>
    </Hideable>
  );
}

Here's a more complex, interactive usage example:

class TestHideable extends Component {
  constructor(props){
    super(props);

    this.state = { hide_content: false };

    this.toggleContent = this.toggleContent.bind(this);
  }

  toggleContent(){
    this.setState({hide_content: !this.state.hide_content});
  }

  render(){
    const { hide_content } = this.state;

    return (
      <View style={{paddingTop: 20}}>
        <Hideable
          hide={hide_content}
          anotherProp="foo"
          style={{
            padding: 10,
            height: 100,
            borderWidth: 0.5,
            borderColor: '#d6d7da',
            marginTop: 100,
            marginBottom: 100,
            backgroundColor: 'red',
            zIndex: 2
          }} >
            <Text>I am showing!</Text>
        </Hideable>
        <Text onPress={this.toggleContent}>{hide_content ? 'SHOW!' : 'HIDE!'}</Text>
      </View>
    )
  }
}

export default TestHideable;

You should be able to pass through whatever styles or props you'd like to Hideable's View, make it animatable, etc.

Also, @tioback, I noticed you set a negative zIndex, is that something you recommend? It's worked for me without it but I wasn't sure if it was needed for certain devices or certain use cases. I also noticed that on my device I had to remove the vertical margins and padding as well, so there may be other styles to watch out for.

I'm still pretty new to React Native so, grain of salt.

@tioback
Copy link

tioback commented Oct 7, 2016

@liamfd no, I don't recommend for, nor against it. Might be some left-over from a failed test.
Nice component, BTW.

@asgvard
Copy link

asgvard commented Nov 10, 2016

Hi,

TL;DR;
If you have array of components with zIndexes, and then one of them dynamically removed (becomes null by state change for ex.), instead of reordering the remaining components, the removed one still "partially" remains in the viewport and covers everything underneath by white area.

Details:
I'm struggling with the same issue. Afaik, zIndex only reorders the Views on native, but seems that whenever the element becomes null (removed from the tree), it still overlaps all the elements that had lower zIndex at some point, even though this "newly becoming null" element doesn't have it's own zIndex as it doesn't have styles anymore.
So this means that the element is not removed completely when it's set to null, because the green color in the initial example still stays.
It's a huge problem for me, because I'm building cross-platform app and I'm trying to have as much shared code between Web and Native as possible. I'm using react-router, so I have few <Match> components as siblings. To implement animated transitions between pages I'm using react-motion, that basically sets component to "null" at the end of some spring animation when it's not matching the route. Assuming that I cannot control the order of <Match> components (because they're in a separate route config which is again shared between Web and Native), it causes the issue that if I go from the last page to previous one, the last one becomes "null" at the end of animation and jumps to the front layer. All pages are absolutely positioned and fullscreen, so in result I see the full white screen, because this "null" element overlaps everything. It's not a best option for me to use workaround like <Hideable>, because it's platform agnostic, and my goal is to have this animated transitions as a shared component, as well as the react-router <Match>'es.

Here is slightly modified example using three absolutely positioned elements, each subsequent of them have lower zIndex, so red is on top (60px height), green is underneath (90px height), and the blue is underneath (120px height). When the green is removed, the expected behaviour would be that green is completely removed from subviews, the remaining two elements would have their zIndex (order) recalculated, so we would see only red and blue underneath it.
But instead the green one stays alive, and even covers the blue element with "invisible" white overlay. (The same I see in my app with the routes described above). And it occurs ONLY when dynamically remove this green element. If I initially set the flag to false, it doesn't appear which is correct.

Modified example in the fresh react-native app 0.37.0:

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View
} from 'react-native';

export default class zindex extends Component {
  constructor(props) {
    super(props);
    this.state = {
      showGreen: true
    };
  }
  render() {
    return (
      <View style={{flex: 1}}>
        <View style={[styles.item, {zIndex: 3, backgroundColor: 'red'}]}>
          <Text>zIndex: 3</Text>
        </View>
        {this.state.showGreen ?
          <View style={[styles.item, {zIndex: 2, backgroundColor: 'green', height: 90}]}>
            <Text>zIndex: 2</Text>
          </View> : null
        }
        <View style={[styles.item, {zIndex: 1, backgroundColor: 'blue', height: 120}]}>
          <Text>zIndex: 1</Text>
        </View>
        <View style={styles.button}>
          <Text onPress={() => this.setState({ showGreen: !this.state.showGreen })}>
            Toggle green
          </Text>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  item: {
    position: 'absolute',
    left: 0,
    right: 0,
    top: 0,
    height: 60
  },
  button: {
    position: 'absolute',
    left: 0,
    right: 0,
    backgroundColor: 'gray',
    top: 150,
    height: 30
  }
});

AppRegistry.registerComponent('zindex', () => zindex);

Starting page:
screen shot 2016-11-10 at 20 30 19

Result:
screen shot 2016-11-10 at 20 30 38

Expected behaviour:
screen shot 2016-11-10 at 20 31 01

Not sure how I will proceed with this issue, but I will keep posting any ideas if I find them.

Update 1: If I initially put the showGreen to false, then add it by toggling the button, everything is fine. I suspect that the views are reordered by zIndex only when they're added, but not when they're removed. Might it be related to this commit ? In the ViewGroupManager.java and ReactViewManager.java the reordering is not called on removeViewAt, might this be a problem? I'm not a Java expert though, would appreciate if someone could have a closer look at this particular case.

@tuckerconnelly
Copy link
Contributor

tuckerconnelly commented Nov 14, 2016

Yo I'm the author of that commit :)

@asgvard Thanks for the fantastic bug reporting. I think you're right, check out:

I think the fix is as simple as matching the addView method so it looks like:

  public void removeViewAt(T parent, int index) {
    parent.removeViewAt(index);
    reorderChildrenByZIndex(parent)
  }

I don't really have the bandwidth to PR a fix and test it right now, but you probably could :) It's a good first PR.

@asgvard
Copy link

asgvard commented Nov 15, 2016

@tuckerconnelly Hey :) So today I made a try to fix this issue with simply adding the reorderChildrenByZIndex(parent); in removeViewAt for ViewGroupManager, but unfortunately it didn't helped. As I mentioned I'm not a Java developer, so I took some simple steps to debug this by using Logs. I added a accessibilityLabel to my <View>'s so I can easily see which one is logged in Java code with view.getContentDescription().

Here is my modified JS code:

<View accessibilityLabel="RED" style={[styles.item, {zIndex: 3, backgroundColor: 'red'}]}>
          <Text>zIndex: 3</Text>
        </View>
        {this.state.showGreen ?
          <View accessibilityLabel="GREEN" style={[styles.item, {zIndex: 2, backgroundColor: 'green', height: 90}]}>
            <Text>zIndex: 2</Text>
          </View> : null
        }
        <View accessibilityLabel="BLUE" style={[styles.item, {zIndex: 1, backgroundColor: 'blue', height: 120}]}>
          <Text>zIndex: 1</Text>
        </View>
        <View accessibilityLabel="BUTTON" style={styles.button}>
          <Text onPress={() => this.setState({ showGreen: !this.state.showGreen })}>
            Toggle green
          </Text>
        </View>

The result leads me to even more deep problem. From JS prospective I'm removing Green view, but in Java it actually tries to remove Blue:

11-15 00:30:59.657 12091 12091 I REORDER : Indice to remove: 1
11-15 00:30:59.657 12091 12091 I REORDER : Tag to delete: 8 which is: GREEN
11-15 00:30:59.657 12091 12091 I REORDER : manageChildren in NativeViewHierarchyManager...
11-15 00:30:59.657 12091 12091 I REORDER : indexToRemove: 1
11-15 00:30:59.657 12091 12091 I REORDER : viewToRemove: BLUE ID: 12
11-15 00:30:59.657 12091 12091 I REORDER : removeViewAt in ViewGroupManager invoked for index: 1
11-15 00:30:59.657 12091 12091 I REORDER : Children before removing: 
11-15 00:30:59.657 12091 12091 I REORDER : 0 child is: BUTTON
11-15 00:30:59.657 12091 12091 I REORDER : 1 child is: BLUE
11-15 00:30:59.657 12091 12091 I REORDER : 2 child is: GREEN
11-15 00:30:59.657 12091 12091 I REORDER : 3 child is: RED
11-15 00:30:59.657 12091 12091 I REORDER : Children after removing: 
11-15 00:30:59.657 12091 12091 I REORDER : 0 child is: BUTTON
11-15 00:30:59.657 12091 12091 I REORDER : 1 child is: GREEN
11-15 00:30:59.657 12091 12091 I REORDER : 2 child is: RED
11-15 00:30:59.657 12091 12091 I REORDER : Reordering started...
11-15 00:30:59.657 12091 12091 I REORDER : Queued to reorder: BUTTON
11-15 00:30:59.657 12091 12091 I REORDER : Queued to reorder: GREEN
11-15 00:30:59.657 12091 12091 I REORDER : Queued to reorder: RED
11-15 00:30:59.658 12091 12091 I REORDER : Reordering end...
11-15 00:30:59.658 12091 12091 I REORDER : view is bringed to front: BUTTON
11-15 00:30:59.658 12091 12091 I REORDER : view is bringed to front: GREEN
11-15 00:30:59.658 12091 12091 I REORDER : view is bringed to front: RED

Seems that in NativeViewHierarchyManager in the method manageChildren it uses indexes to remove views, but since the indexes changed internally in Java (after reordering by zIndex from [RED, GREEN, BLUE, BUTTON] to [BUTTON, BLUE, GREEN, RED]), and JS doesn't know about it, maybe it's more reliable to remove by tags? Because tagsToDelete contain the correct one, which is 8 (GREEN), and 12 is BLUE.
Also this bug might lead to another unexpected things because dropView is still called on GREEN :) So basically the BLUE is removed from view manager, but GREEN get dropped.

Will post any updates.
Cheers

@asgvard
Copy link

asgvard commented Nov 15, 2016

UPDATE:

First of all, I found out that it has nothing to do with the reorderChildrenByZIndex() at all, because we actually don't need to reorder children after we remove something, because the relative zIndex order for remaining items remain the same. The problem was exactly in that it was removing the wrong View by it's index in the array.

Fixed by relying on tagsToDelete instead of indicesToRemove in manageChildren method of NativeViewHierarchyManager. Because after reordering we cannot rely on the item index in the ViewGroup, but the tag seems to be reliable way of identifying the View anytime. I think the similar reason of using tagsToDelete was applied here.

Also this issue happens when Adding views, it adds them in the wrong index :) But since after adding the views we're doing reorder again, it doesn't matter that it was added into the wrong place.

So the proposed solution is to change manageChildren method, so it will first make a loop by tagsToDelete, perform removeView() on all the tagsToDelete, then after the cleanup of all the nodes to delete, we perform a loop by viewsToAdd and that's it. It will remove the huge chunk of code from here to here, because essentially all it's doing is viewManager.removeViewAt(viewToManage, indexToRemove); only in the case if the node is not animated and not in the tagsToDelete array. So instead we could just move this responsibility to the loop by tagsToDelete.

I will prepare PR soon :)

@tuckerconnelly
Copy link
Contributor

@asgvard Legend! Thanks!

@asgvard
Copy link

asgvard commented Nov 15, 2016

Seems this PR brakes few unit tests (yes I should've double-checked that :) ), will investigate why it failed and prepare another one.

@asgvard
Copy link

asgvard commented Nov 15, 2016

Ok so seems initial idea of just using tagsToDelete instead of indicesToRemove wasn't good. Because actually indicesToRemove consists the nodes that were moved from somewhere, plus the nodes to delete completely. So instead I propagated tagsToRemove from UIImplementation, because it's exactly the same as indicesToRemove, but in our case tag is more reliable way to safely remove nodes.

@mvf4z7
Copy link

mvf4z7 commented Nov 17, 2016

Any updates on this issue?

@asgvard
Copy link

asgvard commented Nov 17, 2016

@mvf4z7 Just keep track of this PR:
#10954

My local Android unit and integration tests passed. Circle and Travis CI failed for some unrelated reasons, so as soon as someone from Reviewers could have a look into this, maybe we will have it merged. I wouldn't mind to have it asap as well ;)

goshacmd pushed a commit to goshacmd/react-native that referenced this issue Dec 12, 2016
Commit facebook@3d3b067 implemented zIndex support for Android. However after zIndex resorting of the views, the JS doesn't know about the new order. When dynamically removing the view from an array of views, JS sends the command to remove the view at certain index, which is not the same in Java since the order of items changed.

Test Plan:
Related issue and the simple empty react-native 0.37 app where it was tested:
facebook#8968

In `manageChildren` in `NativeViewHierarchyManager.java` there was a loop by `indicesToRemove`, call `viewManager.removeViewAt(viewToManage, indexToRemove)`. However if the view is animated and it's ID in the array of `tagsToDelete`, do nothing and delegate this job of removing item to the loop by `tagsToDelete`.

Now we propagate `tagsToRemove` instead from `UIImplementation` to `NativeViewHierarchyOptimized` to `NativeViewHierarchyManager` and using it instead of `indicesToRemove`.

Also by having the old `indicesToRemove` loop we having the case that the *wrong* view is removed, but then, *correct* view is dropped in `tagsToDelete` loop. In result we have inconsistency when removing one view but dropping another. It's described in more details in mentioned above related issue comments.
@bakr-baroudi
Copy link

Any updates on this issue?

@farzd
Copy link

farzd commented Jan 17, 2017

any updates?

@franleplant
Copy link

I'm experiencing this also, zIndex + position:absolute breaks in android

@grubstarstar
Copy link

I'm still experiencing this just for the record!

@srdjanprpa
Copy link

me to, any update?

@glenn-axsy
Copy link

For anyone still stuck on the old RN here's a function I use that can be spread into your styles. It's based on the ideas presented by others above:

function zIndexWorkaround(val: number): Object {
    return Platform.select({
        ios: {zIndex: val},
        android: {elevation: val}
    });
}

Use it like this in place of e.g: zIndex: 100

...zIndexWorkaround(100)

Remove the Flow type checking from the function if you don't need it

@Noitidart
Copy link

@glenn-axsy and others reading his comment, just a warning: elevation is not a one-to-one solution for zIndex. For me it never worked how I needed it, I would get shadows when I didn't, and also the pointerEvents order was incorrect.

Noitidart referenced this issue Nov 9, 2017
Reviewed By: shergin

Differential Revision: D5917111

fbshipit-source-id: e3d97f26b6aada199f700ec6659ace0d7dffd4c5
@shuaibird
Copy link

My RN version is 0.49.5 and I've encountered the same issue when setting the position to absolute. My workaround is rearranging the components, whose orders are based on the zIndex. This may introduce some chaos but should be a better approach than setting the elevation.

@cruisecoders
Copy link

My react-native version is 0.49.3 and I am still encountering this problem with the zIndex and the elevation in android. I also used the @glenn-axsy workaround but still couldn't solve the issue.
skype_picture 1

In the image below I want the user icon on top of the View but it is hiding and not coming on the top as expected

@mnsrv
Copy link

mnsrv commented Dec 21, 2017

Why this issue is closed? zIndex still not working on Android, and elevation is not the solution, because of mess of pointer-events.

@kumarryogeshh
Copy link

Issue exists with react-native version 0.50.3
Works fine on iOS, compatibility issues with Android.

@Fortidude
Copy link

still not working on Android :/

Invalid props.style key elevation supplied to Image.

@jacklovepdf
Copy link

you can finish the work without zIndex

@AugustoAleGon
Copy link

It is still not working! this is a mess. I was using zindex but not using is not the solution

@joaosilva05
Copy link

+1. Works great on ios, breaks views in android

@FMdigit
Copy link

FMdigit commented Jan 9, 2018

@janicduplessis
any update?

@FMdigit
Copy link

FMdigit commented Jan 9, 2018

Issue exists with react-native version 0.51.0 too

@rolignu2
Copy link

damm work great in IOS but in ANDROID crash , any update ?

@nikasvan
Copy link

nikasvan commented Jan 31, 2018

It's been unresolved for too long, any updates?

@Dror-Bar
Copy link

Dror-Bar commented Feb 8, 2018

0.51.0
Works as expected on IOS.
On android neither zIndex nor elevation works for me.
pretty disappointing.

@VeroGA
Copy link

VeroGA commented Feb 9, 2018

0.52.1
having the same issue

@aprilmintacpineda
Copy link

still occur on RN@0.52.0

@igorarkhipenko
Copy link

0.54.0
The problem still exists

@aprilmintacpineda
Copy link

please check this newly opened issue. It has a repo that reproduces the problem.

#18344

@isaachinman
Copy link

v0.54.2

Still a problem.

@nirpeled
Copy link

nirpeled commented May 7, 2018

Still having this issue with v0.55.2

@rosti-comi
Copy link

still

@casey-budee
Copy link

Can we just agree that this issue needs to be reopen NOW?

@henrycity
Copy link

Still exist in v0.55.4

@SourceCipher
Copy link

Yep, having the same issue still. Even setting as absolute, the component will never render on top of the components, always will be hidden by the parent component.

@facebook facebook locked as resolved and limited conversation to collaborators Jul 19, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jul 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests