初ReactNative。
「ReactNativeが使いやすい/悪いより、AndroidのフレームワークでAndroidアプリ作りたくないじゃん」
って話聞いて、それな感あったので手をつけてみる。
ググってもあまりヒットしなかったFlatList(UITableView?)のフォーカスを残すやつと、
よくやるボタンのインタラクションの実装サンプル。
参考
ReactNativeでサクッとReactjs記事リーダーを作ってみる
成果物
FlatListのフォーカス残し
iOSのViewWillApearとかで、tableView.deselectRows やるやつ。
Cell
フォーカスのスタイルを作って適用
ArticleCell.js
const styles = StyleSheet.create({
bg:{
position:'absolute',
left:0,right:0,top:0,bottom:0,
backgroundColor: '#cccccc'
},
})
constructor(){
super();
this.selected = false
this.focusValue = new Animated.Value(0.0)
this.cellBgStyle = [styles.bg, {opacity:this.focusValue}]
}
render() {
this._focusAnimate()
return (
<TouchableWithoutFeedback onPress={this._onPress}>
<View style={[styles.cell]}>
<Animated.View style={this.cellBgStyle}></Animated.View>
<Text style={styles.title}>{article.title}</Text>
<Text style={styles.pubDate}>{article.dateString}</Text>
</View>
</TouchableWithoutFeedback>
)
}
フォーカス/ブラーの切り替え
ArticleCell.js
_focusAnimate() {
let itemSelected = this.props.data.item.selected
// 状態が違かったらアニメーション
if(itemSelected && !this.selected){
Animated.timing(this.focusValue, {toValue: 0.5, duration: 1},).start();
}else if(!itemSelected && this.selected){
Animated.timing(this.focusValue, {toValue: 0.0, duration: 200},).start();
}
this.selected = itemSelected
}
FlatList
表示
data が datasource的な
extraData が reloadData的な
ArticleListViewController.js
render() {
return (
<FlatList
data={this.state.articleList}
extraData={this.state.reloadCount}
keyExtractor={item => item.link}
renderItem={(data) => {
return <ArticleCell
data={data}
onSelectData={this._onSelectData}
></ArticleCell>
}}>
</FlatList>
)
}
行選択時 selectRow
article.selected = true
にして
reloadData
ArticleListViewController.js
// 記事選択しましたー
_onSelectData = (data) => {
let article = data.item
article.selected = true
this._reloadListView()
this.props.navigator.push({
title: article.title,
component: ArticleDetailViewController,
// viewWillAppear的な。画面戻って来ました。
passProps: { article: article, onBack:()=>{
this._deselectView()
} }
})
}
戻って来たら deselectRow
articleList.selected = false
にして
reloadData
ArticleListViewController.js
// 選択状態解除
_deselectView(){
let articleList = this.state.articleList
for(let i = 0, to = articleList.length; i<to; i++){
let article = articleList[i]
article.selected = false
}
this._reloadListView()
}
ボタンのインタラクション
スタイルを適用
ArticleDetailViewController.js
// フォーカスViewの大きさの計算
const windowWidth = Dimensions.get('window').width
const overlayWidth = windowWidth * 1.1 /* ← てきとうにおおきめ */
const buttonHeight = 54
const styles = StyleSheet.create({
shareText:{
// 略
},
// めんどい。。
shareOverlay:{
position:'absolute',
width:overlayWidth, height:overlayWidth,
left:-(overlayWidth - windowWidth) / 2,
top:-((overlayWidth - buttonHeight) / 2),
backgroundColor:'#6699ff',
borderRadius: overlayWidth / 2
}
})
constructor(){
// focus/blur用のアニメーション値
this.focusValue = new Animated.Value(0.0)
this.blurValue = new Animated.Value(1.0)
// ↑を使ったstyle
this.shareStyle = {
text: [styles.shareText, {opacity:this.blurValue}]
, focusBg: [styles.shareOverlay, {transform:[{scale:this.focusValue}]}]
, focusText: [styles.shareText, {color:'#ffffff', opacity:this.focusValue}]
}
}
render() {
this._focusAnimate()
return (
<TouchableWithoutFeedback onPress={() => this._tryShare()}>
<View style={styles.shareButton}>
<Animated.View style={this.shareStyle.focusBg}></Animated.View>
<Animated.Text style={this.shareStyle.text}>シェアする</Animated.Text>
<Animated.Text style={this.shareStyle.focusText}>シェアする</Animated.Text>
</View>
</TouchableWithoutFeedback>
)
}
focus / blur
ボタンタップでfocusしてアラート表示。
アラート閉じる時にblur
ArticleDetailViewController.js
_tryShare(){
this.setState({focusShare:true})
Alert.alert(
'そのうちシェアする',
null,
[
{text: 'OK', onPress: () => {
this.setState({focusShare:false})
}}
],
{ cancelable: false }
)
}
_focusAnimate(){
if(this.state.focusShare && !this.focusedShare){
Animated.parallel([
Animated.timing(this.focusValue, {toValue: 1.0, duration: 300},).start()
, Animated.timing(this.blurValue, {toValue: 0.0, duration: 300},).start()
])
this.focusedShare = true
}else if(!this.state.focusShare && this.focusedShare){
Animated.parallel([
Animated.timing(this.focusValue, {toValue: 0.0, duration: 250},).start()
, Animated.timing(this.blurValue, {toValue: 1.0, duration: 250},).start()
])
this.focusedShare = false
}
}
コード全体
App.js
App.js
import React, { Component } from 'react';
import ArticleList from './ArticleListViewController';
import { NavigatorIOS} from 'react-native';
export default class App extends Component {
render() {
return (
<NavigatorIOS
style={{flex: 1}}
initialRoute={{
component: ArticleList,
title: 'NakadoriBooks'
}}/>
);
}
}
Article.js
Article.js
import XmlDom from 'xmldom';
export default class Article{
constructor(xml){
this.xml = xml
this.selected = false
}
static createFromRssString(rssString){
var parser = new XmlDom.DOMParser()
var xml = parser.parseFromString(rssString)
var itemList = xml.getElementsByTagName("item")
var articleList = []
for(var i=0,max=itemList.length;i<max;i++){
var itemData = itemList[i]
var article = new Article(itemData)
articleList.push(article)
}
return articleList
}
get title () {
return this.xml.getElementsByTagName("title").item(0).textContent
}
get link () {
return this.xml.getElementsByTagName("guid").item(0).textContent
}
get content () {
return this.xml.getElementsByTagName("content:encoded").item(0).textContent
}
get pubDate(){
return new Date(this.xml.getElementsByTagName("pubDate").item(0).textContent)
}
get dateString(){
var date = this.pubDate
var y = date.getFullYear();
var m = date.getMonth() + 1;
var d = date.getDate();
return y + "/" + m + "/" + d;
}
}
ArticleCell.js
.js
import React from 'react';
import {StyleSheet, Text,View,Animated,TouchableWithoutFeedback} from 'react-native';
export default class ArticleCell extends React.PureComponent{
constructor(){
super();
this.selected = false
this.focusValue = new Animated.Value(0.0)
this.cellBgStyle = [styles.bg, {opacity:this.focusValue}]
}
_onPress = () => {
this.props.onSelectData(this.props.data);
}
_focusAnimate() {
let itemSelected = this.props.data.item.selected
// 状態が違かったらアニメーション
if(itemSelected && !this.selected){
Animated.timing(this.focusValue, {toValue: 0.5, duration: 1},).start();
}else if(!itemSelected && this.selected){
Animated.timing(this.focusValue, {toValue: 0.0, duration: 200},).start();
}
this.selected = itemSelected
}
render() {
let data = this.props.data
let article = data.item
let index = data.index
this._focusAnimate()
return (
<TouchableWithoutFeedback onPress={this._onPress}>
<View style={[styles.cell]}>
<Animated.View style={this.cellBgStyle}></Animated.View>
<Text style={styles.title}>{article.title}</Text>
<Text style={styles.pubDate}>{article.dateString}</Text>
</View>
</TouchableWithoutFeedback>
)
}
}
const styles = StyleSheet.create({
cell:{
padding:10,
borderBottomWidth: 1,
borderBottomColor: '#cccccc'
},
bg:{
position:'absolute',
left:0,right:0,top:0,bottom:0,
backgroundColor: '#cccccc'
},
title:{
fontSize:18,
fontWeight:"bold",
lineHeight: 22,
backgroundColor:'transparent'
},
pubDate:{
marginTop:15,
textAlign:"right",
backgroundColor: 'transparent'
}
})
ArticleListViewController.js
.js
import React from 'react';
import { StyleSheet, FlatList, View, ActivityIndicator, NavigatorIOS} from 'react-native';
import Article from './Article';
import ArticleCell from './ArticleCell';
import ArticleDetailViewController from './ArticleDetailViewController';
const RssUrl = "https://nakadoribooks.com/feed/"
export default class ArticleListViewController extends React.Component {
constructor(){
super();
this.state = {
articleList: []
, reloadCount: 0
, loading: false
}
}
componentDidMount(){
this._fetchData()
}
render() {
return (
<View style={styles.listView}>
<FlatList
data={this.state.articleList}
extraData={this.state.reloadCount}
keyExtractor={item => item.link}
renderItem={(data) => {
return <ArticleCell
data={data}
onSelectData={this._onSelectData}
></ArticleCell>
}}>
</FlatList>
{this.state.loading &&
<View style={styles.loading}>
<ActivityIndicator size='large' />
</View>
}
</View>
)
}
// 選択状態解除
_deselectView(){
let articleList = this.state.articleList
for(let i = 0, to = articleList.length; i<to; i++){
let article = articleList[i]
article.selected = false
}
this._reloadListView()
}
// reloadData
_reloadListView(){
this.setState({
reloadCount: this.state.reloadCount + 1
})
}
// 記事選択しましたー
_onSelectData = (data) => {
let article = data.item
article.selected = true
this._reloadListView()
this.props.navigator.push({
title: article.title,
component: ArticleDetailViewController,
// viewWillAppear的な。画面戻って来ました。
passProps: { article: article, onBack:()=>{
this._deselectView()
} }
})
}
// RSS 取ってくる
_fetchData(){
this.setState({
loading: true
})
fetch(RssUrl)
.then((response) => response.text())
.then((responseData) => {
var articleList = Article.createFromRssString(responseData)
this.setState({
articleList: articleList
, loading: false
})
})
.done();
}
}
const styles = StyleSheet.create({
listView:{
flex:1
},
loading: {
backgroundColor:'rgba(0,0,0,0.3)',
position: 'absolute',
left: 0, right: 0, top: 0, bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
})
ArticleDetailViewController.js
.js
import React from 'react';
import { StyleSheet,Alert,View,Text,WebView,Animated,Dimensions,TouchableWithoutFeedback} from 'react-native';
export default class ArticleDetailViewController extends React.Component {
constructor(){
super();
this.state = {
focusShare: false
}
this.focusedShare = false
// focus/blur用のアニメーション値
this.focusValue = new Animated.Value(0.0)
this.blurValue = new Animated.Value(1.0)
// ↑を使ったstyle
this.shareStyle = {
text: [styles.shareText, {opacity:this.blurValue}]
, focusBg: [styles.shareOverlay, {transform:[{scale:this.focusValue}]}]
, focusText: [styles.shareText, {color:'#ffffff', opacity:this.focusValue}]
}
}
componentWillUnmount(){
// push元に伝える
this.props.onBack()
}
render() {
this._focusAnimate()
return (
<View style={styles.detailView}>
<WebView
style={styles.webView}
source={{html: this.props.article.content}}
automaticallyAdjustContentInsets={false}
contentInset={{bottom:54}}
/>
<View style={styles.footerView}>
<TouchableWithoutFeedback onPress={() => this._tryShare()}>
<View style={styles.shareButton}>
<Animated.View style={this.shareStyle.focusBg}></Animated.View>
<Animated.Text style={this.shareStyle.text}>シェアする</Animated.Text>
<Animated.Text style={this.shareStyle.focusText}>シェアする</Animated.Text>
</View>
</TouchableWithoutFeedback>
</View>
</View>
)
}
_focusAnimate(){
if(this.state.focusShare && !this.focusedShare){
Animated.parallel([
Animated.timing(this.focusValue, {toValue: 1.0, duration: 300},).start()
, Animated.timing(this.blurValue, {toValue: 0.0, duration: 300},).start()
])
this.focusedShare = true
}else if(!this.state.focusShare && this.focusedShare){
Animated.parallel([
Animated.timing(this.focusValue, {toValue: 0.0, duration: 250},).start()
, Animated.timing(this.blurValue, {toValue: 1.0, duration: 250},).start()
])
this.focusedShare = false
}
}
_tryShare(){
this.setState({focusShare:true})
Alert.alert(
'そのうちシェアする',
null,
[
{text: 'OK', onPress: () => {
this.setState({focusShare:false})
}}
],
{ cancelable: false }
)
}
}
// 大きさの計算
const windowWidth = Dimensions.get('window').width
const overlayWidth = windowWidth * 1.1
const buttonHeight = 54
const styles = StyleSheet.create({
detailView:{ flex:1 },
webView: {
flex:1
},
footerView:{
height:buttonHeight,
position:'absolute',
bottom:0,left:0,right:0,
borderTopWidth:1,
borderTopColor: '#cccccc',
backgroundColor:'rgba(255, 255, 255, 0.9)',
overflow: 'hidden'
},
shareButton:{
flex:1,
justifyContent: 'center',
},
shareText:{
textAlign: 'center',
fontSize:18,
fontWeight:'bold',
position:'absolute',
left:0,right:0
},
// めんどい。。
shareOverlay:{
position:'absolute',
width:overlayWidth, height:overlayWidth,
left:-(overlayWidth - windowWidth) / 2,
top:-((overlayWidth - buttonHeight) / 2),
backgroundColor:'#6699ff',
borderRadius: overlayWidth / 2
}
})
感想
思ってたより全然いい。実機デバッグも楽。
次
React文化を学ぶ。
Androidに書き出す。