LoginSignup
4
1

More than 5 years have passed since last update.

ReactNative FlatListのフォーカス残すのと、ボタンのインタラクションの実装

Posted at

初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に書き出す。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1