React-Native 瀑布流布局

参差不起的列表页对于视觉效果确实有很好的体验,今天就介绍一下React-Native实现瀑布流的效果。

实现效果参考与:https://www.jianshu.com/p/88a56de0191d

效果图:

创建MasonryList.js在需要的地方引用

import*asReactfrom'react';

import{

VirtualizedList,

View,

ScrollView,

StyleSheet,

findNodeHandle,

RefreshControl,

}from'react-native';

typeColumn= {

index:number,

totalHeight:number,

data:Array,

heights:Array,

};

const_stateFromProps= ({numColumns,data,getHeightForItem})=>{

constcolumns:Array =Array.from({

length:numColumns,

}).map((col,i)=>({

index:i,

totalHeight:0,

data:[],

heights:[],

}));

data.forEach((item,index)=>{

constheight=getHeightForItem({item,index});

constcolumn=columns.reduce(

(prev,cur)=>(cur.totalHeight

columns[0],

);

column.data.push(item);

column.heights.push(height);

column.totalHeight+=height;

});

return{columns};

};

exporttypeProps= {

data:Array,

numColumns:number,

renderItem: ({item:any,index:number,column:number})=>?React.Element<

any,

>,

getHeightForItem: ({item:any,index:number})=>number,

ListHeaderComponent?: ?React.ComponentType,

ListEmptyComponent?: ?React.ComponentType,

/**

  * 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,

/**

  * 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,

};

typeState= {

columns:Array,

};

// 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).

classFakeScrollViewextendsReact.Component<{style?:any,children?:any}> {

render() {

return(

<Viewstyle={this.props.style}>

{this.props.children}

</View>

);

}

}

exportdefaultclassMasonryListextendsReact.Component {

staticdefaultProps= {

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 = [];

_scrollRef: ?ScrollView;

_endsReached=0;

componentWillReceiveProps(newProps:Props) {

this.setState(_stateFromProps(newProps));

}

getScrollResponder() {

if(this._scrollRef&&this._scrollRef.getScrollResponder) {

returnthis._scrollRef.getScrollResponder();

}

returnnull;

}

getScrollableNode() {

if(this._scrollRef&&this._scrollRef.getScrollableNode) {

returnthis._scrollRef.getScrollableNode();

}

returnfindNodeHandle(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)=>{

constcolumn=this.state.columns[columnIndex];

letoffset=0;

for(letii=0;ii

offset+=column.heights[ii];

}

return{length:column.heights[rowIndex],offset,index:rowIndex};

};

_renderScrollComponent= ()=><FakeScrollViewstyle={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;

letheaderElement;

if(ListHeaderComponent) {

headerElement=<ListHeaderComponent/>;

}

letemptyElement;

if(ListEmptyComponent) {

emptyElement=<ListEmptyComponent/>;

}

constcontent= (

<Viewstyle={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>

);

constscrollComponent=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,

);

returnscrollComponent;

}

}

conststyles=StyleSheet.create({

contentContainer:{

flexDirection:'row',

},

column:{

flex:1,

},

});

实现代码

importReact, {Component}from'react';

import{

Platform,

StyleSheet,

Text,

View,

TouchableOpacity,

Dimensions,

Image,

TextInput,

AsyncStorage,

StatusBar,

BackHandler,

ToastAndroid,

ScrollView,

ImageBackground,

SafeAreaView

}from'react-native';

import{commonStyle}from'../../../utils/commonStyle';

import*asnavutilfrom'../../../utils/navutil';

importRefreshListViewfrom'../../component/RefreshListView';

importMasonryListfrom'../../component/MasonryList';

import*ascommonfrom'../../../utils/common';

import{width,height}from'../../../utils/Device';

import*asCommonUtilsfrom'../../../utils/CommonUtils';

importImagePickerfrom'react-native-syan-image-picker'

constitemWidth= (width-16) /2;//瀑布

import{observer,inject}from'mobx-react'

@inject("wbStore")

@observer

exportdefaultclassAppextendsComponent{

constructor(props){

super(props);

this.state={

refreshing:false,

data:[],

}

}

componentDidMount(){

this.onRefreshing()

}

onRefreshing= ()=>{//下拉刷新,未优化

this.setState({

refreshing:true,

})

letformData=newFormData();

formData.append('page',1);

formData.append('count',4);

formData.append('channel_category_id',this.props.items.channel_category_id) ;

formData.append('type',0);

formData.append('is_app',1);

formData.append('oauth_token',common.rundata.session.oauth_token);

formData.append('oauth_token_secret',common.rundata.session.oauth_token_secret);

fetch(common.urls.domain+"?"+common.urls.channelList, {

method:'POST',

headers:{

'Charset':'utf-8',

'Content-Type':'multipart/form-data',

},

body:formData,

}).then((response)=>{

if(response.ok) {

returnresponse.json();

}

}).then((json)=>{

console.log(json.data.feed_list)

console.log(8888)

this.setState({

refreshing:false,

data:json.data.feed_list,

})

}).catch((error)=>{

console.error(error);

});

}

_onEndReached= ()=>{//上拉加载,未优化

letformData=newFormData();

formData.append('page',this.state.data.length/10+1);

formData.append('count',4);

formData.append('channel_category_id',this.props.items.channel_category_id) ;

formData.append('type',0);

formData.append('is_app',1);

formData.append('oauth_token',common.rundata.session.oauth_token);

formData.append('oauth_token_secret',common.rundata.session.oauth_token_secret);

fetch(common.urls.domain+"?"+common.urls.channelList, {

method:'POST',

headers:{

'Charset':'utf-8',

'Content-Type':'multipart/form-data',

},

body:formData,

}).then((response)=>{

if(response.ok) {

returnresponse.json();

}

}).then((json)=>{

console.log(json.data.feed_list)

console.log(9999)

this.setState({

refreshing:false,

data:[...this.state.data,...json.data.feed_list]

})

}).catch((error)=>{

console.error(error);

});

}

_getHeightForItem= ({item})=>{//获取图片高

returnMath.max(itemWidth,itemWidth/item.image[0].attach_origin_width*item.image[0].attach_origin_height);

}

_renderItem= ({item})=>{

constitemHeight=this._getHeightForItem({item});

return(

<TouchableOpacity

activeOpacity={0.7}

onPress={()=>this._onPressContent(item)}

style={{marginRight:15,marginTop:15}}>

<Imagestyle={{width:itemWidth,height:itemHeight,borderRadius:4}}source={{uri:item.image[0].attach_origin}}/>

<Viewstyle={{backgroundColor:'#FFF',borderBottomColor:'#808080',borderBottomWidth:0.6,padding:8}}>

<TextnumberOfLines={5}style={{lineHeight:25,color:'#808080',}}>{item.content}</Text>

<Viewstyle={{flexDirection:'row',alignItems:'center'}}>

<Imagestyle={{width:15,height:15}}source={require('../../../images/ic_anonymous_like.png')}/>

<Textstyle={{marginLeft:8}}>{item.Fabulous}</Text>

</View>

</View>

<Viewstyle={{flexDirection:'row',backgroundColor:'#FFF',padding:8}}>

<TouchableOpacityactiveOpacity={0.7}onPress={()=>{

navutil.push_navigator("MyPage",{uid:item.user_id})

}}style={{width:35}}>

<Viewstyle={{width:35,height:35,borderRadius:15}}>

<Imagestyle={{width:35,height:35,borderRadius:15}}source={{uri:item.avatar_middle}}/>

</View>

</TouchableOpacity>

<Viewstyle={{flex:1}}>

<Viewstyle={{flexDirection:'row',alignItems:'center',marginLeft:8}}>

<Viewstyle={{flex:1}}>

<Textstyle={{fontSize:13,color:'black'}}>{item.user_name}</Text>

<Viewstyle={{flexDirection:'row',alignItems:'center',marginTop:2}}>

{

item.sex=='男'?<Imagestyle={{width:10,height:10}}source={require('../../../images/nan.png')}/>

:

<Imagestyle={{width:10,height:10}}source={require('../../../images/nv.png')}/>

}

<Textstyle={{fontSize:12,color:'#808080',marginLeft:5}}>{item.grade}</Text>

</View>

</View>

</View>

</View>

</View>

</TouchableOpacity>

)

}

_onPressContent= (item)=>{

navutil.push_navigator('TjDetail',{id:item.feed_id})

}

render(){

return(

<Viewstyle={{flex:1,backgroundColor:'#DDD',paddingLeft:15}}>

<SafeAreaView>

<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={(myItem,index)=>("index"+index)}

/>

</SafeAreaView>

</View>

)

}

}

conststyles=StyleSheet.create({

item:{

}

});

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

推荐阅读更多精彩内容