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

TabBarIOS (Tabs) in AppleTV flickering when switching between tabs #15081

Closed
rada opened this Issue Jul 18, 2017 · 11 comments

Comments

Projects
None yet
5 participants
@rada

rada commented Jul 18, 2017

Is this a bug report?

Yes

Have you read the Bugs section of the Contributing to React Native Guide?

Yes

Environment

  1. react-native -v: 0.46.1
  2. node -v: 6.5.0
  3. npm -v: 3.10.3
  4. yarn --version (if you use Yarn):

Then, specify:

  • Target Platform: AppleTV
  • Development Operating System: macOS Sierra (10.12.5)
  • Build tools: Xcode (8.3.3)

Steps to Reproduce

  1. Switch to any Tab back and forth
  2. Switch to any Tab back and forth

Expected Behavior

The switch to any tab is smooth and without any flickering or selection falling back to previous tab.

Actual Behavior

Switching between TabBarIOS.Items is flickering and is not smooth. While selecting the new tab, It seems like first the new tab is selected, then immediately the old one and then again the new tab. This results in tab flickering when switching between the tabs. Worse case is, when the tabs render some more complex composed element. In this case, the new tab is not being selected, as the render is taking longer. Once it is rendered, we can select it but again the flickering occurs.

It looks like by the time the JS processes the received native event (tab switch) and sends an event (prop) back to Native, the tab selection falls back to the previous tab.

I have included very basic TabBar example with plain view and one text where flickering occurs. However, given the simple View is used in tabs here, switching is occasionally smooth.

Reproducible Demo

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

export default class tvTabBar extends Component {

	_tabItems: Array<{ key: string }> = [
		{ key: 'TAB 1' },
		{ key: 'TAB 2' },
		{ key: 'TAB 3' },
		{ key: 'TAB 4' },
	];

	constructor(props) {
		super(props);
		this.state = { activeTab: 'TAB 1' };
	}

	_getTabItem = (key: string) => {
		switch (key) {
			case 'TAB 1':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'red', borderWidth: 10 }}>
						<Text style={{ fontSize: 30 }}>TAB 1</Text>
					</View>
				);
			case 'TAB 2':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'green', borderWidth: 20 }}>
						<Text style={{ fontSize: 30 }}>TAB 2</Text>
					</View>
				);
			case 'TAB 3':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'darkblue', borderWidth: 30 }}>
						<Text style={{ fontSize: 30 }}>TAB 3</Text>
					</View>
				);
			case 'TAB 4':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'deeppink', borderWidth: 40 }}>
						<Text style={{ fontSize: 30 }}>TAB 4</Text>
					</View>
				);
			default:
				return null;
		}
	}

	_setTab(key) {
		return () => {
			console.log('Selecting tab: ', key);
			this.setState({ activeTab: key });
		};
	}

	render() {
		return (
			<TabBarIOS>
				{
					this._tabItems.map((tabItem) => {
						return (
							<TabBarIOS.Item
								key={tabItem.key}
								onPress={this._setTab(tabItem.key)}
								title={tabItem.key}
								selected={tabItem.key === this.state.activeTab}
							>
								{ this._getTabItem(tabItem.key) }
							</TabBarIOS.Item>
						);
					})
				}
			</TabBarIOS>
		);
	}
}

AppRegistry.registerComponent('tvTabBar', () => { return tvTabBar; });

link to project in gitlab: https://gitlab.com/radamich/tvTabBar

@hramos

This comment has been minimized.

Show comment
Hide comment
@hramos
Contributor

hramos commented Jul 18, 2017

@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 18, 2017

Collaborator

Thanks for the bug report, I'll take a look.... might be a race condition between the native focus engine calls and JS

Collaborator

dlowder-salesforce commented Jul 18, 2017

Thanks for the bug report, I'll take a look.... might be a race condition between the native focus engine calls and JS

@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 18, 2017

Collaborator

@rada While I'm working on this, I have a suggested code change that seems to remove most of the flickering.

    _setTab(key) {
        return () => {
            if(this.state.activeTab !== key) {
                console.log('Selecting tab: ', key);
                this.setState({ activeTab: key });
            }
        };
    }
