StickerHeaderRecyclerView的一种简易实现

使用Canvas裁剪技术实现RecyclerView与粘性Header重叠部分不显示的效果,Header无需设置固定背景色。

实现思路

  1. 自定义RecyclerView,重写dispatchDraw()方法进行画布裁剪
  2. 获取粘性Header的高度作为裁剪区域
  3. 使用clipRect()方法排除粘性Header占据的区域
  4. 确保粘性Header始终位于RecyclerView之上

完整代码实现

xml 复制代码
<!-- activity_main.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/dynamic_background">
    
    <!-- 自定义RecyclerView -->
    <com.example.StickyHeaderRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
    <!-- 粘性Header视图 -->
    <TextView
        android:id="@+id/stickyHeader"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:gravity="center_vertical"
        android:padding="16dp"
        android:text="A seguir"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:textStyle="bold" />
        
</FrameLayout>
java 复制代码
// StickyHeaderRecyclerView.java
public class StickyHeaderRecyclerView extends RecyclerView {
    private View mStickyHeader;
    private int mStickyHeaderHeight = -1;
    
    public StickyHeaderRecyclerView(Context context) {
        super(context);
        init();
    }
    
    public StickyHeaderRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    public StickyHeaderRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    
    private void init() {
        // 启用裁剪功能
        setClipToPadding(false);
    }
    
    public void setStickyHeader(View stickyHeader) {
        this.mStickyHeader = stickyHeader;
        if (mStickyHeader != null) {
            // 添加布局监听器获取Header高度
            mStickyHeader.addOnLayoutChangeListener(new OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, 
                                         int bottom, int oldLeft, int oldTop, 
                                         int oldRight, int oldBottom) {
                    if (mStickyHeaderHeight != mStickyHeader.getHeight()) {
                        mStickyHeaderHeight = mStickyHeader.getHeight();
                        requestLayout();
                    }
                }
            });
        }
    }
    
    @Override
    protected void dispatchDraw(Canvas canvas) {
        // 保存当前画布状态
        canvas.save();
        
        // 裁剪画布,排除粘性Header占据的区域
        if (mStickyHeader != null && mStickyHeaderHeight > 0) {
            // 创建裁剪区域 - 从顶部到Header高度的区域不绘制
            canvas.clipRect(0, mStickyHeaderHeight, getWidth(), getHeight());
        }
        
        // 绘制子视图
        super.dispatchDraw(canvas);
        
        // 恢复画布状态
        canvas.restore();
    }
}
java 复制代码
// MainActivity.java
public class MainActivity extends AppCompatActivity {
    private StickyHeaderRecyclerView recyclerView;
    private TextView stickyHeader;
    private MusicAdapter adapter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        recyclerView = findViewById(R.id.recyclerView);
        stickyHeader = findViewById(R.id.stickyHeader);
        
        // 设置布局管理器
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        
        // 创建并设置适配器
        List<MusicItem> musicList = createMusicList();
        adapter = new MusicAdapter(musicList);
        recyclerView.setAdapter(adapter);
        
        // 设置粘性Header
        recyclerView.setStickyHeader(stickyHeader);
        
