当前位置:首页 > 技术 > 正文内容

深入解析StickyGridHeadersGridView:粘性网格头部视图的工作原理

访客 技术 2026年7月25日 1

StickyGridHeaders 是一个专为 Android 设计的开源库,旨在为网格视图添加分组头部固定(粘性)的功能。其核心组件 StickyGridHeadersGridView 极大地增强了用户在浏览分组数据时的体验。本文将剖析 StickyGridHeadersGridView 的内部机制,阐述其如何实现滚动时保持头部元素可见的特性。

核心功能及适用场景

StickyGridHeadersGridView 基于 Android 的原生 GridView 进行扩展,解决了普通网格在滚动时无法固定分组标题的痛点。它允许开发者轻松创建类似通讯录或商品分类的界面,当用户向上滚动列表时,当前分组的头部会停留在屏幕顶部,直至下一个分组的头部完全取代它。这种交互模式在以下场景中尤为适用:

  • 按首字母排序的联系人列表。
  • 按分类展示的商品或文章列表。
  • 按日期分组的事件或日志记录。

关键类结构与重要属性

StickyGridHeadersGridView 继承自 GridView,并集成了滚动、点击等事件监听器。其类定义如下:

public class StickyGridHeadersGridView extends GridView implements OnScrollListener,
        OnItemClickListener, OnItemSelectedListener, OnItemLongClickListener
  

该类包含一系列用于控制粘性头部行为的关键属性:

  • mAreHeadersSticky:一个布尔值,用于启用或禁用粘性头部功能。
  • mStickiedHeader:指向当前被固定在顶部的头部视图对象。
  • mCurrentHeaderId:记录当前固定头部视图的唯一标识符。
  • mHeaderBottomPosition:存储粘性头部视图底部在屏幕上的纵坐标。
  • mOnHeaderClickListenermOnHeaderLongClickListener:分别用于响应头部视图的单击和长按事件。

粘性头部实现机制

头部视图的生成与替换

StickyGridHeadersGridView 通过其适配器(Adapter)来获取和管理头部视图。核心的头部视图生成逻辑在 getHeaderView 方法中完成。当需要更新头部时,会调用 swapStickiedHeader 方法,该方法负责先从视图层级中分离旧的头部视图,然后附加新的头部视图。

private void swapStickiedHeader(View newStickiedHeader) {
    detachHeader(mStickiedHeader); // 分离旧的头部
    attachHeader(newStickiedHeader); // 附加新的头部
    mStickiedHeader = newStickiedHeader; // 更新当前头部引用
}
  

滚动事件处理与头部状态更新

StickyGridHeadersGridView 实现了 OnScrollListener 接口,并在滚动发生时触发 onScroll 方法。该方法会调用 scrollChanged 方法来处理粘性头部的逻辑更新。

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    // ... (处理外部滚动监听器)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
        scrollChanged(firstVisibleItem); // 调用核心处理逻辑
    }
}
  

scrollChanged 方法是实现粘性头部的关键。它会根据当前可见的列表项(firstVisibleItem)来判断应该显示哪个分组的头部,并在必要时调用 swapStickiedHeader 来更换头部视图,然后重新测量和布局头部。

private void scrollChanged(int firstVisibleItem) {
    // ... (根据 firstVisibleItem 计算新的头部 ID 和位置)
    if (mCurrentHeaderId != newHeaderId) { // 如果需要更换头部
        View newHeader = mAdapter.getHeaderView(selectedHeaderPosition, mStickiedHeader, this);
        swapStickiedHeader(newHeader);
        measureHeader(); // 重新测量头部尺寸
        mCurrentHeaderId = newHeaderId; // 更新当前头部 ID
    }
    // ... (计算头部在屏幕上的具体位置和与下一个头部的距离)
}
  

自定义绘制实现粘性效果

为了将粘性头部正确地绘制在视图顶部,StickyGridHeadersGridView 重写了 dispatchDraw 方法。在这个方法中,它首先调用父类的 dispatchDraw 来绘制常规的网格内容,然后处理普通头部(非粘性)的绘制,最后绘制粘性头部。

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas); // 绘制网格内容
    // ... (绘制非粘性头部)

    if (drawStickiedHeader) { // 如果需要绘制粘性头部
        // ... (计算绘制区域 mClippingRect)
        canvas.save(); // 保存画布状态
        canvas.clipRect(mClippingRect); // 裁剪绘制区域
        canvas.translate(getPaddingLeft(), top); // 位移到正确位置
        mStickiedHeader.draw(canvas); // 绘制头部视图
        canvas.restore(); // 恢复画布状态
    }
}
  

通过 canvas.clipRectcanvas.translate,可以精确控制粘性头部在屏幕上的绘制区域和位置,确保其始终位于可视区域的顶部,且不被其他元素覆盖。

核心方法的深入分析

setAdapter(ListAdapter adapter)