Collaborator

dlowder-salesforce commented Jul 18, 2017

@rada While I'm working on this, I have a suggested code change that seems to remove most of the flickering.

    _setTab(key) {
        return () => {
            if(this.state.activeTab !== key) {
                console.log('Selecting tab: ', key);
                this.setState({ activeTab: key });
            }
        };
    }
@rada

This comment has been minimized.

Show comment
Hide comment
@rada

rada Jul 18, 2017

@dlowder-salesforce thanks, the switching indeed improves by the condition however in case we have a more complex component, the tabs still flicker. Let me know if you need an example with such component.

rada commented Jul 18, 2017

@dlowder-salesforce thanks, the switching indeed improves by the condition however in case we have a more complex component, the tabs still flicker. Let me know if you need an example with such component.

dlowder-salesforce added a commit to dlowder-salesforce/react-native that referenced this issue Jul 19, 2017

@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 19, 2017

Collaborator

@rada I have a fix, please feel free to try it out and let me know.

Collaborator

dlowder-salesforce commented Jul 19, 2017

@rada I have a fix, please feel free to try it out and let me know.

@rada

This comment has been minimized.

Show comment
Hide comment
@rada

rada Jul 19, 2017

@dlowder-salesforce thanks, this fix works in case the component in the selected tab already finished rendering. However it's not working if we switch tab while the tab is not fully rendered yet.

We are developing an application for AppleTV and once the first tab is shown, we fetch data from GraphQL server and display it in a ListView. The problem is that if we switch tabs during ListView rendering (renderRow), the tabs are again flickering and jumping around.

I have updated the example at https://gitlab.com/radamich/tvTabBar and added a listView in the first tab and also timeout in componentDidMount. After 3 seconds, the listView is updated and is rendering 2000 rows (just for testing purposes so there's time to switch tabs, we don't have this amount of data in our app).

Steps to reproduce:
Scenario 1:

  1. Start the application
  2. Once the TAB 1 is loaded, wait 3 seconds for ListView to render data
  3. When the ListView starts rendering (you should see it in the TAB 1), switch to TAB 2
  4. you should see tab selection jumping back and forth

Scenario 2:

  1. Start the application
  2. Once TAB 1 is loaded, immediately switch to TAB 2 (or TAB 3/4)
  3. When the ListView (in TAB 1) starts rendering (after 3 seconds since app start, you can confirm it in the console log), switch to TAB 2 or 3 or 4.
  4. you should see tab selection jumping back and forth

Let me know if you need any more information. Thanks.

List.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

export default class List extends Component {

	
	constructor(props) {
		super(props);

		this.ds = new ListView.DataSource({ rowHasChanged: (r1: Object, r2: Object) => { return r1 !== r2; } });

		this.state = {
			dataSource: this.ds.cloneWithRows([{ value: 1 }]),
		}
	};

	_renderRow(rowData) {
		console.log('RenderRow: ', rowData.value);
		return (
			<View>
				<Text>{rowData.value}</Text>
			</View>
		)
	}
	
	componentDidMount() {
		setTimeout(() => {
			const data = Array.from({ length: 2000 }).map((_, index) => {
				return { value: index };
			})
			this.setState({
				dataSource: this.ds.cloneWithRows(data),
			})
		}, 3000)
	}
	
	render() {
		return (
			<View>
				<ListView
					dataSource={this.state.dataSource}
					enableEmptySections
					removeClippedSubviews={false}
					renderRow={this._renderRow}
				/>
			</View>
		);
	}
}

index.ios.js/index.tvos.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

import List from './List';

export default class tvTabBar extends Component {

	_tabItems: Array<{ key: string }> = [
		{ key: 'TAB 1' },
		{ key: 'TAB 2' },
		{ key: 'TAB 3' },
		{ key: 'TAB 4' },
	];

	constructor(props) {
		super(props);
		this.state = { activeTab: 'TAB 1' };
	}

