RecycleView 实现复杂首页布局三种方式

做过电商类应用的朋友可能都会遇到一个比较头疼的问题:复杂的首页布局如何实现。参考百度糯米,美团,bilibili等应用,都会发现其首页的布局相对复杂,例如下图bilibili的首页(第二张是demo实现的效果图),可以看到在同一个页面中先是有列表布局出现,然后出现了2列的网格布局,接着3列的网格布局,最后还出现了瀑布流式布局:

这里写图片描述 这里写图片描述

这样的效果该怎么做呢?是使用LinearLayoutManager、GridLayoutManager还是StaggeredGridLayoutManager?还是根本不是使用的RecycleView,是用ScrollView硬布局实现的?或者使用了多个RecycleView进行嵌套,来实现3种混合布局的排版的?

下面我们一条一条进行梳理:

  • 首先,我们发现页面的长度是无限长度的,可以不断下拉刷新,所有排除ScrollView的可能,基本断定是使用的是RecyclerView
  • 我们注意到同一个页面中出现了3中混合布局的排版,有可能是使用RecycleView进行了2级嵌套,在线性RecycleView中嵌套了网格和瀑布的RecycleView?
  • 如果没有进行嵌套的话,有没有办法用一种自定义的布局管理器实现这3种效果呢?
  • 会不会是使用了某些3方控件?

针对以上3点疑问,我分别提供3种对应的解决方案来实现上图的效果:

方案一:RecyclerView的2级嵌套

看到同一个滚动控件中出现了3种混合布局,多数人第一映像就是进行嵌套。 
如果进行嵌套的话,嵌套什么?从效果图来看,上图的一个栏目中的视图数量似乎是固定的,这意味着可以使用RelativeLayout等布局进行硬排版。确实如果真是固定的这样做当然更好,但是注意到点击每个栏目上的刷新按钮的时候,偶尔会出现两个视图交换位置的动画,这是RecyclerView特有的,而且也没有人告诉我每个栏目中的视图数量就是固定,万一哪天又多了一排呢,所以我们还是嵌套RecyclerView,具体嵌套规则如下图:

这里写图片描述

其实最外层的RecyclerView1换成ScrollView也可以,只要把内部的RecyclerView依次拼接起来就可以,实现起来也更加简单,实现方式千千万,自己选个喜欢的而已,我只是为了便于拓展,万一需要动态的增加栏目呢。

上一篇就说到了RecyclerView的嵌套的问题,给每个RecyclerView设置对应的Fully****LayoutManager就可以了。具体实现参见源码,我就不贴了,重新getItemViewType方法为每个position位置的item设置不同的type类型,然后在onCreateViewHolder创建对应的Holder,最后在onBindViewHolder为不同类型的item设置不同的Fully布局的子RecyclerView就行了。

上关键代码(有删减):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
            
            
@Override
public void onAttachedToRecyclerView ( final RecyclerView recyclerView ) {
super . onAttachedToRecyclerView ( recyclerView );
RecyclerView . LayoutManager manager = recyclerView . getLayoutManager ();
if ( manager instanceof GridLayoutManager ) {
final GridLayoutManager gridManager = (( GridLayoutManager ) manager );
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
int type = getItemViewType ( position );
switch ( type ){
case TYPE_SLIDER:
case TYPE_TYPE2_HEAD:
case TYPE_TYPE3_HEAD:
return 6 ;
case TYPE_TYPE2:
return 3 ;
case TYPE_TYPE3:
return 2 ;
default :
return 3 ;
}
}
});
}
}
来自CODE的代码片
GridView.java
 1
 2
 3
 4
 5
 6
            
            
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
return gridManager . getSpanCount ();
}
}
来自CODE的代码片
GridViewManager.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
            
            
@Override
public void onBindViewHolder ( RecyclerView . ViewHolder holder , int position ) {
if ( holder instanceof MyViewHolder1 ){
holder . child_recyclerView . setLayoutManager ( new FullyLinearLayoutManager ( context ));
} else if ( holder instanceof MyViewHolder2 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 2 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder3 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder4 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
}
}
来自CODE的代码片
RecyclerView.java
 1
            
            
<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.setLayoutManager</span>(new GridLayoutManager(recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.getContext</span>(), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, GridLayoutManager<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.VERTICAL</span>, false))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
来自CODE的代码片
blog_20160928_3_4374465

这种方式我不多介绍,因为在前一篇也说过,google不建议对RecyclerView、ListView进行嵌套,嵌套使用的话那么视图复用机制就等于白费了,

依然会在第一次加载时初始化所有的item,如果数据量过大则会产生性能障碍,所以我也遵循google的建议不推荐使用嵌套解决问题,我更加推荐下

面一种方案。

方案二:使用特殊的(自定义)布局管理器

