2015-02-19 01:39:09 +00:00
|
|
|
/**
|
2016-07-12 12:51:57 +00:00
|
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the BSD-style license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
*
|
2015-03-26 18:24:15 +00:00
|
|
|
* The examples provided by Facebook are for non-commercial testing and
|
|
|
|
* evaluation purposes only.
|
2015-03-23 22:07:33 +00:00
|
|
|
*
|
2015-03-26 18:24:15 +00:00
|
|
|
* Facebook reserves all rights not expressly granted.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
|
|
|
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
2015-02-19 01:39:09 +00:00
|
|
|
*
|
2015-12-02 02:03:58 +00:00
|
|
|
* @provides ListViewPagingExample
|
2015-02-19 01:39:09 +00:00
|
|
|
* @flow
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
2016-04-09 03:36:40 +00:00
|
|
|
var React = require('react');
|
|
|
|
var ReactNative = require('react-native');
|
2015-02-19 01:39:09 +00:00
|
|
|
var {
|
|
|
|
Image,
|
|
|
|
LayoutAnimation,
|
|
|
|
ListView,
|
|
|
|
StyleSheet,
|
|
|
|
Text,
|
|
|
|
TouchableOpacity,
|
|
|
|
View,
|
2016-04-09 03:36:40 +00:00
|
|
|
} = ReactNative;
|
2015-02-19 01:39:09 +00:00
|
|
|
|
2015-12-02 02:03:58 +00:00
|
|
|
var NativeModules = require('NativeModules');
|
|
|
|
var {
|
|
|
|
UIManager,
|
|
|
|
} = NativeModules;
|
|
|
|
|
2015-11-17 16:48:22 +00:00
|
|
|
var THUMB_URLS = [
|
2015-12-30 22:49:13 +00:00
|
|
|
require('./Thumbnails/like.png'),
|
|
|
|
require('./Thumbnails/dislike.png'),
|
|
|
|
require('./Thumbnails/call.png'),
|
|
|
|
require('./Thumbnails/fist.png'),
|
|
|
|
require('./Thumbnails/bandaged.png'),
|
|
|
|
require('./Thumbnails/flowers.png'),
|
|
|
|
require('./Thumbnails/heart.png'),
|
|
|
|
require('./Thumbnails/liking.png'),
|
|
|
|
require('./Thumbnails/party.png'),
|
|
|
|
require('./Thumbnails/poke.png'),
|
|
|
|
require('./Thumbnails/superlike.png'),
|
|
|
|
require('./Thumbnails/victory.png'),
|
|
|
|
];
|
2015-02-19 01:39:09 +00:00
|
|
|
var NUM_SECTIONS = 100;
|
|
|
|
var NUM_ROWS_PER_SECTION = 10;
|
|
|
|
|
|
|
|
var Thumb = React.createClass({
|
|
|
|
getInitialState: function() {
|
|
|
|
return {thumbIndex: this._getThumbIdx(), dir: 'row'};
|
|
|
|
},
|
2015-12-02 02:03:58 +00:00
|
|
|
componentWillMount: function() {
|
|
|
|
UIManager.setLayoutAnimationEnabledExperimental &&
|
|
|
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
|
|
},
|
2015-02-19 01:39:09 +00:00
|
|
|
_getThumbIdx: function() {
|
|
|
|
return Math.floor(Math.random() * THUMB_URLS.length);
|
|
|
|
},
|
|
|
|
_onPressThumb: function() {
|
|
|
|
var config = layoutAnimationConfigs[this.state.thumbIndex % layoutAnimationConfigs.length];
|
|
|
|
LayoutAnimation.configureNext(config);
|
|
|
|
this.setState({
|
|
|
|
thumbIndex: this._getThumbIdx(),
|
|
|
|
dir: this.state.dir === 'row' ? 'column' : 'row',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
render: function() {
|
|
|
|
return (
|
2015-07-20 23:29:40 +00:00
|
|
|
<TouchableOpacity
|
|
|
|
onPress={this._onPressThumb}
|
|
|
|
style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
|
2015-12-30 22:49:13 +00:00
|
|
|
<Image style={styles.img} source={THUMB_URLS[this.state.thumbIndex]} />
|
|
|
|
<Image style={styles.img} source={THUMB_URLS[this.state.thumbIndex]} />
|
|
|
|
<Image style={styles.img} source={THUMB_URLS[this.state.thumbIndex]} />
|
2015-07-20 23:29:40 +00:00
|
|
|
{this.state.dir === 'column' ?
|
|
|
|
<Text>
|
|
|
|
Oooo, look at this new text! So awesome it may just be crazy.
|
|
|
|
Let me keep typing here so it wraps at least one line.
|
|
|
|
</Text> :
|
|
|
|
<Text />
|
|
|
|
}
|
2015-02-19 01:39:09 +00:00
|
|
|
</TouchableOpacity>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var ListViewPagingExample = React.createClass({
|
|
|
|
statics: {
|
|
|
|
title: '<ListView> - Paging',
|
|
|
|
description: 'Floating headers & layout animations.'
|
|
|
|
},
|
|
|
|
|
|
|
|
getInitialState: function() {
|
|
|
|
var getSectionData = (dataBlob, sectionID) => {
|
|
|
|
return dataBlob[sectionID];
|
|
|
|
};
|
|
|
|
var getRowData = (dataBlob, sectionID, rowID) => {
|
|
|
|
return dataBlob[rowID];
|
|
|
|
};
|
|
|
|
|
2015-03-03 16:38:50 +00:00
|
|
|
var dataSource = new ListView.DataSource({
|
2015-02-19 01:39:09 +00:00
|
|
|
getRowData: getRowData,
|
|
|
|
getSectionHeaderData: getSectionData,
|
|
|
|
rowHasChanged: (row1, row2) => row1 !== row2,
|
|
|
|
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
|
|
|
|
});
|
|
|
|
|
|
|
|
var dataBlob = {};
|
|
|
|
var sectionIDs = [];
|
|
|
|
var rowIDs = [];
|
|
|
|
for (var ii = 0; ii < NUM_SECTIONS; ii++) {
|
|
|
|
var sectionName = 'Section ' + ii;
|
|
|
|
sectionIDs.push(sectionName);
|
|
|
|
dataBlob[sectionName] = sectionName;
|
|
|
|
rowIDs[ii] = [];
|
|
|
|
|
|
|
|
for (var jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
|
|
|
|
var rowName = 'S' + ii + ', R' + jj;
|
|
|
|
rowIDs[ii].push(rowName);
|
|
|
|
dataBlob[rowName] = rowName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
|
|
|
|
headerPressCount: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-05-25 01:20:12 +00:00
|
|
|
renderRow: function(rowData: string, sectionID: string, rowID: string): ReactElement<any> {
|
2015-02-19 01:39:09 +00:00
|
|
|
return (<Thumb text={rowData}/>);
|
|
|
|
},
|
|
|
|
|
2015-03-18 22:57:49 +00:00
|
|
|
renderSectionHeader: function(sectionData: string, sectionID: string) {
|
2015-02-19 01:39:09 +00:00
|
|
|
return (
|
|
|
|
<View style={styles.section}>
|
|
|
|
<Text style={styles.text}>
|
|
|
|
{sectionData}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
renderHeader: function() {
|
|
|
|
var headerLikeText = this.state.headerPressCount % 2 ?
|
|
|
|
<View><Text style={styles.text}>1 Like</Text></View> :
|
|
|
|
null;
|
|
|
|
return (
|
2015-07-20 23:29:40 +00:00
|
|
|
<TouchableOpacity onPress={this._onPressHeader} style={styles.header}>
|
|
|
|
{headerLikeText}
|
|
|
|
<View>
|
|
|
|
<Text style={styles.text}>
|
|
|
|
Table Header (click me)
|
|
|
|
</Text>
|
2015-02-19 01:39:09 +00:00
|
|
|
</View>
|
|
|
|
</TouchableOpacity>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
renderFooter: function() {
|
|
|
|
return (
|
|
|
|
<View style={styles.header}>
|
|
|
|
<Text onPress={() => console.log('Footer!')} style={styles.text}>
|
|
|
|
Table Footer
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
render: function() {
|
|
|
|
return (
|
|
|
|
<ListView
|
|
|
|
style={styles.listview}
|
|
|
|
dataSource={this.state.dataSource}
|
|
|
|
onChangeVisibleRows={(visibleRows, changedRows) => console.log({visibleRows, changedRows})}
|
|
|
|
renderHeader={this.renderHeader}
|
|
|
|
renderFooter={this.renderFooter}
|
|
|
|
renderSectionHeader={this.renderSectionHeader}
|
|
|
|
renderRow={this.renderRow}
|
|
|
|
initialListSize={10}
|
Fixed missing rows on UIExplorer <ListView> - Grid Layout example
Summary:
public
I was looking into the missing panels at the bottom of the <ListView> - Grid Layout example, and found that it was caused by several problems, some in the example and some in ListView itself.
The first problem seemed to be a bug in the `_getDistanceFromEnd()` method, which calculates whether the ListView needs to load more content based on the distance of the visible content from the bottom of the scrollview. This was previously using the function
Math.max(scrollProperties.contentLength, scrollProperties.visibleLength) - scrollProperties.visibleLength - scrollProperties.offset
to calculate the amount the user could scroll before they run out of content. This sort-of works in most cases because `scrollProperties.contentLength` is usually longer than `scrollProperties.visibleLength`, so this would generally evaluate to
scrollProperties.contentLength - scrollProperties.visibleLength - scrollProperties.offset
which meant that it would be positive as long as there was content still to be displayed offscreen, and negative when you reached the end of the content. This logic breaks down if `contentLength` is less than `visibleLength`, however. For example, if you have 300pts of content loaded, and your scrollView is 500pts tall, and your scroll position is zero, this evaluates to
Math.max(300, 500) - 500 - 0 = 0
In other words, the algorithm is saying that you have zero pts of scroll content remaining before you need to reload. But actually, the bottom 200pts of the screen are empty, so you're really 200pts in debt, and need to load extra rows to fill that space. The correct algorithm is simply to get rid of the `Math.max` and just use
scrollProperties.contentLength - scrollProperties.visibleLength - scrollProperties.offset
I originally thought that this was the cause of the gap, but it isn't, because ListView has `DEFAULT_SCROLL_RENDER_AHEAD = 1000`, which means that it tries to load at least 1000pts more content than is currently visible, to avoid gaps. This masked the bug, so in practice it wasn't causing an issue.
The next problem I found was that there is an implict assumption in ListView that the first page of content you load is sufficient to cover the screen, or rather, that the first _ second page is sufficient. The constants `DEFAULT_INITIAL_ROWS = 10` and `DEFAULT_PAGE_SIZE = 1`, mean that when the ListView first loads, the following happens:
1. It loads 10 rows of content.
2. It checks if `_getDistanceFromEnd() < DEFAULT_SCROLL_RENDER_AHEAD` (1000).
3. If it is, it loads another `DEFAULT_PAGE_SIZE` rows of content, then stops.
In the case of the ListView Grid Layout example, this meant that it first loaded 10 cells, then loaded another 1, for a total of 11. The problem was that going from 10 to 11 cells isn't sufficient to fill the visible scroll area, and it doesn't change the `contentSize` (since the cells wrap onto the same line), and since ListView doesn't try to load any more until the `contentSize` or `scrollOffset ` changes, it stops loading new rows at that point.
I tried fixing this by calling `_renderMoreRowsIfNeeded()` after `_pageInNewRows()` so that it will continue to fetch new rows until the `_getDistanceFromEnd()` is less than the threshold, rather than stopping after the first page and waiting until the `contentSize` or `scrollOffset` change, but although this solves the problem for the Grid Layout example, it leads to over-fetching in the more common case of a standard row-based ListView.
In the end, I just increased the `pageSize` to 3 for the Grid Layout example, which makes more sense anyway since loading a page that is not a multiple of the number of cells per row confuses the `_renderMoreRowsIfNeeded` algorithm, and leads to gaps at the bottom of the view.
This solved the problem, however there was still a "pop-in" effect, where the additional rows were paged in after the ListView appeared. This was simply a misconfiguration in the example itself: The default of 10 rows was insufficient to fill the screen, so I changed the `initialListSize` prop to `20`.
Reviewed By: javache
Differential Revision: D2911690
fb-gh-sync-id: 8d6bd78843335fb091e7e24f7c2e6a416b0321d3
shipit-source-id: 8d6bd78843335fb091e7e24f7c2e6a416b0321d3
2016-02-10 16:36:15 +00:00
|
|
|
pageSize={4}
|
|
|
|
scrollRenderAheadDistance={500}
|
2015-02-19 01:39:09 +00:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
_onPressHeader: function() {
|
|
|
|
var config = layoutAnimationConfigs[Math.floor(this.state.headerPressCount / 2) % layoutAnimationConfigs.length];
|
|
|
|
LayoutAnimation.configureNext(config);
|
|
|
|
this.setState({headerPressCount: this.state.headerPressCount + 1});
|
|
|
|
},
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
var styles = StyleSheet.create({
|
|
|
|
listview: {
|
|
|
|
backgroundColor: '#B0C4DE',
|
|
|
|
},
|
|
|
|
header: {
|
|
|
|
height: 40,
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'center',
|
|
|
|
backgroundColor: '#3B5998',
|
|
|
|
flexDirection: 'row',
|
|
|
|
},
|
|
|
|
text: {
|
|
|
|
color: 'white',
|
|
|
|
paddingHorizontal: 8,
|
|
|
|
},
|
|
|
|
rowText: {
|
|
|
|
color: '#888888',
|
|
|
|
},
|
|
|
|
thumbText: {
|
|
|
|
fontSize: 20,
|
|
|
|
color: '#888888',
|
|
|
|
},
|
|
|
|
buttonContents: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'center',
|
|
|
|
marginHorizontal: 5,
|
|
|
|
marginVertical: 3,
|
|
|
|
padding: 5,
|
|
|
|
backgroundColor: '#EAEAEA',
|
|
|
|
borderRadius: 3,
|
|
|
|
paddingVertical: 10,
|
|
|
|
},
|
|
|
|
img: {
|
|
|
|
width: 64,
|
|
|
|
height: 64,
|
|
|
|
marginHorizontal: 10,
|
2015-07-07 21:14:14 +00:00
|
|
|
backgroundColor: 'transparent',
|
2015-02-19 01:39:09 +00:00
|
|
|
},
|
|
|
|
section: {
|
|
|
|
flexDirection: 'column',
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
padding: 6,
|
|
|
|
backgroundColor: '#5890ff',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
var animations = {
|
|
|
|
layout: {
|
|
|
|
spring: {
|
2015-03-31 03:12:32 +00:00
|
|
|
duration: 750,
|
2015-02-19 01:39:09 +00:00
|
|
|
create: {
|
2015-03-31 03:12:32 +00:00
|
|
|
duration: 300,
|
2015-02-19 01:39:09 +00:00
|
|
|
type: LayoutAnimation.Types.easeInEaseOut,
|
|
|
|
property: LayoutAnimation.Properties.opacity,
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
type: LayoutAnimation.Types.spring,
|
|
|
|
springDamping: 0.4,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
easeInEaseOut: {
|
2015-03-31 03:12:32 +00:00
|
|
|
duration: 300,
|
2015-02-19 01:39:09 +00:00
|
|
|
create: {
|
|
|
|
type: LayoutAnimation.Types.easeInEaseOut,
|
|
|
|
property: LayoutAnimation.Properties.scaleXY,
|
|
|
|
},
|
|
|
|
update: {
|
2015-03-31 03:12:32 +00:00
|
|
|
delay: 100,
|
2015-02-19 01:39:09 +00:00
|
|
|
type: LayoutAnimation.Types.easeInEaseOut,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
var layoutAnimationConfigs = [
|
|
|
|
animations.layout.spring,
|
|
|
|
animations.layout.easeInEaseOut,
|
|
|
|
];
|
|
|
|
|
|
|
|
module.exports = ListViewPagingExample;
|