	_getTabItem = (key: string) => {
		switch (key) {
			case 'TAB 1':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'red', borderWidth: 10 }}>
						<List />
						<Text style={{ fontSize: 30 }}>TAB 1</Text>
					</View>
				);
			case 'TAB 2':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'green', borderWidth: 20 }}>
						<Text style={{ fontSize: 30 }}>TAB 2</Text>
					</View>
				);
			case 'TAB 3':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'darkblue', borderWidth: 30 }}>
						<Text style={{ fontSize: 30 }}>TAB 3</Text>
					</View>
				);
			case 'TAB 4':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'deeppink', borderWidth: 40 }}>
						<Text style={{ fontSize: 30 }}>TAB 4</Text>
					</View>
				);
			default:
				return null;
		}
	}

	_setTab(key) {
		return () => {
			console.log('Selecting tab: ', key);
			this.setState({ activeTab: key });
		};
	}

	render() {
		return (
			<TabBarIOS>
				{
					this._tabItems.map((tabItem) => {
						return (
							<TabBarIOS.Item
								key={tabItem.key}
								onPress={this._setTab(tabItem.key)}
								title={tabItem.key}
								selected={tabItem.key === this.state.activeTab}
							>
								{ this._getTabItem(tabItem.key) }
							</TabBarIOS.Item>
						);
					})
				}
			</TabBarIOS>
		);
	}
}

AppRegistry.registerComponent('tvTabBar', () => { return tvTabBar; });

rada commented Jul 19, 2017

@dlowder-salesforce thanks, this fix works in case the component in the selected tab already finished rendering. However it's not working if we switch tab while the tab is not fully rendered yet.

We are developing an application for AppleTV and once the first tab is shown, we fetch data from GraphQL server and display it in a ListView. The problem is that if we switch tabs during ListView rendering (renderRow), the tabs are again flickering and jumping around.

I have updated the example at https://gitlab.com/radamich/tvTabBar and added a listView in the first tab and also timeout in componentDidMount. After 3 seconds, the listView is updated and is rendering 2000 rows (just for testing purposes so there's time to switch tabs, we don't have this amount of data in our app).

Steps to reproduce:
Scenario 1:

  1. Start the application
  2. Once the TAB 1 is loaded, wait 3 seconds for ListView to render data
  3. When the ListView starts rendering (you should see it in the TAB 1), switch to TAB 2
  4. you should see tab selection jumping back and forth

Scenario 2:

  1. Start the application
  2. Once TAB 1 is loaded, immediately switch to TAB 2 (or TAB 3/4)
  3. When the ListView (in TAB 1) starts rendering (after 3 seconds since app start, you can confirm it in the console log), switch to TAB 2 or 3 or 4.
  4. you should see tab selection jumping back and forth

Let me know if you need any more information. Thanks.

List.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

export default class List extends Component {

	
	constructor(props) {
		super(props);

		this.ds = new ListView.DataSource({ rowHasChanged: (r1: Object, r2: Object) => { return r1 !== r2; } });

		this.state = {
			dataSource: this.ds.cloneWithRows([{ value: 1 }]),
		}
	};

	_renderRow(rowData) {
		console.log('RenderRow: ', rowData.value);
		return (
			<View>
				<Text>{rowData.value}</Text>
			</View>
		)
	}
	
	componentDidMount() {
		setTimeout(() => {
			const data = Array.from({ length: 2000 }).map((_, index) => {
				return { value: index };
			})
			this.setState({
				dataSource: this.ds.cloneWithRows(data),
			})
		}, 3000)
	}
	
	render() {
		return (
			<View>
				<ListView
					dataSource={this.state.dataSource}
					enableEmptySections
					removeClippedSubviews={false}
					renderRow={this._renderRow}
				/>
			</View>
		);
	}
}

index.ios.js/index.tvos.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

import List from './List';

export default class tvTabBar extends Component {

	_tabItems: Array<{ key: string }> = [
		{ key: 'TAB 1' },
		{ key: 'TAB 2' },
		{ key: 'TAB 3' },
		{ key: 'TAB 4' },
	];

	constructor(props) {
		super(props);
		this.state = { activeTab: 'TAB 1' };
	}

