React Native 实现瀑布流列表页,分组+组内横向的列表页.....

React Native 实现瀑布流列表页,分组+组内横向的列表页.....

随着React Native的更新,因为其跨平台的优越性,越来越多的公司和项目采用其作为其快速开发和迭代的基础语言.
但是其并不是任何控件其都已经涵盖了,就拿我们常见的列表页来说, 一般通用的纵向或者横向列表我们可以使用RN里面的FlatList,需要分组的时候我们可以使用SectionList,但是当我们又想要分组又想要组内横向排列的时候我们会发现已有的控件就无法满足我们了....
那么是否就是黔驴技穷呢?并不是,万能的开发者不会因为这些小的需求而难住,于是我们可以通过SectionList+FlatList的组合来实现.
于是有了大的列表首先SectionList来进行分组,每组只有一个组内元素,这个组内元素是一个FlatList,然后在想普通的渲染FlatList的方式即可.

import React, { Component } from 'react';
import { 
    Dimensions, 
    SafeAreaView,
    SectionList, 
    FlatList,
    View, 
    Text, 
    TouchableOpacity, 
    StyleSheet,
    Image 
} from 'react-native';
const { width, height } = Dimensions.get('window');

const numColumns = 5;

export default class Me extends Component {
    render() {
        const data = [{
            content: [
                {key: 'mine_icon_hot', title: '排行榜'},
                {key: 'mine_icon_preview', title: '审帖'},
                {key: 'mine_icon_manhua', title: '漫画'},
                {key: 'mine_icon_activity', title: '我的收藏'},
                {key: 'mine_icon_nearby', title: '附近'},
                {key: 'mine_icon_random', title: '随机穿越'},
                {key: 'mine_icon_feedback', title: '意见反馈'},
                {key: 'mine_icon_more', title: '更多'},
            ],
            key: 'content',
        }];
        return (
            <SafeAreaView style={styles.container}>
                <SectionList
                    sections={[{data}]}
                    renderItem={this._renderSectionItem}
                    ListHeaderComponent={this._ListHeaderComponent}
                    ListFooterComponent={this._ListFooterComponent}
                    keyExtractor={this._keyExtractor}
                    />
            </SafeAreaView>
        )
    }

    _keyExtractor = (item, index) => {
        return item.key;
    }

    _ListHeaderComponent = () => {
        return (
            <TouchableOpacity 
                activeOpacity={0.7}
                style={styles.header}
            >
                <View style={styles.headerUser}>
                    <Image 
                        source={{uri: 'default_header'}} 
                        style={{width: 50, height: 50}}
                    />
                    <Text style={{marginHorizontal: 10}}>百思不得姐</Text>
                    <Image 
                        source={{uri: 'profile_level1'}}
                        style={{width: 36, height: 15}}
                    />
                </View>
                <Image 
                    source={{uri: 'arrow_right'}}
                    style={{width: 7, height: 12}}
                />
            </TouchableOpacity> 
        )
    }

    _renderItem = ({item}) => {
        return (
            <TouchableOpacity 
                activeOpacity={0.7}
                style={styles.item}
            >
                <Image 
                    source={{uri: item.key}}  
                    style={styles.itemImage}
                />
                <Text style={styles.itemText}>{item.title}</Text>
            </TouchableOpacity>
        )
    }

    _renderSectionItem = ({section}) => {
        return (
            <FlatList
                data={section.data[0].content}
                numColumns={numColumns}
                renderItem={this._renderItem}
                style={{backgroundColor: '#fff'}}
                scrollEnabled={false}
            />
        )
    }
    
    _ListFooterComponent = () => {
        return (
            <TouchableOpacity 
                activeOpacity={0.7}
                style={styles.footer}
            >
                <Text>好友动态</Text>
                <Image 
                    source={{uri:'arrow_right'}}
                    style={{width: 7, height: 12}}
                />
            </TouchableOpacity>
        )
    }
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    header: {
        height: 100,
        backgroundColor: '#fff',
        marginBottom: 10,
        flexDirection: 'row',
        alignItems: 'center',
        paddingHorizontal: 15,
    },
    headerUser: {
        flex: 1, 
        flexDirection: 'row', 
        alignItems: 'center',
    },
    footer: {
        height: 50,
        backgroundColor: '#fff',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        paddingHorizontal: 15,
        marginTop: 10,
    },
    item: {
        backgroundColor: '#fff',
        width: width/numColumns,
        height: 80,  
        alignItems: 'center',
        justifyContent: 'center',
    },
    itemImage: {
        width: 40,
        height: 40,
        marginBottom: 5,
    },
    itemText: {
        fontSize: 12,
    }

})

  

