1. 程式人生 > >簡單粗暴實現RecycleView的瀑布流的粘性頭部(非ItemDecoration實現)

簡單粗暴實現RecycleView的瀑布流的粘性頭部(非ItemDecoration實現)

專案要用到粘性頭部,以前的ListView和GridView的還好整,RecycleView的一片茫然,在github上找了很多發現好複雜,使用ItemDecoration實現,這貨以我的智商真難搞懂,或者只適配了LinearLayoutManager和GridLayoutManager,很少適配了StaggeredGridLayoutManager,我的需求恰恰是瀑布流,只設置兩個粘性頭部,於是我利用幀佈局and監聽滑動事件移動佈局來實現了這一需求。

首先看下Demo的效果圖吧:
這裡寫圖片描述

是不是還過得去呢^_^,接下來貼程式碼,程式碼有詳細的註釋了

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height
="match_parent" android:background="#ffffff" android:scrollbars="none" />
<RelativeLayout android:id="@+id/rl_sticky_head" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="2.5dp" android:layout_marginRight
="2.5dp">
<include layout="@layout/flashgo_header" /> </RelativeLayout> <RelativeLayout android:id="@+id/rl_sticky_head_fake" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="2.5dp" android:layout_marginRight="2.5dp" android:visibility="invisible"> <include android:id="@+id/tv_fake_sticky_head" layout="@layout/flashgo_header" /> </RelativeLayout> </FrameLayout>

主佈局為一個幀佈局,包含了RecyclerView和兩個flashgo_header,flashgo_header佈局是用來做為粘性頭部的,RecyclerView在位置0那裡會相應地新增一個flashgo_header佈局,這樣子RecyclerView那麼真正要展示的圖片位置就不會收到遮掩了,
那麼幹哈要兩個呢,rl_sticky_head是作為Header One使用的;rl_sticky_head_fake是作為Header Two使用的,最外層就是它了,預設為INVISIBLE。
它的使用如下圖講解所示,是用來偽造RecycleView頭部二到頂部固定的效果
這裡寫圖片描述

然後就是MainActivity的程式碼了:
該解釋的都解釋了……

public class MainActivity extends AppCompatActivity{

    private int mHalfScreenWidth;

    private RecyclerView mRecyclerView;
    private RecyclerAdapter mAdapter;
    private StaggeredGridLayoutManager mManager;

    private RelativeLayout mStickyHeadLayout;
    private RelativeLayout mFakeStickyHeadLayout;
    private TextView mStickyHead;
    private TextView mFakeStickyHead;
    private int mStickyHeadHeight;

    private int mHeaderOneCount = 0;

