HelloRN代码分析笔记
HelloRN是一个react-native入门demo,适配android ios,主要展示网页、图片、音乐、地图,项目使用版本"react": “~15.4.0-rc.4”, “react-native”: “0.40.0”。
github地址
网页展示
在文章内容页面,作者使用WebView控件展示网页内容,灵活运用fetch分别获取网页内容及css文件,将其合并后赋值给WebView的source属性。
export default class Image extends Component {
constructor() {
super()
this.state = {
html: ''
}
}
componentDidMount() {
this.timer = setTimeout(() => this._fetch(),400)
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer)
}
_fetch() {
fetch('http://news-at.zhihu.com/api/4/news/' + this.props.articleID)
.then((response) => response.json())
.then((responseJson) => {
fetch('http://daily.zhihu.com/css/share.css?v=5956a')
.then((responseCSS) => responseCSS.text())
.then((css) => {
let cssLink = '<style>'+css+'</style>',
imgLink = '<div class="img-wrap"><h1 class="headline-title">'+responseJson.title+'</h1><span class="img-source"></span><img src="'+responseJson.image+'" alt=""><div class="img-mask"></div></div>'
this.setState({
html: cssLink + responseJson.body.replace(/<div class=\"img-place-holder\"><\/div>/, imgLink),
})
})
})
}
render() {
return (
<View style={styles.container}>
<WebView
style={{flex:1}}
source={{html: this.state.html}}
javaScriptEnabled={false}
/>
</View>
)
}
}
图片展示
在页面加载时,单独编写控件LoadingSpinner增加用户体验。
export default class LoadingSpinner extends Component {
render() {
return (
<View style={styles.container}>
<ActivityIndicator
animating={this.props.animating}
color='#FFDB42'
size='large'
/>
</View>
)
}
}
作者编写方法_fetchImages(page = 1, callback)用于分页加载数据,编写控件ImageModal用于用户点击列表后展示图片。通常列表页的state包含“是否正在加载”、“数据源”、“数据分页”等状态。
let deviceWidth = Dimensions.get('window').width
const ImageItem = ({ url, images, rowID, t }) => {
let gif = url.endsWith('.gif'), newUrl = url
if (gif) newUrl = url.replace('mw690','small')
.replace('mw1024','small')
.replace('mw1200','small')
return (
<TouchableWithoutFeedback onPress={() => {t.setState({modalUri: url, modalHide: false})}}>
<View style={{padding: 10, margin: 10, borderRadius: 5, backgroundColor: '#FFF'}}>
<Image
style={{
flex: 1,
height: 200,
justifyContent: 'center',
alignItems: 'center'
}}
resizeMethod='scale'
source={{uri: newUrl}}
>
<Text
style={{
backgroundColor: 'transparent',
color: '#FFF',
fontSize: 18
}}
>
{gif && 'PLAY'}
</Text>
</Image>
</View>
</TouchableWithoutFeedback>
)
}
export default class Images extends Component {
constructor() {
super()
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
this.state = {
imageDS: ds,
images: [],
isRefreshing: false,
currentPage: 1,
modalUri: '',
modalHide: true
}
}
componentDidMount() {
this._fetchImages()
}
_fetchImages(page = 1, callback) {
let url = 'http://i.jandan.net/?oxwlxojflwblxbsapi=jandan.get_ooxx_comments&page=' + page
fetch(url)
.then((res) => res.json())
.then((res) => {
let tmp = page == 1 ? [] : this.state.images
res.comments.forEach((ele, index, arr) => {
tmp = tmp.concat(ele.pics)
})
this.setState({
imageDS: this.state.imageDS.cloneWithRows(tmp),
images: tmp,
currentPage: page
}, callback && callback())
})
}
_onRefresh() {
this.setState({isRefreshing: true})
this._fetchImages(1,this.setState({isRefreshing: false}))
}
_onLoadMore() {
let page = ++this.state.currentPage
this._fetchImages(page)
}
render() {
if (this.state.images.length == 0) return <LoadingSpinner animating={true} />
return (
<View style={styles.container}>
<ListView
dataSource={this.state.imageDS}
renderRow={(rowData, sectionID, rowID) =>
<ImageItem url={rowData} images={this.state.images} key={rowID} rowID={rowID} t={this} />
}
refreshControl={
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this._onRefresh.bind(this)}
tintColor='#FFDB42'
title='拼命加载中'
titleColor="black"
colors={['black']}
progressBackgroundColor="#FFDB42"
/>
}
onEndReachedThreshold={250}
onEndReached={this._onLoadMore.bind(this)}
/>
<ImageModal
uri={this.state.modalUri}
hide={this.state.modalHide}
onPress={()=>this.setState({modalHide: true})}
/>
</View>
)
}
}
音乐展示
作者使用react-native-login插件播放音乐,使用react-natve-modalbox插件作为界面弹出层,使用react-native-root-toast作为信息提示。
let deviceWidth = Dimensions.get('window').width
const SongItem = ({ data, rowID, t }) => {
let current = t.state.currentSong == data
return (
<TouchableOpacity
onPress={() => {
t.setState({
sliderValue: 0,
current: '00:00',
videoPause: false,
playButton: 'pause-circle',
currentSong: t.state.songs[rowID]
},t.refs.modal.close())
}
}
>
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#fff',
alignItems: 'center',
padding: 15
}}
>
<View style={{flexDirection: 'row', alignItems: 'flex-end',}}>
<Text style={{color: current ? 'red' : 'black'}}>{data.name}</Text>
<Text style={{fontSize: 11, color: current ? 'red' : '#AAA'}}> - {data.artists[0].name}</Text>
</View>
{ current && <Icon name='play' size={12} color='red' /> }
</View>
</TouchableOpacity>
)
}
export default class Music extends Component {
constructor() {
super()
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
this.state = {
songDS: ds,
songs: [],
currentSong: {},
sliderValue: 0,
videoPause: false,
playButton: 'pause-circle',
current: '00:00',
}
}
componentDidMount() {
// 网易云音乐 云音乐热歌榜 api
let url = 'http://music.163.com/api/playlist/detail?id=3778678&updateTime=-1'
fetch(url)
.then((data) => {
return data.json()
})
.then((res) => {
let songs = res.result.tracks
// 取前20首
songs.length = 20
this.setState({
songDS: this.state.songDS.cloneWithRows(songs),
songs: songs,
currentSong: songs[0],
})
})
}
_playButton() {
this.setState({
playButton: this.state.videoPause ? 'pause-circle' : 'play-circle',
videoPause: !this.state.videoPause
})
}
_onProgress(data) {
// currentTime 23.313s
let val = parseInt(data.currentTime * 1000)
this.setState({
sliderValue: val,
current: this._formatTime(Math.floor(data.currentTime))
})
}
_formatTime(time) {
// 71s -> 01:11
let min = Math.floor(time / 60)
let second = time - min * 60
min = min >= 10 ? min : '0' + min
second = second >= 10 ? second : '0' + second
return min + ':' + second
}
render() {
if (!this.state.currentSong.name) return <LoadingSpinner animating={true} />
return (
<View style={styles.container}>
{ this.state.songs.length != 0 ?
<Video
source={{uri: this.state.currentSong.mp3Url}} // Can be a URL or a local file.
ref='video' // Store reference
rate={1.0} // 0 is paused, 1 is normal.
volume={1.0} // 0 is muted, 1 is normal.
muted={false} // Mutes the audio entirely.
paused={this.state.videoPause} // Pauses playback entirely.
repeat={false} // Repeat forever.
playInBackground={false} // Audio continues to play when app entering background.
playWhenInactive={false} // [iOS] Video continues to play when control or notification center are shown.
progressUpdateInterval={250.0} // [iOS] Interval to fire onProgress (default to ~250ms)
onProgress={this._onProgress.bind(this)}
onEnd={() => {
let index = this.state.songs.indexOf(this.state.currentSong)
index = index == this.state.songs.length-1 ? 0 : index+1
this.setState({
currentSong: this.state.songs[index],
sliderValue: 0,
current: '00:00',
})
}}
onError={(e) => {
console.log(e)
Toast.show('mp3资源出错',{
position: Toast.positions.CENTER,
onHidden: () => {
let index = this.state.songs.indexOf(this.state.currentSong)
index = index == this.state.songs.length-1 ? 0 : index+1
this.setState({
currentSong: this.state.songs[index],
sliderValue: 0,
current: '00:00',
})
}
})
}}
/>
:
null
}
<Image
style={styles.image}
source={{uri: this.state.currentSong.album.picUrl}}
resizeMode='cover'
/>
<View style={styles.playingInfo}>
<Text>{this.state.currentSong.name} - {this.state.currentSong.artists[0].name}</Text>
<Text>{this.state.current} - {this._formatTime(Math.floor(this.state.currentSong.duration / 1000))}</Text>
</View>
<View style={styles.playingControl}>
<TouchableOpacity onPress={this._playButton.bind(this)}>
<Icon name={this.state.playButton} size={40} color='#FFDB42' />
</TouchableOpacity>
<Slider
ref='slider'
style={{flex: 1, marginLeft: 10, marginRight: 10}}
value={this.state.sliderValue}
onValueChange={(value) => {
this.setState({
videoPause: true,
current: this._formatTime(Math.floor(value / 1000))
})
}
}
onSlidingComplete={(value) => {
this.refs.video.seek(value / 1000)
// 判断是否处于播放状态
if (this.state.playButton === 'pause-circle') this.setState({videoPause: false})
}
}
maximumValue={this.state.currentSong.duration}
step={1}
minimumTrackTintColor='#FFDB42'
/>
<TouchableOpacity onPress={() => this.refs.modal.open()}>
<Icon name='list-ul' size={30} color='#FFDB42' />
</TouchableOpacity>
</View>
<Modal style={styles.modal} position={"bottom"} ref='modal'>
<ListView
initialListSize={20}
dataSource={this.state.songDS}
renderRow={(rowData, sectionID, rowID) => <SongItem data={rowData} key={rowID} rowID={rowID} t={this} />}
renderSeparator={(sectionID, rowID, adjacentRowHighlighted) => {
return <View style={{borderWidth: .3, borderColor: '#CCC'}} key={rowID}></View>
}}
/>
</Modal>
</View>
)
}
}