这里写图片描述

这里我们可以看到中间的一组就是横向排列的,虽然我们不一定要使用这种方式来布局,但是这个最起码是一个解决的方案,这个案例可能不明显,但是如果是有很多分组,组内又全部都是横向排列的时候,这个方案就很容易完成了.

至于瀑布流,虽然一直以来都被人诟病,因为其虽然好看,但是其因为每一个元素的高度都不一样,所以需要计算每一个的高度,从而确定下一个的排列的位置,于是这就会比较耗性能,同时因为其特殊性,导致其开发难度相较一般的列表页会稍微复杂一点,但是其实我们只要掌握其原理我们还是很容易写出来的.

首先我们要选择使用什么控件来写,一般我们会第一时间想到scrollView,这个是可以的,但是因为其相对来说封装的东西不多,且没有自己的复用机制,相对来说我们的开发难度会复杂一些,
其实一直都有一个很好的控件,但是我们一般都会忽略,那就是FlatList和SectionList的底层---VirtualizedList
是的就是这个控件,通过简单的封装就实现了FlatList和SectionList,其有自己的渲染item和复用的机制,其封装了上拉刷新和下拉加载,我们可以使用这个控件来定义任何我们想要的各种变种的列表,比如瀑布流,比如音乐卡片.....

import * as React from 'react';
import {
  VirtualizedList,
  View,
  ScrollView,
  StyleSheet,
  findNodeHandle,
  RefreshControl,
} from 'react-native';

type Column = {
  index: number,
  totalHeight: number,
  data: Array<any>,
  heights: Array<number>,
};

const _stateFromProps = ({ numColumns, data, getHeightForItem }) => {
  const columns: Array<Column> = Array.from({
    length: numColumns,
  }).map((col, i) => ({
    index: i,
    totalHeight: 0,
    data: [],
    heights: [],
  }));

  data.forEach((item, index) => {
    const height = getHeightForItem({ item, index });
    const column = columns.reduce(
      (prev, cur) => (cur.totalHeight < prev.totalHeight ? cur : prev),
      columns[0],
    );
    column.data.push(item);
    column.heights.push(height);
    column.totalHeight += height;
  });

  return { columns };
};

export type Props = {
  data: Array<any>,
  numColumns: number,
  renderItem: ({ item: any, index: number, column: number }) => ?React.Element<
    any,
  >,
  getHeightForItem: ({ item: any, index: number }) => number,
  ListHeaderComponent?: ?React.ComponentType<any>,
  ListEmptyComponent?: ?React.ComponentType<any>,
  /**
   * Used to extract a unique key for a given item at the specified index. Key is used for caching
   * and as the react key to track item re-ordering. The default extractor checks `item.key`, then
   * falls back to using the index, like React does.
   */
  keyExtractor?: (item: any, index: number) => string,
  // onEndReached will get called once per column, not ideal but should not cause
  // issues with isLoading checks.
  onEndReached?: ?(info: { distanceFromEnd: number }) => void,
  contentContainerStyle?: any,
  onScroll?: (event: Object) => void,
  onScrollBeginDrag?: (event: Object) => void,
  onScrollEndDrag?: (event: Object) => void,
  onMomentumScrollEnd?: (event: Object) => void,
  onEndReachedThreshold?: ?number,
  scrollEventThrottle: number,
  renderScrollComponent: (props: Object) => React.Element<any>,
  /**
   * Set this true while waiting for new data from a refresh.
   */
  refreshing?: ?boolean,
  /**
   * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
   * sure to also set the `refreshing` prop correctly.
   */
  onRefresh?: ?Function,
};
type State = {
  columns: Array<Column>,
};

// This will get cloned and added a bunch of props that are supposed to be on
// ScrollView so we wan't to make sure we don't pass them over (especially
// onLayout since it exists on both).
class FakeScrollView extends React.Component<{ style?: any, children?: any }> {
  render() {
    return (
      <View style={this.props.style}>
        {this.props.children}
      </View>
    );
  }
}

export default class MasonryList extends React.Component<Props, State> {
  static defaultProps = {
    scrollEventThrottle: 50,
    numColumns: 1,
    renderScrollComponent: (props: Props) => {
      if (props.onRefresh && props.refreshing != null) {
        return (
          <ScrollView
            {...props}
            refreshControl={
              <RefreshControl
                refreshing={props.refreshing}
                onRefresh={props.onRefresh}
              />
            }
          />
        );
      }
      return <ScrollView {...props} />;
    },
  };

