Android魔法系列: http://blog.csdn.net/chzphoenix/article/details/77962259
项目的github地址: FastWidget4Android  很多炫酷的自定义效果,欢迎fork和star!

1、效果展示

这个效果是一年多前完成的,是模仿了当时喵街app的首页的效果,现在整理出来可能有些过时了,不过一些知识点和思路还是很有帮助的。实现后效果如下:

2、效果分析

首先我们看静止状态,如图: Android魔术(第五弹)—— 一步步实现滑动折叠列表_第1张图片
这时处于顶端展示的item相对于其他item是展开的状态,有几点表现:一是整体高度要高一些;二是无遮罩高亮状态;三是文字内容大一些。这样就达到了一个凸显的效果。
然后我们观察滑动中的状态,如图: Android魔术(第五弹)—— 一步步实现滑动折叠列表_第2张图片
当我们向上滑动的时候,可以看到第一个item开始折叠,而第二个item逐渐展开,同时遮罩效果减弱,文字内容逐渐变大。这样就产生了滑动折叠的效果。
而且,为了能让最后的item也可以凸显出来,我们需要在列表的结尾插入一个footer以保证最后的item可以置顶显示,如图: Android魔术(第五弹)—— 一步步实现滑动折叠列表_第3张图片


3、Item布局

效果分析完了,下面我们来看看如何实现。 首先是Item的布局,这里只关注重要的部分,代码如下: xmlns: android = "http://schemas.android.com/apk/res/android"
              android :layout_width= "match_parent"
              android :layout_height= "wrap_content" >

            android :id= "@+id/item_content"
        android :layout_width= "match_parent"
        android :layout_height= "@dimen/scroll_fold_item_height" >

                    android :id= "@+id/item_img"
            android :layout_width= "match_parent"
            android :layout_height= "match_parent"
            android :scaleType= "fitXY" />

                    android :id= "@+id/item_img_shade"
            android :layout_width= "match_parent"
            android :layout_height= "match_parent"
            android :src= "#000000" />

                    android :id= "@+id/scale_item_content"
            android :layout_width= "200dp"
            android :layout_height= "wrap_content"
            android :layout_centerHorizontal= "true"
            android :orientation= "vertical" >

            ...
           
       

        ...
       
   
最外层用FrameLayout,这样当 FrameLayout高度变小时,item_content可以超出 FrameLayout的范围,产生折叠的效果。 item_content的高度是固定不变的,真正改变的是外层的 FrameLayout。 scale_item_content中是那些大小可变的文字内容 布局比较简单,后面会讲到如何使用这些layout达到效果。
另外还有一个footer的布局,因为很简单就不贴出代码了。

3、实现Adapter

列表是通过RecyclerView来实现的,所以我们先实现Adapter。代码也比较简单,我们挑重点说。 首先是Adapter的两个基本方法的实现: @Override
public ViewHolder  onCreateViewHolder(ViewGroup parent , int viewType) {
    if(viewType ==  0) {
      View item = LayoutInflater. from( mContext).inflate(R.layout. scroll_fold_list_item , null) ;
      return new ItemViewHolder(item) ;
   }
    else{
      View bottom = LayoutInflater. from( mContext).inflate(R.layout. scroll_fold_list_footer , null) ;
      return new BottomViewHolder(bottom) ;
   }
}

@Override
public void  onBindViewHolder(ViewHolder holder , int position) {
   holder.initData(position) ;
} 这里使用viewType来区分普通的item和footer(通过getItemViewType方法)。BottomViewHolder和ItemViewHolder继承同一个类,代码如下: abstract class ViewHolder  extends RecyclerView.ViewHolder{
   View  item ;

   public  ViewHolder(View itemView) {
      super(itemView) ;
      item = itemView ;
   }

    abstract void  initData( int position) ;
}

class BottomViewHolder  extends ViewHolder{

    public  BottomViewHolder(View itemView) {
      super(itemView) ;
   }

    @Override
    void  initData( int position) {
      ViewGroup.LayoutParams bottomParams =  itemView.getLayoutParams() ;
      if(bottomParams ==  null){
         bottomParams =  new ViewGroup.LayoutParams(ViewGroup.LayoutParams. MATCH_PARENT 0) ;
      }
      bottomParams. height recyclerView.getHeight() -  itemHeight 10 ;
      itemView.setLayoutParams(bottomParams) ;
   }
}

class ItemViewHolder  extends ViewHolder{
   View  content ;
   ImageView  image ;
   TextView  name ;

   public  ItemViewHolder(View itemView) {
      super(itemView) ;
      item = itemView ;
      content = itemView.findViewById(R.id. item_content) ;
      image = (ImageView)itemView.findViewById(R.id. item_img) ;
      name = (TextView) itemView.findViewById(R.id. item_name) ;
   }

