diff --git a/examples/gridview/app.js b/examples/gridview/app.js new file mode 100644 index 0000000..cf9f1d5 --- /dev/null +++ b/examples/gridview/app.js @@ -0,0 +1,67 @@ +'use strict'; + +var React = require('react'); +var ReactCanvas = require('react-canvas'); +var Item = require('./components/Item'); +var articles = require('../common/data'); + +var Surface = ReactCanvas.Surface; +var GridView = ReactCanvas.GridView; + +var App = React.createClass({ + render: function () { + var size = this.getSize(); + return ( + + + + ); + }, + + renderItem: function (itemIndex, scrollTop, scrollLeft) { + var article = articles[itemIndex % articles.length]; + return ( + + ); + }, + + getSize: function () { + return document.getElementById('main').getBoundingClientRect(); + }, + + + // GridView + // ======== + + getGridViewStyle: function () { + var size = this.getSize(); + return { + top: 0, + left: 0, + width: size.width, + height: size.height, + backgroundColor: '#fff' + }; + }, + + getNumberOfItems: function () { + return 100; + }, + + getNumberOfColumns: function () { + return 4; + } +}); + +React.render(, document.getElementById('main')); diff --git a/examples/gridview/components/Item.js b/examples/gridview/components/Item.js new file mode 100644 index 0000000..2c185c5 --- /dev/null +++ b/examples/gridview/components/Item.js @@ -0,0 +1,75 @@ +/** @jsx React.DOM */ + +'use strict'; + +var React = require('react'); +var ReactCanvas = require('react-canvas'); + +var Group = ReactCanvas.Group; +var Image = ReactCanvas.Image; +var Text = ReactCanvas.Text; + +var Item = React.createClass({ + + propTypes: { + width: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + imageUrl: React.PropTypes.string.isRequired, + title: React.PropTypes.string.isRequired, + itemIndex: React.PropTypes.number.isRequired, + }, + + statics: { + getItemHeight: function () { + return 180; + }, + getItemWidth: function () { + return 180; + }, + }, + + render: function () { + return ( + + + {this.props.title} + + ); + }, + + getStyle: function () { + return { + width: this.props.width, + height: this.props.height, + backgroundColor: (this.props.itemIndex % 2) ? '#eee' : '#a5d2ee', + borderColor: '#999', + borderWidth: 1 + }; + }, + + getImageStyle: function () { + return { + top: 10, + left: 10, + width: 60, + height: 60, + backgroundColor: '#ddd', + borderColor: '#999', + borderWidth: 1 + }; + }, + + getTitleStyle: function () { + return { + top: 10, + left: 80, + width: this.props.width - 90, + height: this.props.height, + fontSize: 12, + lineHeight: 18, + }; + } + +}); + +module.exports = Item; diff --git a/examples/gridview/index.html b/examples/gridview/index.html new file mode 100644 index 0000000..28d8687 --- /dev/null +++ b/examples/gridview/index.html @@ -0,0 +1,17 @@ + + + + + + ReactCanvas: GridView + + + + + +
+ + + diff --git a/lib/GridView.js b/lib/GridView.js new file mode 100644 index 0000000..2ef7801 --- /dev/null +++ b/lib/GridView.js @@ -0,0 +1,217 @@ +'use strict'; + +var React = require('react'); +var assign = require('react/lib/Object.assign'); +var Scroller = require('scroller'); +var Group = require('./Group'); +var clamp = require('./clamp'); + +var GridView = React.createClass({ + + propTypes: { + style: React.PropTypes.object, + numberOfItemsGetter: React.PropTypes.func.isRequired, + itemHeightGetter: React.PropTypes.func.isRequired, + itemWidthGetter: React.PropTypes.func.isRequired, + itemGetter: React.PropTypes.func.isRequired, + numberOfColumnsGetter: React.PropTypes.func.isRequired, + snapping: React.PropTypes.bool, + scrollingDeceleration: React.PropTypes.number, + scrollingPenetrationAcceleration: React.PropTypes.number, + onScroll: React.PropTypes.func + }, + + getDefaultProps: function () { + return { + style: { left: 0, top: 0, width: 0, height: 0 }, + snapping: false, + scrollingDeceleration: 0.95, + scrollingPenetrationAcceleration: 0.08 + }; + }, + + getInitialState: function () { + return { + scrollTop: 0, + scrollLeft: 0, + }; + }, + + componentDidMount: function () { + this.createScroller(); + this.updateScrollingDimensions(); + }, + + render: function () { + var items = this.getVisibleItemIndexes().map(this.renderItem); + return ( + React.createElement(Group, { + style: this.props.style, + onTouchStart: this.handleTouchStart, + onTouchMove: this.handleTouchMove, + onTouchEnd: this.handleTouchEnd, + onTouchCancel: this.handleTouchEnd}, + items + ) + ); + }, + + renderItem: function (itemIndex) { + var scrollTop = this.state.scrollTop; + var scrollLeft = this.state.scrollLeft; + var item = this.props.itemGetter(itemIndex, scrollTop, scrollLeft); + var itemHeight = this.props.itemHeightGetter(); + var itemWidth = this.props.itemWidthGetter(); + var columnCount = this.props.numberOfColumnsGetter(); + var row = Math.floor(itemIndex / columnCount); + var column = itemIndex % columnCount; + var style = { + top: 0, + left: 0, + width: itemWidth, + height: itemHeight, + translateY: (row * itemHeight) - scrollTop, + translateX: (column * itemWidth) - scrollLeft, + zIndex: itemIndex + }; + + return ( + React.createElement(Group, {style: style, key: itemIndex}, + item + ) + ); + }, + + // Events + // ====== + + handleTouchStart: function (e) { + if (this.scroller) { + this.scroller.doTouchStart(e.touches, e.timeStamp || Date.now()); + } + }, + + handleTouchMove: function (e) { + if (this.scroller) { + e.preventDefault(); + this.scroller.doTouchMove(e.touches, e.timeStamp || Date.now(), e.scale); + } + }, + + handleTouchEnd: function (e) { + if (this.scroller) { + this.scroller.doTouchEnd(e.timeStamp || Date.now()); + if (this.props.snapping) { + this.updateScrollingDeceleration(); + } + } + }, + + handleScroll: function (left, top) { + this.setState({ scrollTop: top, scrollLeft: left }); + if (this.props.onScroll) { + this.props.onScroll(top, left); + } + }, + + // Scrolling + // ========= + + createScroller: function () { + var options = { + scrollingX: true, + scrollingY: true, + decelerationRate: this.props.scrollingDeceleration, + penetrationAcceleration: this.props.scrollingPenetrationAcceleration, + }; + this.scroller = new Scroller(this.handleScroll, options); + }, + + updateScrollingDimensions: function () { + var width = this.props.style.width; + var height = this.props.style.height; + var numberOfItems = this.props.numberOfItemsGetter(); + var numberOfColumns = this.props.numberOfColumnsGetter(); + var rows = Math.floor(numberOfItems / numberOfColumns); + var scrollWidth = numberOfColumns * this.props.itemWidthGetter(); + var scrollHeight = rows * this.props.itemHeightGetter(); + this.scroller.setDimensions(width, height, scrollWidth, scrollHeight); + }, + + getVisibleItemIndexes: function () { + var itemIndexes = []; + var itemHeight = this.props.itemHeightGetter(); + var itemWidth = this.props.itemWidthGetter(); + var itemCount = this.props.numberOfItemsGetter(); + var columnCount = this.props.numberOfColumnsGetter(); + var scrollTop = this.state.scrollTop; + var scrollLeft = this.state.scrollLeft; + var itemScrollTop = 0; + var itemScrollLeft = 0; + + for (var index=0; index < itemCount; index++) { + var row = Math.floor(index / columnCount); + var column = index % columnCount; + + itemScrollTop = (row * itemHeight) - scrollTop; + itemScrollLeft = (column * itemWidth) - scrollLeft; + + // Item is completely off-screen bottom + if (itemScrollTop >= this.props.style.height) { + continue; + } + + // Item is completely off-screen top + if (itemScrollTop <= -this.props.style.height) { + continue; + } + + // Item is completely off-screen right + if (itemScrollLeft >= this.props.style.width) { + continue; + } + + // Item is completely off-screen left + if (itemScrollLeft <= -this.props.style.width) { + continue; + } + + // Part of item is on-screen. + itemIndexes.push(index); + } + + return itemIndexes; + }, + + updateScrollingDeceleration: function () { + var currVelocity = this.scroller.__decelerationVelocityY; + var currScrollTop = this.state.scrollTop; + var targetScrollTop = 0; + var estimatedEndScrollTop = currScrollTop; + + while (Math.abs(currVelocity).toFixed(6) > 0) { + estimatedEndScrollTop += currVelocity; + currVelocity *= this.props.scrollingDeceleration; + } + + // Find the page whose estimated end scrollTop is closest to 0. + var closestZeroDelta = Infinity; + var pageHeight = this.props.itemHeightGetter(); + var pageCount = this.props.numberOfItemsGetter(); + var pageScrollTop; + + for (var pageIndex=0, len=pageCount; pageIndex < len; pageIndex++) { + pageScrollTop = (pageHeight * pageIndex) - estimatedEndScrollTop; + if (Math.abs(pageScrollTop) < closestZeroDelta) { + closestZeroDelta = Math.abs(pageScrollTop); + targetScrollTop = pageHeight * pageIndex; + } + } + + this.scroller.__minDecelerationScrollTop = targetScrollTop; + this.scroller.__maxDecelerationScrollTop = targetScrollTop; + } + +}); + +module.exports = GridView; diff --git a/lib/ReactCanvas.js b/lib/ReactCanvas.js index ba4dd79..d4c2eb0 100644 --- a/lib/ReactCanvas.js +++ b/lib/ReactCanvas.js @@ -8,6 +8,7 @@ var ReactCanvas = { Image: require('./Image'), Text: require('./Text'), ListView: require('./ListView'), + GridView: require('./GridView'), FontFace: require('./FontFace'), measureText: require('./measureText') diff --git a/webpack.config.js b/webpack.config.js index b82ceef..9a75726 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ module.exports = { entry: { 'listview': ['./examples/listview/app.js'], + 'gridview': ['./examples/gridview/app.js'], 'timeline': ['./examples/timeline/app.js'], 'css-layout': ['./examples/css-layout/app.js'] },