这种方案是我认为最为优秀的做法,它完全符合Google制定的标准:使用布局管理器来管理布局。我们继续观察首页布局的图示,我们真的要为了实现这种混合布局自己去写一个布局管理器吗?我们发现上面出现了列表、网格、瀑布流3种交叉混排的混合布局。我们先把瀑布流放在一边,仔细想想如果我们把网格的列数设置为1列,那不就是一个列表布局吗,也就是说我们使用网格布局管理器就可以做出列表的样式,所以说虽然是说用自定义布局管理器,但实际上不需要我们自定义,GridLayoutManager为我们提供了动态改变每个item所占列数的方法:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
            
            
@Override
public void onAttachedToRecyclerView ( final RecyclerView recyclerView ) {
super . onAttachedToRecyclerView ( recyclerView );
RecyclerView . LayoutManager manager = recyclerView . getLayoutManager ();
if ( manager instanceof GridLayoutManager ) {
final GridLayoutManager gridManager = (( GridLayoutManager ) manager );
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
int type = getItemViewType ( position );
switch ( type ){
case TYPE_SLIDER:
case TYPE_TYPE2_HEAD:
case TYPE_TYPE3_HEAD:
return 6 ;
case TYPE_TYPE2:
return 3 ;
case TYPE_TYPE3:
return 2 ;
default :
return 3 ;
}
}
});
}
}
来自CODE的代码片
GridView.java
 1
 2
 3
 4
 5
 6
            
            
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
return gridManager . getSpanCount ();
}
}
来自CODE的代码片
GridViewManager.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
            
            
@Override
public void onBindViewHolder ( RecyclerView . ViewHolder holder , int position ) {
if ( holder instanceof MyViewHolder1 ){
holder . child_recyclerView . setLayoutManager ( new FullyLinearLayoutManager ( context ));
} else if ( holder instanceof MyViewHolder2 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 2 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder3 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder4 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
}
}
来自CODE的代码片
RecyclerView.java
 1
            
            
<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.setLayoutManager</span>(new GridLayoutManager(recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.getContext</span>(), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, GridLayoutManager<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.VERTICAL</span>, false))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
来自CODE的代码片
blog_20160928_3_4374465

getSpanSize方法,返回值就表示当前item占多少列,例如如果我们列数设置的为3列,返回3就表示铺满,也就是和列表一样了。

如图所示,我们给RecyclerView设置一个列数为6的GridLayoutManager,然后再动态地为不同部位的item分别设置SpanSize为6(铺满)、3(1/2)、2(1/3)就行了

这里写图片描述

设置一个列数为6的GridLayoutManager:

recyclerView.setLayoutManager(new GridLayoutManager(recyclerView.getContext(),6, GridLayoutManager.VERTICAL, false));

在onAttachedToRecyclerView方法中动态为不同position设置不同的SpanSize:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
            
            
@Override
public void onAttachedToRecyclerView ( final RecyclerView recyclerView ) {
super . onAttachedToRecyclerView ( recyclerView );
RecyclerView . LayoutManager manager = recyclerView . getLayoutManager ();
if ( manager instanceof GridLayoutManager ) {
final GridLayoutManager gridManager = (( GridLayoutManager ) manager );
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
int type = getItemViewType ( position );
switch ( type ){
case TYPE_SLIDER:
case TYPE_TYPE2_HEAD:
case TYPE_TYPE3_HEAD:
return 6 ;
case TYPE_TYPE2:
return 3 ;
case TYPE_TYPE3:
return 2 ;
default :
return 3 ;
}
}
});
}
}
来自CODE的代码片
GridView.java
 1
 2
 3
 4
 5
 6
            
            
gridManager . setSpanSizeLookup ( new GridLayoutManager . SpanSizeLookup () {
@Override
public int getSpanSize ( int position ) {
return gridManager . getSpanCount ();
}
}
来自CODE的代码片
GridViewManager.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
            
            
@Override
public void onBindViewHolder ( RecyclerView . ViewHolder holder , int position ) {
if ( holder instanceof MyViewHolder1 ){
holder . child_recyclerView . setLayoutManager ( new FullyLinearLayoutManager ( context ));
} else if ( holder instanceof MyViewHolder2 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 2 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder3 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
} else if ( holder instanceof MyViewHolder4 ){
holder . child_recyclerView . setLayoutManager ( new FullyGridLayoutManager ( context , 3 , GridLayoutManager . VERTICAL , false ));
}
}
来自CODE的代码片
RecyclerView.java
 1
            
            
<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.setLayoutManager</span>(new GridLayoutManager(recyclerView<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.getContext</span>(), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, GridLayoutManager<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.VERTICAL</span>, false))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
来自CODE的代码片
blog_20160928_3_4374465

那么有朋友就要问了,说好的瀑布流呢? 

我查阅了StaggeredGridLayoutManager,发现它并没有提供动态设置所占列的方法,只是在StaggeredGridLayoutManager.LayoutParams中提供了这样一个方法:

LayoutParams.setFullSpan(true);

作用是把当前item的宽度设为full(填满),也就是说如果使用StaggeredGridLayoutManager要么不设置,要么就只能填满,

所以无法完成图上的效果,我们也并不是非要完全仿照它,bilibili在最近一次更新后也放弃使用瀑布流式的布局了,统一为列表和网格式混排。

当然如果要实现图上的效果也不是没有办法,只需要换一种方式,改一下item,把设置为FullSpan的item设置为一个多个视图组合的复合item就行了,放个图,代码就不上了:

这里写图片描述


哪种方案最好?

上面的2种方案中,无疑第二种是最佳的解决方案,没有引入过多的依赖,完全使用RecyclerView布局管理器的特性实现,从性能上来说也最佳。但是TwowayView能做到更多其他的支持,比如网格布局等(具体参见github : twoway-view),也不失为一种好的方式,而第一种方案我也就不多说了,不推荐,这种嵌套不仅让代码逻辑变得复杂凌乱(demo里写嵌套的时候我差点没写吐),同时还有性能障碍,所以给出一个结论:2>1

下载

最后,附上demo下载: 
demo下载地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值