        // 添加滚动监听更新粘性Header内容
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                updateStickyHeader();
            }
        });
    }
    
    private List<MusicItem> createMusicList() {
        List<MusicItem> list = new ArrayList<>();
        
        // 添加section headers和items
        list.add(new MusicItem("A seguir", true)); // Section header
        list.add(new MusicItem("3", false));
        
        list.add(new MusicItem("Próximas de:", true)); // Section header
        list.add(new MusicItem("5", false));
        list.add(new MusicItem("4", false));
        list.add(new MusicItem("5", false));
        list.add(new MusicItem("7", false));
        list.add(new MusicItem("8", false));
        list.add(new MusicItem("9", false));
        list.add(new MusicItem("10", false));
        
        list.add(new MusicItem("乱舞春秋", true)); // Section header
        list.add(new MusicItem("Jay Chou", false));
        
        list.add(new MusicItem("Apologize", true)); // Section header
        list.add(new MusicItem("OneRepublic", false));
        
        // 添加更多音乐项...
        
        return list;
    }
    
    private void updateStickyHeader() {
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
        
        if (firstVisiblePosition != -1) {
            // 找到当前section的header
            int currentSectionStart = firstVisiblePosition;
            while (currentSectionStart >= 0 && !adapter.getItem(currentSectionStart).isHeader) {
                currentSectionStart--;
            }
            
            if (currentSectionStart >= 0) {
                MusicItem sectionHeader = adapter.getItem(currentSectionStart);
                stickyHeader.setText(sectionHeader.title);
            }
        }
    }
    
    // 音乐项数据类
    class MusicItem {
        String title;
        boolean isHeader;
        
        MusicItem(String title, boolean isHeader) {
            this.title = title;
            this.isHeader = isHeader;
        }
    }
    
    // 音乐适配器
    class MusicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        private static final int TYPE_HEADER = 0;
        private static final int TYPE_ITEM = 1;
        
        private List<MusicItem> items;
        
        public MusicAdapter(List<MusicItem> items) {
            this.items = items;
        }
        
        @Override
        public int getItemViewType(int position) {
            return items.get(position).isHeader ? TYPE_HEADER : TYPE_ITEM;
        }
        
        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            
            if (viewType == TYPE_HEADER) {
                View view = inflater.inflate(R.layout.item_header, parent, false);
                return new HeaderViewHolder(view);
            } else {
                View view = inflater.inflate(R.layout.item_music, parent, false);
                return new ItemViewHolder(view);
            }
        }
        
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            MusicItem item = items.get(position);
            
            if (holder instanceof HeaderViewHolder) {
                ((HeaderViewHolder) holder).title.setText(item.title);
            } else if (holder instanceof ItemViewHolder) {
                ((ItemViewHolder) holder).title.setText(item.title);
            }
        }
        
        @Override
        public int getItemCount() {
            return items.size();
        }
        
        public MusicItem getItem(int position) {
            return items.get(position);
        }
        
        class HeaderViewHolder extends RecyclerView.ViewHolder {
            TextView title;
            
            HeaderViewHolder(View itemView) {
                super(itemView);
                title = itemView.findViewById(R.id.headerTitle);
            }
        }
        
        class ItemViewHolder extends RecyclerView.ViewHolder {
            TextView title;
            
            ItemViewHolder(View itemView) {
                super(itemView);
                title = itemView.findViewById(R.id.musicTitle);
            }
        }
    }
}

布局文件

xml 复制代码
<!-- item_header.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#80000000"
    android:gravity="center_vertical"
    android:padding="16dp"
    android:textColor="@android:color/white"
    android:textSize="18sp"
    android:textStyle="bold"
    android:id="@+id/headerTitle" />
xml 复制代码
<!-- item_music.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:padding="16dp"
    android:textColor="@android:color/white"
    android:textSize="16sp"
    android:id="@+id/musicTitle" />

实现说明

  1. 自定义RecyclerView :通过重写dispatchDraw()方法,使用clipRect()裁剪画布,排除粘性Header占据的区域
  2. 动态获取Header高度 :通过OnLayoutChangeListener监听粘性Header的高度变化
  3. 多布局适配器:使用两种视图类型区分Section Header和普通项目
  4. 滚动监听:根据滚动位置更新粘性Header的内容
  5. 透明背景:粘性Header使用透明背景,不会遮挡动态变化的布局背景

这种方法确保了无论布局背景如何变化,RecyclerView的内容都不会显示在粘性Header的区域,实现了真正的裁剪效果而非简单的视觉遮挡。

扩展建议

  1. 可以添加动画效果,使粘性Header的切换更加平滑
  2. 可以考虑使用ItemDecoration来实现Section Header,减少适配器的复杂性
  3. 可以添加点击事件处理,使粘性Header具有交互功能

这个解决方案应该能够完美解决您遇到的动态背景下的粘性Header显示问题。

相关推荐
阿巴斯甜20 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker20 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952721 小时前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android