  state = _stateFromProps(this.props);
  _listRefs: Array<?VirtualizedList> = [];
  _scrollRef: ?ScrollView;
  _endsReached = 0;

  componentWillReceiveProps(newProps: Props) {
    this.setState(_stateFromProps(newProps));
  }

  getScrollResponder() {
    if (this._scrollRef && this._scrollRef.getScrollResponder) {
      return this._scrollRef.getScrollResponder();
    }
    return null;
  }

  getScrollableNode() {
    if (this._scrollRef && this._scrollRef.getScrollableNode) {
      return this._scrollRef.getScrollableNode();
    }
    return findNodeHandle(this._scrollRef);
  }

  scrollToOffset({ offset, animated }: any) {
    if (this._scrollRef) {
      this._scrollRef.scrollTo({ y: offset, animated });
    }
  }

  _onLayout = event => {
    this._listRefs.forEach(
      list => list && list._onLayout && list._onLayout(event),
    );
  };

  _onContentSizeChange = (width, height) => {
    this._listRefs.forEach(
      list =>
        list &&
        list._onContentSizeChange &&
        list._onContentSizeChange(width, height),
    );
  };

  _onScroll = event => {
    if (this.props.onScroll) {
      this.props.onScroll(event);
    }
    this._listRefs.forEach(
      list => list && list._onScroll && list._onScroll(event),
    );
  };

  _onScrollBeginDrag = event => {
    if (this.props.onScrollBeginDrag) {
      this.props.onScrollBeginDrag(event);
    }
    this._listRefs.forEach(
      list => list && list._onScrollBeginDrag && list._onScrollBeginDrag(event),
    );
  };

  _onScrollEndDrag = event => {
    if (this.props.onScrollEndDrag) {
      this.props.onScrollEndDrag(event);
    }
    this._listRefs.forEach(
      list => list && list._onScrollEndDrag && list._onScrollEndDrag(event),
    );
  };

  _onMomentumScrollEnd = event => {
    if (this.props.onMomentumScrollEnd) {
      this.props.onMomentumScrollEnd(event);
    }
    this._listRefs.forEach(
      list =>
        list && list._onMomentumScrollEnd && list._onMomentumScrollEnd(event),
    );
  };

  _getItemLayout = (columnIndex, rowIndex) => {
    const column = this.state.columns[columnIndex];
    let offset = 0;
    for (let ii = 0; ii < rowIndex; ii += 1) {
      offset += column.heights[ii];
    }
    return { length: column.heights[rowIndex], offset, index: rowIndex };
  };

  _renderScrollComponent = () => <FakeScrollView style={styles.column} />;

  _getItemCount = data => data.length;

  _getItem = (data, index) => data[index];

  _captureScrollRef = ref => (this._scrollRef = ref);

  render() {
    const {
      renderItem,
      ListHeaderComponent,
      ListEmptyComponent,
      keyExtractor,
      onEndReached,
      ...props
    } = this.props;
    let headerElement;
    if (ListHeaderComponent) {
      headerElement = <ListHeaderComponent />;
    }
    let emptyElement;
    if (ListEmptyComponent) {
      emptyElement = <ListEmptyComponent />;
    }

    const content = (
      <View style={styles.contentContainer}>
        {this.state.columns.map(col =>
          <VirtualizedList
            {...props}
            ref={ref => (this._listRefs[col.index] = ref)}
            key={`$col_${col.index}`}
            data={col.data}
            getItemCount={this._getItemCount}
            getItem={this._getItem}
            getItemLayout={(data, index) =>
              this._getItemLayout(col.index, index)}
            renderItem={({ item, index }) =>
              renderItem({ item, index, column: col.index })}
            renderScrollComponent={this._renderScrollComponent}
            keyExtractor={keyExtractor}
            onEndReached={onEndReached}
            onEndReachedThreshold={this.props.onEndReachedThreshold}
            removeClippedSubviews={false}
          />,
        )}
      </View>
    );

    const scrollComponent = React.cloneElement(
      this.props.renderScrollComponent(this.props),
      {
        ref: this._captureScrollRef,
        removeClippedSubviews: false,
        onContentSizeChange: this._onContentSizeChange,
        onLayout: this._onLayout,
        onScroll: this._onScroll,
        onScrollBeginDrag: this._onScrollBeginDrag,
        onScrollEndDrag: this._onScrollEndDrag,
        onMomentumScrollEnd: this._onMomentumScrollEnd,
      },
      headerElement,
      emptyElement && this.props.data.length === 0 ? emptyElement : content,
    );

    return scrollComponent;
  }
}

