实验性项目无法发布到市场,决定整改
RNDouBan
,决定做一个用react-native写的简单豆瓣客户端react-native-swiper
以实现滑动切屏先上效果图(代码地址)
由于react-native-router-flux
封装了NavBav
,但是不太喜欢,主要是给导航栏添加右侧功能健不太亲民,所有自己封装了一个精简版,有很多不如意的地方,水平有限:app/components/navbar.js:
'use strict';
import React, {
Component
} from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity
} from 'react-native';
import {
Actions
} from 'react-native-router-flux';
import Button from 'react-native-button';
import Icon from 'react-native-vector-icons/Ionicons';
import Style from '../common/style';
class NavBar extends Component {
render() {
return (
<View style={styles.navbar}>
<View style={styles.backBtn}>
{this._renderBack()}
</View>
<View style={styles.navbarTitle}>
<Text style={styles.title}>{this.props.title}</Text>
</View>
<View style={[Style.valignCenter, styles.rightBtn]}>
{this._renderRight()}
</View>
</View>
);
}
_renderBack() {
switch (this.props.back) {
case 'back':
return (
<TouchableOpacity onPress={() => Actions.pop()}>
<Icon name='md-arrow-back' size={20} color='#cdcdcd'></Icon>
</TouchableOpacity>
)
case 'cancel':
return (
<TouchableOpacity onPress={() => Actions.pop()}>
<Text style={Style.blueText}>取消</Text>
</TouchableOpacity>
)
default:
return null;
}
}
_renderRight() {
if (typeof this.props.renderRight === 'function') {
return this.props.renderRight();
}
switch (this.props.renderRight) {
case 'share':
return (
<TouchableOpacity onPress={()=>alert('分享')} style={[styles.buttonStyle]}>
<Icon name='ios-redo-outline' size={14} color='#cdcdcd'></Icon>
</TouchableOpacity>
)
case 'button':
return (
<Button onPress={this.props.onRight.bind(this)} containerStyle={[Style.redBtn, styles.buttonStyle]}>
<Icon name={this.props.rightIcon} size={14} color='#fff'> {this.props.rightTitle}</Icon>
</Button>
)
case 'text':
return (
<TouchableOpacity onPress={this.props.onRight.bind(this)}>
<Text style={Style.redText}> {this.props.rightTitle}</Text>
</TouchableOpacity>
)
default:
return null;
}
}
}
const styles = StyleSheet.create({
navbar: {
// flex: 3,
height: 44,
backgroundColor: '#fff',
borderBottomWidth: 0,
borderBottomColor: '#fff',
flexDirection: 'row',
justifyContent: 'center',
},
navbarTitle: {
flex: 2,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 16,
color: 'black',
},
backBtn: {
flex: 1,
alignItems: 'flex-start',
justifyContent: 'center',
marginLeft: 16,
},
rightBtn: {
flex: 1,
alignItems: 'flex-end',
justifyContent: 'center',
marginRight: 16,
},
buttonStyle: {
padding: 6,
}
});
export default NavBar;
缺点:不能根据导航栈是否为空自动渲染返回按钮
action
如前文所示,豆瓣北京周末活动API地址:http://api.douban.com/v2/event/list?loc=108288&day_type=weekend&type=party
react-native-swiper
地址不是特别的好用但是目前我没发现更加实用的,这个组件本来是用来做轮播图的,看了一下源码,应用的是ViewPagerAndroid
和ScrollView
,所以觉得可以一用,有其他需求的童鞋可以看看源码适当修改,类似的组件大体如此,源码也简单,不到600行,核心:renderScrollView(pages) {
if (Platform.OS === 'ios')
return (
<ScrollView ref="scrollView"
{...this.props}
{...this.scrollViewPropOverrides()}
contentContainerStyle={[styles.wrapper, this.props.style]}
contentOffset={this.state.offset}
onScrollBeginDrag={this.onScrollBegin}
onMomentumScrollEnd={this.onScrollEnd}
onScrollEndDrag={this.onScrollEndDrag}>
{pages}
</ScrollView>
);
return (
<ViewPagerAndroid ref="scrollView"
{...this.props}
initialPage={this.props.loop ? this.state.index + 1 : this.state.index}
onPageSelected={this.onScrollEnd}
style={{flex: 1}}>
{pages}
</ViewPagerAndroid>
);
},
看这段代码应该就很清楚了,如果是android系统就渲染Pager
如果是ios就使用横向的ScrollView
,修改后的app首页如下:
import React, {
PropTypes,
} from 'react';
import {
View,
ScrollView,
Text,
Image,
ListView,
StyleSheet,
TouchableOpacity,
RefreshControl,
Platform,
} from 'react-native';
import {
connect
} from 'react-redux';
import {
Actions
} from 'react-native-router-flux';
import Swiper from 'react-native-swiper';
import {
fetchMovies,
fetchEvents
} from './action';
import Loading from '../components/loading';
import LoadMore from '../components/loadMore';
import Util from '../common/util';
import MovieCard from '../components/movieCard';
import EventCard from '../components/eventCard';
import NavBar from '../components/navbar';
import Style from '../common/style';
class Home extends React.Component {
constructor(props) {
super(props);
let moviesDS = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
let eventsDS = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
this.state = {
moviesDS: moviesDS,
eventsDS: eventsDS,
// data: [],
refreshing: false,
};
}
componentDidMount() {
const {
dispatch,
movies,
} = this.props;
if (movies.subjects.length === 0) {
dispatch(fetchMovies());
dispatch(fetchEvents());
}
}
render() {
const {
isFetching,
movies,
events,
} = this.props;
// if (!isFetching && movies.subjects.length > 0) {
// this.state.data = this.state.data.concat(movies.subjects);
// }
return (
<View style={styles.wrapper}>
<NavBar title='书影音&活动'></NavBar>
<Swiper loop={false} renderPagination={this._renderPagination.bind(this)} style={Style.mt1}
>
<View style={styles.children} dotTitle='电影'>
{isFetching && movies.subjects.length === 0 ? <Loading/ > : (
<ListView dataSource={this.state.moviesDS.cloneWithRows(movies.subjects)} renderRow={this._renderRow.bind(this)}
enableEmptySections={true}
onEndReached={this._handleLoadMore.bind(this)}
onEndReachedThreshold={10}
initialListSize={3}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh.bind(this)}
colors={['#00B51D']}
titleColor='#00B51D'
/>
}
renderFooter={() => this.props.hasMore ? <LoadMore active={isFetching}/> : null }
/>
)}
</View>
<View style={styles.children} dotTitle='活动'>
{isFetching && movies.subjects.length === 0 ? <Loading/ > : (
<ListView dataSource={this.state.eventsDS.cloneWithRows(events.events)} renderRow={this._renderEvent.bind(this)}
enableEmptySections={true}
onEndReached={this._eventLoadMore.bind(this)}
onEndReachedThreshold={10}
initialListSize={3}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefreshEvent.bind(this)}
colors={['#00B51D']}
titleColor='#00B51D'
/>
}
renderFooter={() => this.props.hasMore ? <LoadMore active={isFetching}/> : null }
/>
)}
</View>
</Swiper>
</View>
);
}
_renderPagination(index, total, swiper) {
// By default, dots only show when `total` > 2
if (total <= 1) return null;
let dotStyle = {
width: Util.window.width / total,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 11,
paddingBottom: 11,
}
let children = swiper.props.children;
let dots = children.map((v, k) => {
return (
<View style={[dotStyle, index === k ? styles.activeDot : styles.dot]} key={k}>
<Text style={index === k ? styles.activeDotTitle : styles.dotTitle}>{v.props.dotTitle}</Text>
</View>
)
})
return (
<View pointerEvents='none' style={[styles.paginationStyle]}>
{dots}
</View>
)
}
_onRefresh() {
// 刷新
const {
dispatch
} = this.props;
dispatch(fetchMovies())
}
_handleLoadMore() {
// 加载更多
if (this.props.hasMore) {
const {
dispatch,
movies
} = this.props;
let start = movies.start + movies.count;
dispatch(fetchMovies(start))
}
}
_onRefreshEvent() {
// 刷新
const {
dispatch
} = this.props;
dispatch(fetchEvents())
}
_eventLoadMore() {
// 加载更多
if (this.props.hasMore) {
const {
dispatch,
events
} = this.props;
let start = events.start + events.count;
dispatch(fetchEvents(start))
}
}
_renderRow(row) {
return (
<TouchableOpacity style={styles.movieItem} onPress={() => Actions.detail({url: row.alt})}>
<MovieCard movie={row}></MovieCard>
</TouchableOpacity>
)
}
_renderEvent(row) {
return (
<TouchableOpacity style={styles.movieItem} onPress={() => Actions.detail({url: row.alt})}>
<EventCard event={row}></EventCard>
</TouchableOpacity>
)
}
}
Home.propTypes = {};
Home.defaultProps = {};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
},
movieItem: {
marginTop: 10,
},
paginationStyle: {
position: 'absolute',
top: 1,
height: 44,
width: Util.window.width,
flexDirection: 'row',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
borderBottomWidth: 0.5,
borderBottomColor: '#cdcdcd',
},
activeDot: {
borderBottomWidth: 2,
borderBottomColor: '#00B51D',
},
dot: {
borderBottomWidth: 2,
borderBottomColor: '#fff',
},
dotTitle: {
},
activeDotTitle: {
color: '#00B51D',
},
children: {
flex: 1,
paddingTop: 44,
},
cellSeparator: {
height: 0.5,
backgroundColor: "#DDD"
},
});
function mapStateToProps(state) {
return {
...state.moviesReducer,
...state.eventsReducer,
}
}
export default connect(mapStateToProps)(Home)
主要添加_renderPagination
覆盖原始的pagination
,通过调正style
将滑动条置顶,在每一页View加上dotTitle
,效果图如上,一些style可能不适用或有冲突,检查源码以及高度做调整即可。
前文的
reducer
书写不太对,修改如下:
case RECEIVE_MOVIES:
const movies = {...state.movies};
return {
...state,
isFetching: false,
hasMore: (action.movies.start + action.movies.count) < action.movies.total,
movies: {
start: action.movies.start,
count: action.movies.count,
subjects: action.movies.start === 0 ? action.movies.subjects : [].concat(movies.subjects, action.movies.subjects)
}
}
主要修改分页逻辑,起因是因为在ListView
里面会有三个数据加载
[]
显示页面加载条dataSource
的修改,不然会有很多不明bug,我这里只是简单处理,具体可以依照自己的实际数据结构以及state
的设计。最后附上如何android
build签名的apk:
keytool -genkey -v -keystore release-key.keystore -alias key-alias -keyalg RSA -keysize 2048 -validity 10000
,keytool
命令就不做详细介绍了,如果windows系统找不到,可以使用git的bashandroid/app
目录下,记得修改.gitignore
,不要把证书也提交了android/gradle.properties
加下:RELEASE_STORE_FILE=release-key.keystore
RELEASE_KEY_ALIAS=key-alias
RELEASE_STORE_PASSWORD=*****// 生成证书的密码
RELEASE_KEY_PASSWORD=***** // 另一个密码
android/app/build.gradle
: def enableProguardInReleaseBuilds = true // true可以一定程度减少apk体积
android {
...
defaultConfig {
...
}
signingConfigs {
release {
// 上文配置
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
...
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
...
}
最后 在android
目录下运行./gradlew assembleRelease
成功即可在android/app/build/outputs/apk/app-release.apk
找到apk,运行./gradlew installRelease
可以在设备上测试安装,注意如果是调试机请先卸载debug的apk不然会安装失败。
2024 - 快车库 - 我的知识库 重庆启连科技有限公司 渝ICP备16002641号-10
企客连连 表单助手 企服开发 榜单123