    private List<Integer> mProductInfos;
    private int mProductInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        initData();

    }

    private void initView() {

        if (getSupportActionBar() != null) {
            getSupportActionBar().setTitle("瀑布流粘性頭部Demo");
        }
        // 幀佈局Header One的頭部
        mStickyHeadLayout = (RelativeLayout) findViewById(R.id.rl_sticky_head);

        mStickyHead = (TextView) findViewById(R.id.tv_sticky_head);
        mStickyHead.setText("Header One");
        mStickyHead.measure(0, 0);
        // 獲取頭部的高度,作為是否移動頭部的範圍
        mStickyHeadHeight = mStickyHead.getMeasuredHeight();

        // 幀佈局的Header Two的頭部,在幀佈局裡面的最外的一層,
        // 預設為不可見,用於recycleview移動真正的第二個頭部到剛剛出介面時設定為可見
        // 形成一種recycleview真正的第二個頭部到頂端停住的效果
        // 實際recycleview真正的第二個頭部還是繼續上移,只是幀佈局的Header Two的頭部設定為可見而已
        mFakeStickyHeadLayout = (RelativeLayout) findViewById(R.id.rl_sticky_head_fake);

        mFakeStickyHead = (TextView) findViewById(R.id.tv_fake_sticky_head);
        mFakeStickyHead.setText("Header Two");

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        mRecyclerView.addOnScrollListener(new MyScrollListener());

    }

    class MyScrollListener extends RecyclerView.OnScrollListener {
        // 用於獲取瀑布流的不完全可見的位置,0為左邊不完全可見的位置,1為右邊不完全可見的位置
        int mVisiblePosition[] = new int[2];
        // recycleview的第二個頭部,內容高度跟幀佈局的Header Two的頭部一模一樣
        private View header;
        // 用於recycleview的第二個頭部剛剛進入幀佈局Header One的頭部的範圍時,
        // 強制設定位移量dy為1,防止使用者上推過快,dy過大造成的幀佈局Header One的頭部不同步移動
        boolean isFirstIn;

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            // 查詢第一個不完全可見的左右兩個item的位置
            mManager.findFirstVisibleItemPositions(mVisiblePosition);

            // 獲取第二個頭部的示例,有可能為null
            header = mManager.findViewByPosition(mHeaderOneCount + 1);

            if (mHeaderOneCount != 0
                    && header != null
                    && ViewHelper.getY(header) > 0
                    && ViewHelper.getY(header) < mStickyHeadHeight) {
                //  0<ViewHelper.getY(header)<mStickyHeadHeight 說明recycleview的第二個頭部
                // 進入幀佈局Header One的頭部高度的範圍,此時可以移動幀佈局Header One的頭部
                if (!isFirstIn) {
                    // 剛剛進入高度範圍強制設定位移量dy為1,防止使用者上推過快,
                    // dy過大造成的幀佈局Header One的頭部不同步移動
                    isFirstIn = true;
                    dy = 1;
                }
                // 移動幀佈局Header One的頭部
                mStickyHeadLayout.scrollBy(0, dy);
                // 在此過程中幀佈局的Header Two的頭部設為不可見
                mFakeStickyHeadLayout.setVisibility(View.INVISIBLE);
                if (mStickyHeadLayout.getScrollY() < 0) {
                    // 防止上推過快,移動幀佈局Header One的頭部上移超過其高度
                    mStickyHeadLayout.scrollBy(0, -mStickyHeadLayout.getScrollY());
                }
            } else if (mHeaderOneCount != 0
                    && header != null
                    && ViewHelper.getY(header) <= 0) {
                //  recycleview的第二個頭部剛剛移出介面的時候,設定幀佈局的Header Two的頭部為可見
                //  這樣看上去好像recycleview的第二個頭部剛好停在頂部
                mFakeStickyHeadLayout.setVisibility(View.VISIBLE);
            } else if (mHeaderOneCount != 0
                    && header != null
                    && ViewHelper.getY(header) >= mStickyHeadHeight) {
                // recycleview的第二個頭部超出幀佈局Header One的頭部高度的範圍,
                // 此時設定isFirstIn為false方便下次上推設dy值
                isFirstIn = false;
            }

            if (header != null && dy > 0 && mStickyHeadLayout.getScrollY() != mStickyHeadHeight && ViewHelper.getY(header) < 0) {
                // 這個情況是針對recycleview的第二個頭部移出佈局的時候,由於上推滑動過快,
                // 造成的同步上移的幀佈局Header One的頭部未完全移出或移出超過其高度
                // 這裡再調整幀佈局Header One的頭部上移距離為其高度,即剛剛好移出介面
                mStickyHeadLayout.scrollBy(0, mStickyHeadHeight - mStickyHeadLayout.getScrollY());
            } else if (header != null
                    // 這個情況是針對下推的時候,recycleview的第二個頭部超出幀佈局Header One的頭部高度的範圍,
                    // 但是幀佈局Header One的頭部未完全下移到原來的位置,這裡再調整它移回到最初的位置
                    && dy < 0
                    && mStickyHeadLayout.getScrollY() > 0
                    && ViewHelper.getY(header) > mStickyHeadHeight
                    // 這個起來是針對使用RecyclerView.scrollToPosition(0)的時候,如果此時已經上推了幀佈局Header One的頭部,
                    // 此時幀佈局Header One的頭部不會回到原來位置,因為scrollToPosition太快了
                    // 上面的ViewHelper.getY(header) > 0 && ViewHelper.getY(header) < mStickyHeadHeight此處的判斷執行不了
                    // 下移不回原來的位置,所以這裡特殊處理,如果位置為0的時候,幀佈局Header One的頭部的scrollY不為0,強制移回原處
                    || (mStickyHeadLayout.getScrollY() > 0 && mVisiblePosition[0] == 0)) {
                mStickyHeadLayout.scrollBy(0, -mStickyHeadLayout.getScrollY());
            }

            if (mVisiblePosition[0] > mHeaderOneCount + 1 && mFakeStickyHeadLayout.getVisibility() == View.INVISIBLE) {
                // 針對使用者超快速滑動情況,當位置大於第二個頭部的時候,如果FakeStickyHeadLayout還為不可見的話,需要設為可見
                mFakeStickyHeadLayout.setVisibility(View.VISIBLE);
            } else if (mVisiblePosition[0] < mHeaderOneCount + 1 && mFakeStickyHeadLayout.getVisibility() == View.VISIBLE) {
                // 針對使用者超快速滑動情況,當位置小於第二個頭部的時候,如果FakeStickyHeadLayout還為可見的話,需要設為不可見
                mFakeStickyHeadLayout.setVisibility(View.INVISIBLE);
            }

        }
    }

    class SpacesItemDecoration extends RecyclerView.ItemDecoration {

        private int space;
        private StaggeredGridLayoutManager.LayoutParams lp;

        public SpacesItemDecoration(int space) {
            this.space = space;
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            int position = parent.getChildAdapterPosition(view);

            outRect.left = space;
            outRect.right = space;
            outRect.bottom = space;

            if (position == 0) {
                outRect.top = 0;
            } else if (position == mHeaderOneCount + 1) {
                outRect.top = 0;
            } else {
                lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
                // 左item的對右間隔設為0,保證item間隔一致
                if (lp.getSpanIndex() == 0) {
                    outRect.right = 0;
                }
            }

        }
    }

    private void initData() {

        mProductInfos = new ArrayList<>();

        mProductInfos.add(R.drawable.ic_girls_0);
        mProductInfos.add(R.drawable.ic_girls_1);
        mProductInfos.add(R.drawable.ic_girls_2);
        mProductInfos.add(R.drawable.ic_girls_3);
        mProductInfos.add(R.drawable.ic_girls_4);
        mProductInfos.add(R.drawable.ic_girls_5);
        mProductInfos.add(R.drawable.ic_girls_6);
        mProductInfos.add(R.drawable.ic_girls_7);
        mProductInfos.add(R.drawable.ic_girls_8);
        mProductInfos.add(R.drawable.ic_girls_9);
        mProductInfos.add(R.drawable.ic_girls_10);

        // Header One下面的圖片總數
        mHeaderOneCount = mProductInfos.size();

        mProductInfos.add(R.drawable.ic_view_0);
        mProductInfos.add(R.drawable.ic_view_1);
        mProductInfos.add(R.drawable.ic_view_2);
        mProductInfos.add(R.drawable.ic_view_3);
        mProductInfos.add(R.drawable.ic_view_4);
        mProductInfos.add(R.drawable.ic_view_5);
        mProductInfos.add(R.drawable.ic_view_6);
        mProductInfos.add(R.drawable.ic_view_7);
        mProductInfos.add(R.drawable.ic_view_8);
        mProductInfos.add(R.drawable.ic_view_9);
        mProductInfos.add(R.drawable.ic_view_10);
        mProductInfos.add(R.drawable.ic_view_11);
        mProductInfos.add(R.drawable.ic_view_12);
        mProductInfos.add(R.drawable.ic_view_13);


        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);

        mHalfScreenWidth = metrics.widthPixels / 2;

        // 瀑布流,兩列,垂直方向
        mManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(mManager);
        mRecyclerView.addItemDecoration(new SpacesItemDecoration(getResources().getDimensionPixelSize(R.dimen.common_margin_left)));

        mAdapter = new RecyclerAdapter();
        mRecyclerView.setAdapter(mAdapter);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            // recycleview返回位置0
            mRecyclerView.scrollToPosition(0);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.RecyclerViewHolder> {

        class RecyclerViewHolder extends RecyclerView.ViewHolder {

            // item展示圖片
            ImageView mProductImage;

            // 頭部的文字
            TextView stickyTextview;

            public RecyclerViewHolder(View itemView) {
                super(itemView);
                mProductImage = (ImageView) itemView.findViewById(R.id.iv_home_product);

                stickyTextview = (TextView) itemView.findViewById(R.id.tv_sticky_head);
            }
        }

        // 頭部型別
        public final int VIEW_TYPE_HEADER = 0;
        // item型別
        public final int VIEW_TYPE_REPLY = 1;

        @Override
        public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            RecyclerViewHolder viewHolder;
            switch (viewType) {
                case VIEW_TYPE_HEADER:
                    viewHolder = new RecyclerViewHolder
                            (LayoutInflater.from(MainActivity.this).inflate(R.layout.flashgo_header, parent, false));
                    return viewHolder;
                case VIEW_TYPE_REPLY:
                    viewHolder = new RecyclerViewHolder
                            (LayoutInflater.from(MainActivity.this).inflate(R.layout.recycleview_item, parent, false));
                    return viewHolder;
            }
            return null;
        }

        // 封裝圖片寬高
        ImageSize mImageSize;

        @Override
        public void onBindViewHolder(final RecyclerViewHolder holder, final int position) {

            switch (getItemViewType(position)) {
                case VIEW_TYPE_HEADER:
                    StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) holder.stickyTextview.getLayoutParams();
                    // 佔滿一行
                    lp.setFullSpan(true);
                    if (position == 0) {
                        holder.stickyTextview.setText("Header One");
                    } else {
                        holder.stickyTextview.setText("Header Two");
                    }
                    holder.stickyTextview.setLayoutParams(lp);

                    break;
                case VIEW_TYPE_REPLY:

                    if (position <= mHeaderOneCount) {
                        // 頭部一佔一個位置
                        mProductInfo = mProductInfos.get(position - 1);
                    } else {
                        // 頭部一和二佔兩個位置
                        mProductInfo = mProductInfos.get(position - 2);
                    }

                    // 獲取圖片size
                    mImageSize = getImageSize(MainActivity.this, mProductInfo);

                    // 圖片實際寬度設為螢幕一半,再等比例等到高度
                    final ViewGroup.LayoutParams lp1 = holder.mProductImage.getLayoutParams();
                    lp1.width = mHalfScreenWidth;
                    lp1.height = lp1.width * mImageSize.imageHeight / mImageSize.imageWidth;
                    holder.mProductImage.setLayoutParams(lp1);

                    // 載入圖片
                    Glide.with(MainActivity.this)
                            .load(mProductInfo)
                            .asBitmap()
                            .diskCacheStrategy(DiskCacheStrategy.ALL)
                            .placeholder(R.drawable.ic_loading)
                            .override(lp1.width, lp1.height)
                            .into(holder.mProductImage);
                    break;
            }
        }

        @Override
        public int getItemViewType(int position) {
            // 根據位置確定itemview的型別
            return position == 0 || position == mHeaderOneCount + 1 ? VIEW_TYPE_HEADER : VIEW_TYPE_REPLY;
        }

        @Override
        public int getItemCount() {
            // 處理mProductInfos還有兩個頭部
            return mProductInfos.size() + 2;
        }

    }

    private ImageSize getImageSize(Context context, int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(context.getResources(), resId, options);
        return new ImageSize(options.outWidth, options.outHeight);
    }

    class ImageSize {
        int imageHeight;
        int imageWidth;

        public ImageSize(int imageWidth, int imageHeight) {
            this.imageWidth = imageWidth;
            this.imageHeight = imageHeight;
        }
    }

}

總結:此種實現方式適用於少量頭部的時候,主要在RecyclerView.OnScrollListener的onScrolled方法獲取偏移量dy,並判斷RecycleView頭部二的y座標是否進入到幀佈局Header One的範圍裡面,然後呼叫scrollBy(0,dy)移動幀佈局Header One,並根據RecycleView頭部二離開介面時設定幀佈局Header Two可見偽造固定在頂部效果,難點在於滑動速度過快導致的dy過大造成的佈局位移過小或過大,經本人特殊處理過已無大問題,能有良好的效果。希望對看到的朋友有所啟發,本人覺得這種方法還是稍遜一籌,但是不失為一種解題思路吧。