参差不起的列表页对于视觉效果确实有很好的体验,今天就介绍一下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:{
}
});