Skip to content

zIndex does not work with dynamic components on Android #8968

@madox2

Description

@madox2

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.

Activity

thedgbrt

thedgbrt commented on Aug 30, 2016

@thedgbrt

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

roysG

roysG commented on Sep 7, 2016

@roysG

any news?

tioback

tioback commented on Sep 19, 2016

@tioback

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

dmitru commented on Sep 30, 2016

@dmitru

+1, happened to me in a similar situation.

liamfd

liamfd commented on Oct 7, 2016

@liamfd

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

tioback commented on Oct 7, 2016

@tioback

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

asgvard

asgvard commented on Nov 10, 2016

@asgvard

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

tuckerconnelly commented on Nov 14, 2016

@tuckerconnelly
Contributor

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

asgvard commented on Nov 15, 2016

@asgvard

@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

asgvard commented on Nov 15, 2016

@asgvard

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 :)

93 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @SourceCipher@putuyoga@smiled0g@dmitru@coderdave

        Issue actions

          zIndex does not work with dynamic components on Android · Issue #8968 · facebook/react-native