簡單粗暴實現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過大造成的佈局位移過小或過大,經本人特殊處理過已無大問題,能有良好的效果。希望對看到的朋友有所啟發,本人覺得這種方法還是稍遜一籌,但是不失為一種解題思路吧。