	_getTabItem = (key: string) => {
		switch (key) {
			case 'TAB 1':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'red', borderWidth: 10 }}>
						<List />
						<Text style={{ fontSize: 30 }}>TAB 1</Text>
					</View>
				);
			case 'TAB 2':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'green', borderWidth: 20 }}>
						<Text style={{ fontSize: 30 }}>TAB 2</Text>
					</View>
				);
			case 'TAB 3':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'darkblue', borderWidth: 30 }}>
						<Text style={{ fontSize: 30 }}>TAB 3</Text>
					</View>
				);
			case 'TAB 4':
				return (
					<View style={{ flex: 1, top: 140, borderColor: 'deeppink', borderWidth: 40 }}>
						<Text style={{ fontSize: 30 }}>TAB 4</Text>
					</View>
				);
			default:
				return null;
		}
	}

	_setTab(key) {
		return () => {
			console.log('Selecting tab: ', key);
			this.setState({ activeTab: key });
		};
	}

	render() {
		return (
			<TabBarIOS>
				{
					this._tabItems.map((tabItem) => {
						return (
							<TabBarIOS.Item
								key={tabItem.key}
								onPress={this._setTab(tabItem.key)}
								title={tabItem.key}
								selected={tabItem.key === this.state.activeTab}
							>
								{ this._getTabItem(tabItem.key) }
							</TabBarIOS.Item>
						);
					})
				}
			</TabBarIOS>
		);
	}
}

AppRegistry.registerComponent('tvTabBar', () => { return tvTabBar; });
@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 19, 2017

Collaborator

@rada that's very interesting, I will try that out. I take it that this problem does not occur in iOS?

Collaborator

dlowder-salesforce commented Jul 19, 2017

@rada that's very interesting, I will try that out. I take it that this problem does not occur in iOS?

@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 19, 2017

Collaborator

@rada I tried out your test case.... With my fix, I was still able to reproduce scenario 1 some of the time, but not scenario 2. It seems to me that when the 2000 rows show up, the entire app (including the tab bar) is rerendered, not just the tab with the list view. I'll see if I can figure out a way to prevent that.

Collaborator

dlowder-salesforce commented Jul 19, 2017

@rada I tried out your test case.... With my fix, I was still able to reproduce scenario 1 some of the time, but not scenario 2. It seems to me that when the 2000 rows show up, the entire app (including the tab bar) is rerendered, not just the tab with the list view. I'll see if I can figure out a way to prevent that.

@rada

This comment has been minimized.

Show comment
Hide comment
@rada

rada Jul 20, 2017

@dlowder-salesforce in iOS, tabBar is working fine, we don't see this problem.
Let me know if you need any more info/test.

rada commented Jul 20, 2017

@dlowder-salesforce in iOS, tabBar is working fine, we don't see this problem.
Let me know if you need any more info/test.

dlowder-salesforce added a commit to dlowder-salesforce/react-native that referenced this issue Jul 23, 2017

@dlowder-salesforce

This comment has been minimized.

Show comment
Hide comment
@dlowder-salesforce

dlowder-salesforce Jul 23, 2017

Collaborator

@rada please try out https://github.com/dlowder-salesforce/react-native/tree/tvos-tabbar-fix -- I think this should fix the remaining issues. I tried it with your ListView example code above and it seems to be behaving correctly.

Collaborator

dlowder-salesforce commented Jul 23, 2017

@rada please try out https://github.com/dlowder-salesforce/react-native/tree/tvos-tabbar-fix -- I think this should fix the remaining issues. I tried it with your ListView example code above and it seems to be behaving correctly.

@VladaGit

This comment has been minimized.

Show comment
Hide comment
@VladaGit

VladaGit Jul 25, 2017

@dlowder-salesforce Using "react-native": "git+https://github.com/dlowder-salesforce/react-native.git#tvos-tabbar-fix",
Can not reproduce the issue on the project anymore after using the tvos-tabbar-fix.
Thank you.

VladaGit commented Jul 25, 2017

@dlowder-salesforce Using "react-native": "git+https://github.com/dlowder-salesforce/react-native.git#tvos-tabbar-fix",
Can not reproduce the issue on the project anymore after using the tvos-tabbar-fix.
Thank you.

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