先來(lái)實(shí)現(xiàn)效果鎮(zhèn)樓
image
實(shí)現(xiàn)代碼
Activity
package com.him.stickyheader;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
public List<String> datas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.addItemDecoration(new MyItemDecoration(new MyItemDecoration.GroupInfoCallback() {
// 根據(jù)全局?jǐn)?shù)據(jù)的位置position查詢所屬分組的信息GroupInfo
@Override
public GroupInfo getGroupInfo(int position) {
// 測(cè)試數(shù)據(jù),暫時(shí)10條數(shù)據(jù)一組
int size = 10;
GroupInfo info = new GroupInfo();
info.groupId = position / size;
info.title = info.groupId + "";
info.position = position % size;
info.groupSize = size;
return info;
}
}));
initDatas();
mRecyclerView.setAdapter(new MyAdapter(datas));
}
// 初始化測(cè)試數(shù)據(jù)
private void initDatas() {
datas = new ArrayList<>();
for (int i = 0; i < 100;i++) {
datas.add("test " + i);
}
}
}
Adapter
package com.him.stickyheader;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
public List<String> datas;
public MyAdapter(List<String> datas) {
this.datas = datas;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder viewHolder, int i) {
viewHolder.text.setText(datas.get(i));
}
@Override
public int getItemCount() {
return datas == null ? 0 :datas.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
TextView text;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
text = itemView.findViewById(R.id.text);
}
}
}
ItemDecoration,這是最重要的代碼
package com.him.stickyheader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import java.util.function.IntBinaryOperator;
public class MyItemDecoration extends RecyclerView.ItemDecoration {
// 普通分割線的高度
private int dividerHeight = 1;
// 分組header高度
private int headerHeight = 65;
// 根據(jù)position獲取分組信息的回調(diào)
private GroupInfoCallback callback;
// 畫(huà)筆
private Paint paint;
public MyItemDecoration(GroupInfoCallback callback) {
this.callback = callback;
paint = new Paint();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int itemPosition = parent.getChildAdapterPosition(view);
GroupInfo info = callback.getGroupInfo(itemPosition);
if (info.isFirstItem()) {
// 分組第一個(gè)item撐開(kāi)一個(gè)header的高度
outRect.top = headerHeight;
} else {
// 其他item撐開(kāi)一個(gè)普通divider的高度
outRect.top = dividerHeight;
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
// header左右邊的坐標(biāo),注意考慮parent的左右padding
float left = parent.getPaddingLeft();
float right = parent.getWidth() - parent.getPaddingRight();
// 循環(huán)RecyclerView中當(dāng)前可見(jiàn)的View
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
// 根據(jù)當(dāng)前view獲取在整個(gè)列表中的位置
int itemPosition = parent.getChildAdapterPosition(child);
// 根據(jù)position獲取所屬分組的信息
GroupInfo info = callback.getGroupInfo(itemPosition);
// 重點(diǎn)來(lái)了,敲黑板
// 注意這里的i表示在RecyclerView當(dāng)前可見(jiàn)View中的位置(第幾個(gè))
// i == 0表示當(dāng)前可見(jiàn)的第一個(gè)View
// 這里繪制分組標(biāo)題懸停的效果
if (i == 0) {
// 分組標(biāo)題停在最頂部,就是parent的頂部,然后再考慮頂部的padding
float top = parent.getPaddingTop();
// top加分組高度就是底部坐標(biāo)
float bottom = top + headerHeight;
// 這里是第二個(gè)重點(diǎn)
// 如果沒(méi)這段代碼,就無(wú)法實(shí)現(xiàn)下面的分組標(biāo)題把當(dāng)前懸停的分組標(biāo)題往上推出頁(yè)面的效果
// 分組的最后一個(gè)item且該item的底部坐標(biāo)小于懸停頂部的header的底部坐標(biāo)的情況下,需執(zhí)行以下代碼,重新計(jì)算header的位置
if (info.isLastItem() && bottom > child.getBottom()) {
bottom = child.getBottom();
top = bottom - headerHeight;
}
drawHeader(left, top, right, bottom, c, info);
} else if (info.isFirstItem()) { // 如果不是可見(jiàn)的第一個(gè)view但是是分組的第一個(gè)item,則繪制分組標(biāo)題
// item的頂部再往上一個(gè)header的高度
float top = child.getTop() - headerHeight;
// 頂部坐標(biāo)加上header高度就是底部坐標(biāo)
float bottom = top + headerHeight;
drawHeader(left, top, right, bottom, c, info);
}
}
}
// 根據(jù)計(jì)算出的header位置繪制header及header中的title
private void drawHeader(float left, float top, float right, float bottom, Canvas c, GroupInfo info) {
// 設(shè)置header背景色,然后繪制
paint.setColor(Color.GREEN);
c.drawRect(left, top, right, bottom, paint);
// 設(shè)置title文字大小,然后計(jì)算文字高度,用于接下來(lái)計(jì)算文字繪制的位置
paint.setTextSize(32);
paint.setColor(Color.BLUE);
Rect rect = new Rect();
// 計(jì)算要繪制的文字的寬高
paint.getTextBounds(info.title, 0, info.title.length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
// 文字距左邊的距離
float textX = left + 40;
// 計(jì)算baseline,中文情況下為文字的底部
float textY = top + (headerHeight + textHeight)/2;
c.drawText(info.title, textX, textY, paint);
}
// 根據(jù)所在列表的位置計(jì)算所屬分組的信息
public interface GroupInfoCallback {
GroupInfo getGroupInfo(int position);
}
}
分組信息的Model類
package com.him.stickyheader;
public class GroupInfo {
// 分組的id
public int groupId;
// 分組顯示的title
public String title;
// 在分組中的位置
public int position;
// 分組的大小
public int groupSize;
// 是否是分組的第一個(gè)
public boolean isFirstItem() {
return position == 0;
}
// 是否是分組的最后一個(gè)
public boolean isLastItem() {
return position == (groupSize - 1);
}
}
Activity布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:background="#b5b8de"> <!--設(shè)置和item不同的背景色,當(dāng)做divider的顏色-->
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
item布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</android.support.constraint.ConstraintLayout>
獲取源代碼
掃描以下二維碼關(guān)注我的微信公眾號(hào)野猿新一,發(fā)送“懸停頭部”獲取源代碼的下載方式。
image