Android RecyclerView 頂部懸浮實現

上圖: 本文代碼的Github地址
sticky.gif
思路:
  1. 每一個RecyclerView的item的布局(下文叫itemUI)里面都包含“吸頂文本”這個布局(下文叫StickyLayout),根據當前itemA和上一個itemB的吸頂信息是否相同,決定是否展示itemA的StickyLayout.
  2. 包含RecyclerView的布局(下文叫wrapperUI)最上部分,有一個假的StickyLayout(下文叫FakeStickyLayout).
  3. 監聽RecyclerView的滾動,根據RecyclerView的滾動距離,決定FakeStickyLayout向上或者向下滾動的距離.
代碼解析
  • 先說布局:
wrapperUI.xml

<FrameLayout    
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_behavior="@string/appbar_scrolling_view_behavior

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_sticky_example"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />  

    <include layout="@layout/layout_sticky_header_view" />
</FrameLayout>
itemUI.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"   
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:layout_marginTop="1dp"      
    android:orientation="vertical">  

  <include layout="@layout/layout_sticky_header_view" />  

  <RelativeLayout      
    android:id="@+id/rl_content_wrapper"           ]
    android:layout_width="match_parent"      
    android:layout_height="wrap_content"      
    android:layout_marginTop="1dp"     
    android:padding="10dp">    

  ~~~~~~~

  </RelativeLayout>
</LinearLayout>

這兩個布局沒什么特別,重點是

<include layout="@layout/layout_sticky_header_view" /> 

這個include標簽引用的布局,就是吸頂的那個布局,保證了itemUI中的StickyLayout和wrapperUI中的FakeStickyLayout的布局一致.

  • 再說RecyclerView 的 Adapter:
// RecyclerView 的第一個item,肯定是展示StickyLayout的.
public static final int FIRST_STICKY_VIEW = 1;
// RecyclerView 除了第一個item以外,要展示StickyLayout的.
public static final int HAS_STICKY_VIEW = 2;
// RecyclerView 的不展示StickyLayout的item.
public static final int NONE_STICKY_VIEW = 3;
if (position == 0) {     
    recyclerViewHolder.tvStickyHeader.setVisibility(View.VISIBLE);        
    recyclerViewHolder.tvStickyHeader.setText(stickyExampleModel.sticky);  

    // 第一個item的吸頂信息肯定是展示的,并且標記tag為FIRST_STICKY_VIEW
    recyclerViewHolder.itemView.setTag(FIRST_STICKY_VIEW);

  } else {  
    // 之后的item都會和前一個item要展示的吸頂信息進行比較,不相同就展示,并且標記tag為HAS_STICKY_VIEW
    if (!TextUtils.equals(stickyExampleModel.sticky, stickyExampleModels.get(position - 1).sticky)) {  
      recyclerViewHolder.tvStickyHeader.setVisibility(View.VISIBLE);    
      recyclerViewHolder.tvStickyHeader.setText(stickyExampleModel.sticky);    
      recyclerViewHolder.itemView.setTag(HAS_STICKY_VIEW); 

    } else {       
      // 相同就不展示,并且標記tag為NONE_STICKY_VIEW
      recyclerViewHolder.tvStickyHeader.setVisibility(View.GONE);       
      recyclerViewHolder.itemView.setTag(NONE_STICKY_VIEW);  

    }
  }
// ContentDescription 用來記錄并獲取要吸頂展示的信息
recyclerViewHolder.itemView.setContentDescription(stickyExampleModel.sticky);
  • 說了半天,重點終于來了
rvStickyExample.addOnScrollListener(new RecyclerView.OnScrollListener() {  
    @Override  
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {    
      super.onScrolled(recyclerView, dx, dy);    

      // 找到RecyclerView的item中,和RecyclerView的getTop 向下相距5個像素的那個item
      // (嘗試2、3個像素位置都找不到,所以干脆用了5個像素),
      // 我們根據這個item,來更新吸頂布局的內容,
      // 因為我們的StickyLayout展示的信息肯定是最上面的那個item的信息. 
      View stickyInfoView = recyclerView.findChildViewUnder(tvStickyHeaderView.getMeasuredWidth() / 2, 5);    
      if (stickyInfoView != null && stickyInfoView.getContentDescription() != null) {      
          tvStickyHeaderView.setText(String.valueOf(stickyInfoView.getContentDescription()));    
      }    

      // 找到固定在屏幕上方那個FakeStickyLayout下面一個像素位置的RecyclerView的item,
      // 我們根據這個item來更新假的StickyLayout要translate多少距離. 
      // 并且只處理HAS_STICKY_VIEW和NONE_STICKY_VIEW這兩種tag,
      // 因為第一個item的StickyLayout雖然展示,但是一定不會引起FakeStickyLayout的滾動. 
      View transInfoView = recyclerView.findChildViewUnder(
                         tvStickyHeaderView.getMeasuredWidth() / 2, tvStickyHeaderView.getMeasuredHeight() + 1);    

      if (transInfoView != null && transInfoView.getTag() != null) {      
        int transViewStatus = (int) transInfoView.getTag();      
        int dealtY = transInfoView.getTop() - tvStickyHeaderView.getMeasuredHeight();      

        // 如果當前item需要展示StickyLayout,
        // 那么根據這個item的getTop和FakeStickyLayout的高度相差的距離來滾動FakeStickyLayout. 
        // 這里有一處需要注意,如果這個item的getTop已經小于0,也就是滾動出了屏幕,
        // 那么我們就要把假的StickyLayout恢復原位,來覆蓋住這個item對應的吸頂信息. 
        if (transViewStatus == StickyExampleAdapter.HAS_STICKY_VIEW) {              
          if (transInfoView.getTop() > 0) {          
            tvStickyHeaderView.setTranslationY(dealtY);        
          } else {          
            tvStickyHeaderView.setTranslationY(0);       
          }      
        } else if (transViewStatus == StickyExampleAdapter.NONE_STICKY_VIEW) {        
          // 如果當前item不需要展示StickyLayout,那么就不會引起FakeStickyLayout的滾動. 
          tvStickyHeaderView.setTranslationY(0);      
        }    
      }  
    }
});
相關文章

一個屬性實現帶進度的圓形進度條

寫在末尾:

本人第一次寫簡書,如果有不正確的地方歡迎批評指正.

個人博客:

https://christmasjason.github.io

https://github.com/christmasjason/StickyHeaderView

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容