    void  initData( int position){
      image.setImageResource( IMGS[position]) ;
      name.setText( NAMES[position]) ;
      ViewGroup.LayoutParams params =  item.getLayoutParams() ;
      if(params ==  null){
         params =  new ViewGroup.LayoutParams(ViewGroup.LayoutParams. MATCH_PARENT 0) ;
      }
      params. height itemSmallHeight ;
      content.findViewById(R.id. item_img_shade).setAlpha( ITEM_SHADE_DARK_ALPHA) ;
      item.setLayoutParams(params) ;
   }
}
我们先看BottomViewHolder,动态的设置footer的高度为列表高度减去itemHeight,再加上10像素。这个 itemHeight是展开后item的高度,即置顶的item的高度。这里之所以再加上10像素,是因为如果设置高度正好是余下的高度,当快速滑动到底部的时候有几率会出现问题,所以这里让高度略大于实际展示的高度。
然后来看ItemViewHolder,也是动态的设置高度为ItemSmallHeight,这个高度是收缩后item的高度,而且将遮罩设置为最暗。注意这里全部初始化为收缩状态,没有单独设置一个置顶展开的状态,这个我们后面会解释为什么。

4、监听滑动

上面我们完成了adapter类,添加给RecyclerView即可。不过想要实现效果,就需要监听 RecyclerView的滑动,并做相应的处理,代码如下: list.addOnScrollListener( new RecyclerView.OnScrollListener() {
    @Override
    public void  onScrolled(RecyclerView recyclerView , int dx , int dy) {
      changeItemState() ;
   }

    @Override
    public void  onScrollStateChanged(RecyclerView recyclerView , int newState) {
      ...
   }
}) ;
可以看到在滑动过程(onScrolled)中调用changeItemState()这个函数,代码如下: private void  changeItemState(){
    int firstVisibleIndex =  linearLayoutManager.findFirstVisibleItemPosition() ;
   ViewGroup first = (ViewGroup)  linearLayoutManager.findViewByPosition(firstVisibleIndex) ;
   int firstVisibleOffset = -first.getTop() ;
   int changeheight = ( int) (firstVisibleOffset * (ScrollFoldAdapter. ITEM_CONTENT_TEXT_SCALE 1)) ;

    // 减少当前展示的第一个item的高度。
    if (first ==  null) {
      return;
   }
   changeItemHeight(first itemHeight - changeheight) ;
   changeItemState(first ScrollFoldAdapter. ITEM_CONTENT_TEXT_SCALE ScrollFoldAdapter. ITEM_SHADE_LIGHT_ALPHA) ;

    // 增大当前展示的第二个item的高度,改变内容大小,改变透明度
    if (firstVisibleIndex +  adapter.getItemCount() -  1) {
      ViewGroup second = (ViewGroup)  linearLayoutManager.findViewByPosition(firstVisibleIndex +  1) ;
      changeItemHeight(second itemSmallHeight + changeheight) ;

      float scale = ( float) firstVisibleOffset /  itemSmallHeight
            * (ScrollFoldAdapter. ITEM_CONTENT_TEXT_SCALE 1) +  1.0f ;
      float alpha = (ScrollFoldAdapter. ITEM_SHADE_DARK_ALPHA - ScrollFoldAdapter. ITEM_SHADE_LIGHT_ALPHA)
            * ( - ( float) firstVisibleOffset /  itemSmallHeight)
            + ScrollFoldAdapter. ITEM_SHADE_LIGHT_ALPHA ;
      changeItemState(second scale alpha) ;
   }

    /**
    * 由于快速滑动,导致计算及状态有误 所以下面就是消除这种误差,校准状态。具体如下
    * 将第一个item上面(存在的)的和第二个Item下面的都变为收缩的高度,内容缩放到最小,透明度为0。65
    */
    for ( int i =  0 i <=  linearLayoutManager.findLastVisibleItemPosition() i++) {
      if (i <  adapter.getItemCount() -  && i != firstVisibleIndex && i != firstVisibleIndex +  1) {
         ViewGroup item = (ViewGroup)  linearLayoutManager.findViewByPosition(i) ;
         if(item ==  null){
                    continue;
               }
               changeItemHeight(item itemSmallHeight) ;
         float scale =  1 ;
         float alpha = ScrollFoldAdapter. ITEM_SHADE_DARK_ALPHA ;
         changeItemState(item scale alpha) ;
      }
   }
}
整体思路如下: 获取当前置顶展示的item,计算该item相对于列表顶端的偏移。这个偏移是关键参数,通过这个偏移计算出第一个item收缩的高度和第二个item展开的高度,并且计算第二个item遮罩的透明度和文字内容的大小。 这里调用了另外两个函数changeItemHeight(view, int)和changeItemState(view, float, float)。其中 changeItemHeight(view, int)用来改变item的高度实现展开或折叠;而 changeItemState(view, float, float)用来改变遮罩透明度和文字内容大小。两个函数代码如下: /**
 * 改变一个item的高度。
 *
 *  @param  item
  @param  height
  */
private void  changeItemHeight(View item , int height) {
   ViewGroup.LayoutParams itemParams = item.getLayoutParams() ;
   itemParams. height = height ;
   item.setLayoutParams(itemParams) ;
}

