参差不起的列表页对于视觉效果确实有很好的体验,今天就介绍一下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(
{this.props.children}
);
}
}
exportdefaultclassMasonryListextendsReact.Component {
staticdefaultProps= {
scrollEventThrottle:50,
numColumns:1,
renderScrollComponent:(props:Props)=>{
if(props.onRefresh&&props.refreshing!=null) {
return(
{...props} refreshControl={ refreshing={props.refreshing} onRefresh={props.onRefresh} /> } /> ); } return }, }; 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= ()=> _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= } letemptyElement; if(ListEmptyComponent) { emptyElement= } constcontent= ( {this.state.columns.map(col=> {...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} />, )} ); 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( activeOpacity={0.7} onPress={()=>this._onPressContent(item)} style={{marginRight:15,marginTop:15}}> navutil.push_navigator("MyPage",{uid:item.user_id}) }}style={{width:35}}> { item.sex=='男'? : } ) } _onPressContent= (item)=>{ navutil.push_navigator('TjDetail',{id:item.feed_id}) } render(){ return( 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)} /> ) } } conststyles=StyleSheet.create({ item:{ } });