const styles = StyleSheet.create({
  contentContainer: {
    flexDirection: 'row',
  },
  column: {
    flex: 1,
  },
});

这里就是用到了VirtualizedList来封装成的瀑布流,我们只要像使用FlatList一样的方式就能使用了,当然我们需要自己添加一个计算每一个item的宽度和高度的方法,
于是使用的方式就成了这样

import React, { Component } from 'react';
import {Dimensions, SafeAreaView, Text, View, StyleSheet, TouchableOpacity } from "react-native";
import MasonryList from '../Base/MasonryList';
import PlacehoderImage from '../Base/PlaceholderImage';
const { width, height } = Dimensions.get('window');

const itemWidth = (width - 16) / 2;

const secToTime = (s) => {
    let h = 0, m = 0;
    if(s > 60){
        m = parseInt(s / 60);
        s = parseInt(s % 60);
        if(m > 60) {
            h = parseInt(i / 60);
            m = parseInt(i % 60);
        }
    }
    // 补零
    const zero = (v) => {
        return (v >> 0) < 10 ? ("0" + v) : v;
    };
    return (h == 0 ? [zero(m), zero(s)].join(":") : [zero(h), zero(m), zero(s)].join(":"));
}

export default class ContentWaterfall extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            refreshing: false,
            data: [],
            np: 0,
        }
    }

    componentDidMount = () => {
        this.onRefreshing();
    }

    render() {
        return (
            <SafeAreaView style={styles.container}>
                <MasonryList
                    data={this.state.data}
                    numColumns={2}
                    renderItem={this._renderItem}
                    getHeightForItem={this._getHeightForItem}
                    refreshing = {this.state.refreshing}
                    onRefresh = {this.onRefreshing}
                    onEndReachedThreshold={0.5}
                    onEndReached={this._onEndReached}
                    keyExtractor={this._keyExtractor}
                    />
            </SafeAreaView>
        )
    }

    onRefreshing = () => {
        this.setState({
            refreshing: true,
        })
        const { api } = this.props;
        fetch(api(this.state.np))
        .then((response) => response.json())
        .then((jsonData) => {
            this.setState({
                refreshing: false,
                data: jsonData.list,
                np: jsonData.info.np || 0,
    
            })
        });
    }

    _onEndReached = () => {
        const { api } = this.props;
        fetch(api(this.state.np))
        .then((response) => response.json())
        .then((jsonData) => {
            this.setState({
                data: [...this.state.data, ...jsonData.list],
                np: jsonData.info.np,
            })
        });
    }

    _keyExtractor = (item, index) => {
        return item.text + index;
    }

    _getHeightForItem = ({item}) => {
        return Math.max(itemWidth, itemWidth / item.video.width * item.video.height);
    }

    _renderItem = ({item}) => {
        const itemHeight = this._getHeightForItem({item});
        return (
            <TouchableOpacity 
                activeOpacity={0.7}
                onPress={() => this._onPressContent(item)}
                style={styles.item}>
                <PlacehoderImage 
                    source={{uri: item.video.thumbnail[0]}}
                    placeholder={{uri: 'placeholder'}}
                    style={{width: itemWidth, height: itemHeight, borderRadius: 4}}
                    />
                <View style={styles.itemText}>
                    <Text style={{color: '#fff'}}>{secToTime(item.video.duration)}</Text>
                    <Text style={{color: '#fff'}}>{item.comment}</Text>
                </View>
            </TouchableOpacity>
        )
    }

    _onPressContent = (item) => {
        this.props.navigation.navigate('ContentDetail', {item});
    }

}

const styles = StyleSheet.create({
    container: {
        flex: 1, 
    },
    item: {
        margin: 4,
    },
    itemText: {
        flexDirection: 'row', 
        justifyContent: 'space-between', 
        alignItems: 'center',
        paddingHorizontal: 10, 
        position: 'absolute', 
        left: 0, 
        right: 0, 
        bottom: 0, 
        height: 30, 
        backgroundColor: '#0002', 
        borderBottomLeftRadius: 4, 
        borderBottomRightRadius: 4
    },
})

效果如下


这里写图片描述

于是一个完整的瀑布流就实现了

当然如果你想要看完整的源码,你可以参考我的这个共享的项目
https://github.com/spicyShrimp/Misses
这是一个仿写百思不得姐的项目,已经基本完成,如果觉得不错,可以给个☆☆,您的支持,是我最大的动力......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容