diff --git a/DataRepository.js b/DataRepository.js
index 3ca1f9d..0a249af 100644
--- a/DataRepository.js
+++ b/DataRepository.js
@@ -7,7 +7,7 @@ var {
} = React;
var API_COVER_URL = "http://news-at.zhihu.com/api/4/start-image/1080*1776";
-var API_LATEST_URL = 'http://news.at.zhihu.com/api/4/news/latest';
+var API_LATEST_URL = 'http://news-at.zhihu.com/api/4/news/latest';
var API_HOME_URL = 'http://news.at.zhihu.com/api/4/news/before/';
var API_THEME_URL = 'http://news-at.zhihu.com/api/4/theme/';
var API_THEMES_URL = 'http://news-at.zhihu.com/api/4/themes';
@@ -16,6 +16,7 @@ var KEY_COVER = '@Cover';
var KEY_THEMES = '@Themes:';
var KEY_HOME_LIST = '@HomeList:';
var KEY_THEME_LIST = '@ThemeList:';
+var KEY_THEME_TOPDATA = '@ThemeTop:';
function parseDateFromYYYYMMdd(str) {
if (!str) return new Date();
@@ -85,18 +86,24 @@ DataRepository.prototype.updateCover = function() {
DataRepository.prototype.fetchStories = function(date?: Date,
callback?: ?(error: ?Error, result: ?Object) => void
) {
+ var reqUrl;
+ var topData = null;
if (!date) {
date = new Date();
- };
+ reqUrl = API_LATEST_URL;
+ topData = this._safeStorage(KEY_THEME_TOPDATA);
+ } else {
+ var beforeDate = new Date(date);
+ beforeDate.setDate(date.getDate() + 1);
+ reqUrl = API_HOME_URL + beforeDate.yyyymmdd();
+ }
var localStorage = this._safeStorage(KEY_HOME_LIST + date.yyyymmdd());
- var beforeDate = new Date(date);
- beforeDate.setDate(date.getDate() + 1);
- var networking = this._safeFetch(API_HOME_URL + beforeDate.yyyymmdd());
+ var networking = this._safeFetch(reqUrl);
var merged = new Promise((resolve, reject) => {
- Promise.all([localStorage, networking])
+ Promise.all([localStorage, networking, topData])
.then((values) => {
var error, result;
result = this._mergeReadState(values[0], values[1]);
@@ -107,6 +114,11 @@ DataRepository.prototype.fetchStories = function(date?: Date,
if (error) {
reject(error);
} else {
+ if (values[1] && values[1].top_stories) {
+ result.topData = values[1].top_stories;
+ } else {
+ result.topData = values[2];
+ }
resolve(result);
}
});
@@ -114,7 +126,7 @@ DataRepository.prototype.fetchStories = function(date?: Date,
return merged;
};
-DataRepository.prototype.fetchThemeStories = function(themeId: Number, lastID?: string,
+DataRepository.prototype.fetchThemeStories = function(themeId: number, lastID?: string,
callback?: ?(error: ?Error, result: ?Object) => void
) {
// Home story list
@@ -123,10 +135,7 @@ DataRepository.prototype.fetchThemeStories = function(themeId: Number, lastID?:
if (lastID) {
date = parseDateFromYYYYMMdd(lastID);
date.setDate(date.getDate() - 1);
- } else {
- date = new Date();
}
-
return this.fetchStories(date, callback);
}
@@ -135,14 +144,17 @@ DataRepository.prototype.fetchThemeStories = function(themeId: Number, lastID?:
var localStorage = isRefresh ? this._safeStorage(KEY_THEME_LIST + themeId) : null;
var reqUrl = API_THEME_URL + themeId;
+ var topData = null;
if (lastID) {
reqUrl += '/before/' + lastID;
+ } else {
+ topData = this._safeStorage(KEY_THEME_TOPDATA + themeId);
}
var networking = this._safeFetch(reqUrl);
var merged = new Promise((resolve, reject) => {
- Promise.all([localStorage, networking])
+ Promise.all([localStorage, networking, topData])
.then((values) => {
var error, result;
result = this._mergeReadState(values[0], values[1]);
@@ -153,6 +165,16 @@ DataRepository.prototype.fetchThemeStories = function(themeId: Number, lastID?:
if (error) {
reject(error);
} else {
+ var topDataRet;
+ if (values[1] && values[1].editors) {
+ topDataRet = {};
+ topDataRet.description = values[1].description;
+ topDataRet.background = values[1].background;
+ topDataRet.editors = values[1].editors;
+ } else {
+ topDataRet = values[2];
+ }
+ result.topData = topDataRet;
resolve(result);
}
});
@@ -161,7 +183,7 @@ DataRepository.prototype.fetchThemeStories = function(themeId: Number, lastID?:
return merged;
};
-DataRepository.prototype.saveStories = function(themeList: object,
+DataRepository.prototype.saveStories = function(themeList: object, topData: object,
callback?: ?(error: ?Error, result: ?Object) => void
) {
var homeList = themeList[0];
@@ -182,6 +204,14 @@ DataRepository.prototype.saveStories = function(themeList: object,
}
}
}
+
+ for (var theme in topData) {
+ if (topData.hasOwnProperty(theme)) {
+ //console.log(theme, topData[key]);
+ keyValuePairs.push([KEY_THEME_TOPDATA + theme, JSON.stringify(topData[theme])]);
+ }
+ }
+
AsyncStorage.multiSet(keyValuePairs, callback);
};
diff --git a/DefaultViewPageIndicator.js b/DefaultViewPageIndicator.js
new file mode 100644
index 0000000..e3936a8
--- /dev/null
+++ b/DefaultViewPageIndicator.js
@@ -0,0 +1,102 @@
+'use strict';
+
+var React = require('react-native');
+var {
+ Dimensions,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+ Animated,
+} = React;
+
+var deviceWidth = Dimensions.get('window').width;
+var DOT_SIZE = 6;
+var DOT_SAPCE = 3;
+
+var styles = StyleSheet.create({
+ tab: {
+ alignItems: 'center',
+ },
+
+ tabs: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+
+ dot: {
+ width: DOT_SIZE,
+ height: DOT_SIZE,
+ borderRadius: DOT_SIZE / 2,
+ backgroundColor: 'rgba(100,100,100,0.5)',
+ marginLeft: DOT_SAPCE,
+ marginRight: DOT_SAPCE,
+ },
+
+ curDot: {
+ position: 'absolute',
+ width: DOT_SIZE,
+ height: DOT_SIZE,
+ borderRadius: DOT_SIZE / 2,
+ backgroundColor: 'rgba(250,250,250,0.8)',
+ margin: DOT_SAPCE,
+ bottom: 0,
+ },
+});
+
+var DefaultViewPageIndicator = React.createClass({
+ propTypes: {
+ goToPage: React.PropTypes.func,
+ activePage: React.PropTypes.number,
+ pageCount: React.PropTypes.number
+ },
+
+ getInitialState() {
+ return {
+ viewWidth: 0,
+ };
+ },
+
+ renderIndicator(page) {
+ //var isTabActive = this.props.activePage === page;
+ return (
+ this.props.goToPage(page)}>
+
+
+ );
+ },
+
+ render() {
+ var pageCount = this.props.pageCount;
+ var itemWidth = DOT_SIZE + (DOT_SAPCE * 2);
+ var offset = (this.state.viewWidth - itemWidth * pageCount) / 2 + itemWidth * this.props.activePage;
+
+ var left = offset; /*this.state.offsetX.interpolate({
+ inputRange: [0, 1], outputRange: [0, offset]
+ });*/
+
+ var indicators = [];
+ for (var i = 0; i < pageCount; i++) {
+ indicators.push(this.renderIndicator(i))
+ }
+
+ return (
+ {
+ var viewWidth = event.nativeEvent.layout.width;
+ if (!viewWidth || this.state.viewWidth === viewWidth) {
+ return;
+ }
+ this.setState({
+ viewWidth: viewWidth,
+ });
+ }}>
+ {indicators}
+
+
+ );
+ },
+});
+
+module.exports = DefaultViewPageIndicator;
diff --git a/ListScreen.js b/ListScreen.js
index 44ba74e..1cd7371 100644
--- a/ListScreen.js
+++ b/ListScreen.js
@@ -21,6 +21,7 @@ var StoryItem = require('./StoryItem');
var ThemesList = require('./ThemesList');
var DataRepository = require('./DataRepository');
var SwipeRefreshLayoutAndroid = require('./SwipeRereshLayout');
+var ViewPager = require('./ViewPager');
var API_LATEST_URL = 'http://news.at.zhihu.com/api/4/news/latest';
var API_HISTORY_URL = 'http://news.at.zhihu.com/api/4/news/before/';
@@ -44,6 +45,7 @@ var repository = new DataRepository();
var dataCache = {
dataForTheme: {},
+ topDataForTheme: {},
sectionsForTheme: {},
lastID: {},
};
@@ -68,18 +70,23 @@ var ListScreen = React.createClass({
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
+ var headerDataSource = new ViewPager.DataSource({
+ pageHasChanged: (p1, p2) => p1 !== p2,
+ });
+
return {
isLoading: false,
isLoadingTail: false,
theme: null,
dataSource: dataSource,
+ headerDataSource: headerDataSource,
};
},
componentWillMount: function() {
BackAndroid.addEventListener('hardwareBackPress', this._handleBackButtonPress);
},
componentWillUnmount: function() {
- repository.saveStories(dataCache.dataForTheme);
+ repository.saveStories(dataCache.dataForTheme, dataCache.topDataForTheme);
},
_handleBackButtonPress: function() {
if (this.state.theme) {
@@ -101,6 +108,7 @@ var ListScreen = React.createClass({
dataBlob = isInTheme ? [] : {};
}
var sectionIDs = dataCache.sectionsForTheme[themeId];
+ var topData = dataCache.topDataForTheme[themeId];
this.setState({
isLoading: isRefresh,
@@ -113,6 +121,7 @@ var ListScreen = React.createClass({
.then((responseData) => {
var newLastID;
var dataSouce;
+ var headerDataSource = this.state.headerDataSource;
if (!isInTheme) {
newLastID = responseData.date;
var newDataBlob = {};
@@ -131,7 +140,14 @@ var ListScreen = React.createClass({
dataBlob = newDataBlob;
sectionIDs = newSectionIDs;
+ console.log(responseData);
+ if (isRefresh && responseData.topData) {
+ topData = responseData.topData;
+ headerDataSource = headerDataSource.cloneWithPages(topData.slice())
+ }
+
dataSouce = this.state.dataSource.cloneWithRowsAndSections(newDataBlob, newSectionIDs, null);
+
} else {
var length = responseData.stories.length;
if (length > 0) {
@@ -159,6 +175,7 @@ var ListScreen = React.createClass({
isLoadingTail: (isRefresh ? this.state.isLoadingTail : false),
theme: this.state.theme,
dataSource: dataSouce,
+ headerDataSource: headerDataSource,
});
this.swipeRefreshLayout && this.swipeRefreshLayout.finishRefresh();
@@ -175,6 +192,36 @@ var ListScreen = React.createClass({
})
.done();
},
+ _renderPage: function(
+ story: Object,
+ pageID: number | string,) {
+ return (
+
+
+
+ {story.title}
+
+
+
+ )
+ },
+ _renderHeader: function() {
+ if (this.state.theme) {
+
+ } else {
+ return (
+
+
+
+ );
+ }
+ },
getSectionTitle: function(str) {
var date = parseDateFromYYYYMMdd(str);
if (date.toDateString() == new Date().toDateString()) {
@@ -273,6 +320,7 @@ var ListScreen = React.createClass({
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps={true}
showsVerticalScrollIndicator={false}
+ renderHeader={this._renderHeader}
/>;
var title = this.state.theme ? this.state.theme.name : '首页';
return (
@@ -330,6 +378,27 @@ var styles = StyleSheet.create({
color: '#888888',
margin: 10,
marginLeft: 16,
+ },
+ headerPager: {
+ height: 200,
+ },
+ headerItem: {
+ flex: 1,
+ height: 200,
+ flexDirection: 'row',
+ },
+ headerTitleContainer: {
+ flex: 1,
+ alignSelf: 'flex-end',
+ padding: 10,
+ backgroundColor: 'rgba(0,0,0,0.2)',
+ },
+ headerTitle: {
+ flex: 1,
+ fontSize: 18,
+ fontWeight: '500',
+ color: 'white',
+ marginBottom: 10,
}
});
diff --git a/ViewPager.js b/ViewPager.js
new file mode 100644
index 0000000..8b05480
--- /dev/null
+++ b/ViewPager.js
@@ -0,0 +1,257 @@
+'use strict';
+
+var React = require('react-native');
+var {
+ Dimensions,
+ Text,
+ View,
+ TouchableOpacity,
+ PanResponder,
+ Animated,
+ PropTypes,
+ StyleSheet,
+} = React;
+
+var StaticRenderer = require('StaticRenderer');
+
+var DefaultViewPageIndicator = require('./DefaultViewPageIndicator');
+var deviceWidth = Dimensions.get('window').width;
+var ViewPagerDataSource = require('./ViewPagerDataSource');
+
+var ViewPager = React.createClass({
+
+ statics: {
+ DataSource: ViewPagerDataSource,
+ },
+
+ propTypes: {
+ ...View.propTypes,
+ dataSource: PropTypes.instanceOf(ViewPagerDataSource).isRequired,
+ renderPage: PropTypes.func.isRequired,
+ isLoop: PropTypes.bool,
+ locked: PropTypes.bool,
+ },
+
+ getDefaultProps() {
+ return {
+ isLoop: false,
+ locked: false,
+ }
+ },
+
+ getInitialState() {
+ return {
+ currentPage: 0,
+ viewWidth: 0,
+ childIndex: 0,
+ scrollValue: new Animated.Value(0)
+ };
+ },
+
+ componentWillMount() {
+ var release = (e, gestureState) => {
+ var relativeGestureDistance = gestureState.dx / deviceWidth,
+ //lastPageIndex = this.props.children.length - 1,
+ vx = gestureState.vx;
+
+ var step = 0;
+ if (relativeGestureDistance < -0.5 || (relativeGestureDistance < 0 && vx <= 0.5)) {
+ step = 1;
+ } else if (relativeGestureDistance > 0.5 || (relativeGestureDistance > 0 && vx >= 0.5)) {
+ step = -1;
+ }
+
+ this.props.hasTouch && this.props.hasTouch(false);
+
+ this._movePage(step);
+ }
+
+ this._panResponder = PanResponder.create({
+ // Claim responder if it's a horizontal pan
+ onMoveShouldSetPanResponder: (e, gestureState) => {
+ if (Math.abs(gestureState.dx) > Math.abs(gestureState.dy)) {
+ if (/* (gestureState.moveX <= this.props.edgeHitWidth ||
+ gestureState.moveX >= deviceWidth - this.props.edgeHitWidth) && */
+ this.props.locked !== true && !this.state.fling) {
+ this.props.hasTouch && this.props.hasTouch(true);
+ return true;
+ }
+ }
+ },
+
+ // Touch is released, scroll to the one that you're closest to
+ onPanResponderRelease: release,
+ onPanResponderTerminate: release,
+
+ // Dragging, move the view with the touch
+ onPanResponderMove: (e, gestureState) => {
+ var dx = gestureState.dx;
+ var offsetX = -dx / this.state.viewWidth + this.state.childIndex;
+ this.state.scrollValue.setValue(offsetX);
+ },
+ });
+ },
+
+ goToPage(pageNumber) {
+ console.log('goToPage: ', pageNumber);
+ var pageCount = this.props.dataSource.getPageCount();
+ if (pageNumber < 0 || pageNumber >= pageCount) {
+ console.error('Invalid page number: ', pageNumber);
+ return
+ }
+
+ var step = pageNumber - this.state.currentPage;
+ this._movePage(step);
+ },
+
+ _movePage(step) {
+ var pageCount = this.props.dataSource.getPageCount();
+ var pageNumber = this.state.currentPage + step;
+
+ if (this.props.isLoop) {
+ pageNumber = (pageNumber + pageCount) % pageCount;
+ } else {
+ pageNumber = Math.min(Math.max(0, pageNumber), pageCount - 1);
+ }
+
+ this.props.onChangeTab && this.props.onChangeTab({
+ i: pageNumber, ref: this.props.children[pageNumber]
+ });
+
+ var moved = pageNumber !== this.state.currentPage;
+
+ var scrollStep = (moved ? step : 0) + this.state.childIndex;
+
+ this.state.fling = true;
+ Animated.spring(this.state.scrollValue, {toValue: scrollStep, friction: 10, tension: 50})
+ .start((event) => {
+ if (event.finished) {
+ this.state.fling = false;
+ this.setState({
+ currentPage: pageNumber,
+ });
+ }
+ });
+ },
+
+ renderPageIndicator(props) {
+ if (this.props.renderPageIndicator === false) {
+ return null;
+ } else if (this.props.renderPageIndicator) {
+ return React.cloneElement(this.props.renderPageIndicator(), props);
+ } else {
+ return (
+
+
+
+ );
+ }
+ },
+
+ _getPage(pageIdx: number) {
+ var dataSource = this.props.dataSource;
+ var pageID = dataSource.pageIdentities[pageIdx];
+ return (
+
+ );
+ },
+
+ render() {
+ console.log('render()', this.state.currentPage);
+ var dataSource = this.props.dataSource;
+ var pageIDs = dataSource.pageIdentities;
+
+ var bodyComponents = [];
+
+ var pagesNum = 0;
+ var hasLeft = false;
+ var viewWidth = this.state.viewWidth;
+
+ if(pageIDs.length > 0 && viewWidth > 0) {
+ // left page
+ if (this.state.currentPage > 0) {
+ bodyComponents.push(this._getPage(this.state.currentPage - 1));
+ pagesNum++;
+ hasLeft = true;
+ } else if (this.state.currentPage == 0 && this.props.isLoop) {
+ bodyComponents.push(this._getPage(pageIDs.length - 1));
+ pagesNum++;
+ hasLeft = true;
+ }
+
+ // center page
+ bodyComponents.push(this._getPage(this.state.currentPage));
+ pagesNum++;
+
+ // right page
+ if (this.state.currentPage < pageIDs.length - 1) {
+ bodyComponents.push(this._getPage(this.state.currentPage + 1));
+ pagesNum++;
+ } else if (this.state.currentPage == pageIDs.length - 1 && this.props.isLoop) {
+ bodyComponents.push(this._getPage(0));
+ pagesNum++;
+ }
+ }
+
+ var sceneContainerStyle = {
+ width: this.state.viewWidth * pagesNum,
+ flex: 1,
+ flexDirection: 'row'
+ };
+
+ this.state.childIndex = hasLeft ? 1 : 0;
+ this.state.scrollValue.setValue(this.state.childIndex);
+ var translateX = this.state.scrollValue.interpolate({
+ inputRange: [0, 1], outputRange: [0, -viewWidth]
+ });
+
+ return (
+ {
+ // console.log('ViewPager.onLayout()');
+ var viewWidth = event.nativeEvent.layout.width;
+ if (!viewWidth || this.state.viewWidth === viewWidth) {
+ return;
+ }
+ this.setState({
+ currentPage: this.state.currentPage,
+ viewWidth: viewWidth,
+ });
+ }}
+ >
+
+
+ {bodyComponents}
+
+
+ {this.renderPageIndicator({goToPage: this.goToPage,
+ pageCount: pageIDs.length,
+ activePage: this.state.currentPage,
+ scrollValue: this.state.scrollValue,
+ })}
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ indicators: {
+ flex: 1,
+ alignItems: 'center',
+ position: 'absolute',
+ bottom: 10,
+ left: 0,
+ right: 0,
+ },
+});
+
+module.exports = ViewPager;
diff --git a/ViewPagerDataSource.js b/ViewPagerDataSource.js
new file mode 100644
index 0000000..445bad0
--- /dev/null
+++ b/ViewPagerDataSource.js
@@ -0,0 +1,134 @@
+'use strict';
+
+var invariant = require('invariant');
+var isEmpty = require('isEmpty');
+var warning = require('warning');
+
+function defaultGetPageData(
+ dataBlob: any,
+ pageID: number | string,
+): any {
+ return dataBlob[pageID];
+}
+
+type differType = (data1: any, data2: any) => bool;
+
+type ParamType = {
+ pageHasChanged: differType;
+ getPageData: ?typeof defaultGetPageData;
+}
+
+class ViewPagerDataSource {
+
+ constructor(params: ParamType) {
+ this._getPageData = params.getPageData || defaultGetPageData;
+ this._pageHasChanged = params.pageHasChanged;
+
+ this.pageIdentities = [];
+ }
+
+ cloneWithPages(
+ dataBlob: any,
+ pageIdentities: ?Array,
+ ): ViewPagerDataSource {
+
+ var newSource = new ViewPagerDataSource({
+ getPageData: this._getPageData,
+ pageHasChanged: this._pageHasChanged,
+ });
+
+ newSource._dataBlob = dataBlob;
+
+ if (pageIdentities) {
+ newSource.pageIdentities = pageIdentities;
+ } else {
+ newSource.pageIdentities = Object.keys(dataBlob);
+ }
+
+ newSource._cachedPageCount = newSource.pageIdentities.length;
+ newSource._calculateDirtyPages(
+ this._dataBlob,
+ this.pageIdentities
+ );
+ return newSource;
+ }
+
+ getPageCount(): number {
+ return this._cachedPageCount;
+ }
+
+ /**
+ * Returns if the row is dirtied and needs to be rerendered
+ */
+ pageShouldUpdate(pageIndex: number): bool {
+ var needsUpdate = this._dirtyPages[pageIndex];
+ warning(needsUpdate !== undefined,
+ 'missing dirtyBit for section, page: ' + pageIndex);
+ return needsUpdate;
+ }
+
+ /**
+ * Gets the data required to render the page
+ */
+ getPageData(pageIndex: number): any {
+ if (!this.getPageData) {
+ return null;
+ }
+ var pageID = this.pageIdentities[pageIndex];
+ warning(pageID !== undefined,
+ 'renderPage called on invalid section: ' + pageID);
+ return this._dataBlob[pageID];
+ }
+
+ /**
+ * Private members and methods.
+ */
+
+ _getPageData: typeof defaultGetPageData;
+ _pageHasChanged: differType;
+
+ _dataBlob: any;
+ _dirtyPages: Array;
+ _cachedRowCount: number;
+
+ pageIdentities: Array;
+
+ _calculateDirtyPages(
+ prevDataBlob: any,
+ prevPageIDs: Array,
+ ): void {
+ // construct a hashmap of the existing (old) id arrays
+ var prevPagesHash = keyedDictionaryFromArray(prevPageIDs);
+ this._dirtyPages = [];
+
+ var dirty;
+ for (var sIndex = 0; sIndex < this.pageIdentities.length; sIndex++) {
+ var pageID = this.pageIdentities[sIndex];
+ dirty = !prevPagesHash[pageID];
+ var pageHasChanged = this._pageHasChanged
+ if (!dirty && pageHasChanged) {
+ dirty = pageHasChanged(
+ this._getPageData(prevDataBlob, pageID),
+ this._getPageData(this._dataBlob, pageID)
+ );
+ }
+ this._dirtyPages.push(!!dirty);
+ }
+ }
+
+}
+
+function keyedDictionaryFromArray(arr) {
+ if (isEmpty(arr)) {
+ return {};
+ }
+ var result = {};
+ for (var ii = 0; ii < arr.length; ii++) {
+ var key = arr[ii];
+ warning(!result[key], 'Value appears more than once in array: ' + key);
+ result[key] = true;
+ }
+ return result;
+}
+
+module.exports = ViewPagerDataSource;