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显示问题。

相关推荐
张风捷特烈13 小时前
FlutterUnit 3.3.0 | 全组件、全属性、鸿蒙支持来袭
android·前端·flutter
咖啡の猫14 小时前
Android开发简介
android
咖啡の猫15 小时前
Android开发-工程结构
android
_祝你今天愉快1 天前
Android Binder 驱动 - Media 服务启动流程
android
法迪1 天前
【Android 16】Android W 的冻结机制内核分析
android·功耗
阿华的代码王国1 天前
【Android】OkHttp发起GET请求 && POST请求
android·java·okhttp·网络连接
WAsbry1 天前
Android 屏幕适配系列开篇:核心概念、官方机制与主流方案
android·面试
liang_jy1 天前
抽象工厂模式
android·设计模式·面试