该方法被重写以确保传入的适配器能够满足库的要求。它会根据传入适配器的类型,将其包装成内部所需的 StickyGridHeadersBaseAdapter 接口实现。

@Override
public void setAdapter(ListAdapter adapter) {
    StickyGridHeadersBaseAdapter baseAdapter;
    if (adapter instanceof StickyGridHeadersBaseAdapter) {
        baseAdapter = (StickyGridHeadersBaseAdapter) adapter;
    } else if (adapter instanceof StickyGridHeadersSimpleAdapter) {
        baseAdapter = new StickyGridHeadersSimpleAdapterWrapper((StickyGridHeadersSimpleAdapter) adapter);
    } else {
        baseAdapter = new StickyGridHeadersListAdapterWrapper(adapter);
    }
    this.mAdapter = new StickyGridHeadersBaseAdapterWrapper(getContext(), this, baseAdapter);
    super.setAdapter(this.mAdapter);
    // ... (其他初始化逻辑)
}
  

onTouchEvent(MotionEvent ev)

重写此方法用于处理用户在视图上的触摸操作,特别是与粘性头部相关的交互。它会检测用户是否点击在当前的粘性头部上,并触发相应的点击事件回调。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    final int action = ev.getActionMasked();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            float y = ev.getY();
            mMotionHeaderPosition = findMotionHeader(y); // 查找触摸点是否在头部区域
            // ... (处理触摸按下状态)
            break;
        case MotionEvent.ACTION_UP:
            if (mMotionHeaderPosition != NO_MATCHED_HEADER) {
                // 如果之前检测到点击在头部上,则触发点击事件
                performHeaderClick(header, headerViewPositionToId(mMotionHeaderPosition));
            }
            // ... (处理触摸抬起后的清理工作)
            break;
        // ... (处理其他触摸事件)
    }
    return super.onTouchEvent(ev);
}
  

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

在测量视图尺寸时,该方法会根据传入的参数计算网格的列数(如果设置为自动适应),并更新适配器中的列数设置。同时,它会调用 measureHeader() 来确保头部视图的尺寸被正确计算。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ... (根据 mNumColumns 计算实际显示的列数 mNumMeasuredColumns)
    if (mAdapter != null) {
        mAdapter.setNumColumns(mNumMeasuredColumns); // 更新适配器中的列数
    }
    measureHeader(); // 测量头部视图尺寸
    super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 调用父类测量
}
  

使用指南与注意事项

基本集成步骤

  1. 在 XML 布局文件中声明 StickyGridHeadersGridView
    <com.tonicartos.widget.stickygridheaders.StickyGridHeadersGridView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/grid_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:numColumns="auto_fit" />
          
  2. 创建一个实现了 StickyGridHeadersBaseAdapter 接口的自定义适配器。
  3. 在 Activity 或 Fragment 中找到 StickyGridHeadersGridView 实例,设置适配器并监听头部点击事件:
    StickyGridHeadersGridView gridView = findViewById(R.id.grid_view);
    MyStickyGridAdapter adapter = new MyStickyGridAdapter(...);
    gridView.setAdapter(adapter);
    gridView.setOnHeaderClickListener((parent, view, id) -> {
        // 处理头部被点击的逻辑
    });
          

重要提示

  • 项目维护状态:官方已明确表示该项目已被 SuperSLiM (一个 RecyclerView 的 LayoutManager) 取代,对于新项目,强烈建议使用 SuperSLiM。
  • 适配器要求:确保您实现的适配器能够正确提供头部视图的 ID 和视图本身。
  • 视图复用:和所有 Android 列表视图一样,注意头部视图的复用机制,以避免性能问题。
  • 功能开关:可以通过调用 setAreHeadersSticky(false) 来临时禁用粘性头部功能。

源码结构概览

StickyGridHeadersGridView 的源代码位于库的 library/src/com/tonicartos/widget/stickygridheaders/ 目录下。相关的适配器接口和包装类(如 StickyGridHeadersBaseAdapter.java, StickyGridHeadersBaseAdapterWrapper.java)也在此目录下。

示例代码可以在 example/ 目录下找到,可供开发者参考。

结论

StickyGridHeadersGridView 通过精巧地结合事件监听、视图管理和自定义绘制,成功实现了网格视图中分组头部的粘性悬浮效果。其核心在于滚动过程中对当前可见项的实时分析,以及对视图绘制顺序和区域的精确控制。尽管该库已被更新的解决方案所取代,但对其实现原理的深入研究,对于理解 Android 的视图绘制机制、事件分发以及自定义控件的开发模式,仍然具有重要的参考价值。

您可以克隆该项目的源代码进行学习:

git clone https://gitcode.com/gh_mirrors/st/StickyGridHeaders
  

通过研究 StickyGridHeadersGridView 的实现,开发者不仅能掌握如何使用该库(或类似库),更能从中学习到许多关于 Android UI 开发的宝贵技巧。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。