/**
 * 改变一个item的状态,包括透明度,大小等
 *  @param  item
  @param  scale
  @param  alpha
  */
private void  changeItemState(ViewGroup item , float scale , float alpha) {
    if (item.getChildCount() >  0) {
      View changeView = item.findViewById(R.id. scale_item_content) ;
      changeView.setScaleX(scale) ;
      changeView.setScaleY(scale) ;

      View shade = item.findViewById(R.id. item_img_shade) ;
      shade.setAlpha(alpha) ;
   }
}
改变高度很简单,没必要解释了。改变遮罩透明度就是改变其alpha,而文字内容大小的改变则是利用setScaleX和setScaleY两个函数,实际上是将scale_item_content这个layout整个进行缩放,其内容就会随着变大/小。
回到 changeItemState()函数,改变了第一个和第二个item后,可以看到又将其他的item置为收缩状态。这是因为快速滑动会造成某些item处于中间的状态,做这一步操作就是校正快速滑动导致的一些问题。
上面我们提到过,所有的item都初始化成收缩状态了。其实当RecyclerView添加到屏幕上时,是一定会产生滑动的。所以我们进入页面的时候,我们什么都没有操作,滑动监听的函数却被调用了。这样通过 changeItemState()函数就可以将置顶的item变为展开状态,所以初始的展示状态是正确的。

5、回弹效果

以上是滑动的时候的处理,然而这样还不够。当滑动停止的时候,有可能第一个item正处于显示一半的状态,这样第二个item也没有完全展开,显示效果不好。 所以我们还需要实现一个回弹效果,当滑动停止的时候,让列表自动调整到某一个item正好置顶的状态。 这部分的处理在滑动监听的onScrollStateChanged中,代码如下: list.addOnScrollListener( new RecyclerView.OnScrollListener() {
    @Override
    public void  onScrolled(RecyclerView recyclerView , int dx , int dy) {
      changeItemState() ;
   }

    @Override
    public void  onScrollStateChanged(RecyclerView recyclerView , int newState) {
      if (newState == RecyclerView. SCROLL_STATE_IDLE) {
          int firstVisibleIndex =  linearLayoutManager.findFirstVisibleItemPosition() ;
         View first =  linearLayoutManager.findViewByPosition(firstVisibleIndex) ;
         int firstVisibleOffset = -first.getTop() ;
         if (firstVisibleOffset ==  0) {
            return;
         }
          if (firstVisibleOffset <  itemSmallHeight 2) {
            list.scrollBy( 0 -firstVisibleOffset) ;
         else {
            list.scrollBy( 0 itemSmallHeight - firstVisibleOffset) ;
         }
         changeItemState() ;
      }
   }
}) ;
上面是完整的滑动监听的代码。在 onScrollStateChanged中,判断状态是否是滑动结束(SCROLL_STATE_IDLE)。如果滑动结束,判断顶部显示的item的偏移,根据偏移的大小选择回弹方向。如果偏移很小(第一个item大部分内容显示出来了),则下滚至第一个item置顶的状态;否则上滚至第二个item置顶的状态。 这样保证了静止状态下一定有一个item完全置顶高亮显示。 最后又调用了changeItemState函数,主要目的是校正一些误差。

6、总结一下

整个效果中其实没有太多难点,主要是考察了对RecyclerView滑动的理解。目前这个版本在快滑时还有一个小问题。 除了RecyclerView这个版本,实际上这个效果还有一个ScrollView的版本。其实在ListView和RecyclerView上实现这个效果都多少有些问题。所以我早期自己实现了能够复用和回收的 ScrollView,利用这个自定义的 ScrollView实现了这个效果,并且为其自定义了scroller使其回弹有了动画效果。 ScrollView版本目前未发现任何问题,但是由于很多功能要自己实现,整体代码比较复杂,就选用了 RecyclerView这个版本来给大家讲解。大家有兴趣可以去github上的项目中,切到tag v1.0就可以看到了。

项目的github地址:

FastWidget4Android  很多炫酷的自定义效果,欢迎fork和star!
Android魔法系列: http://blog.csdn.net/chzphoenix/article/details/77962259

联系我

Android魔术(第五弹)—— 一步步实现滑动折叠列表_第4张图片


更多相关文章

  1. android变色状态栏
  2. Android 动画分析之翻转效果
  3. Android 监听U盘OTG挂载状态
  4. Android 广播监听应用APK卸载、覆盖、安装的状态
  5. 如何在Android中实现全屏,去掉标题栏效果
  6. Android实现自定义滑动式抽屉效果菜单
  7. Android设置沉浸式状态栏时改变状态栏的颜色(只对MIUI V6可用)

随机推荐

  1. android中去掉ActionBar或TabWidget的分
  2. Android用HTTP下载报错“android.os.Stri
  3. Android Studio中Edittext监听回车事件,
  4. Android中获得屏幕的尺寸
  5. android移动开发的很好的功能的网页
  6. 设置环境变量
  7. Android +Xstream
  8. android 跑马灯实现
  9. 2011.08.12——— android MediaPlayer
  10. js与android iOS 交互兼容