Android使用FrameLayout+RecyclerView实现悬浮置顶封装功能

news2025/8/2 17:44:01

一、实际开发效果图

默认效果:

滚动后的效果:

二、效果实现方式

  • CoordinatorLayout + AppBarLayout + RecyclerView
    (适用于简单的悬浮View不超过一屏的情况,头部固定,数据简单)

  • FrameLayout + RecyclerView
    (适用于复杂的多条目布局,且悬浮条目位置受后台数据的影响而生改变)

建议:能用1的情况,尽量不用2

针对方式1的实现,自己去百度。下面主要讲的是方式2的实现

三、实现效果分析

实现思路:将要悬浮的条目创建一个新的,添加到FrameLayout里面,当RecyclerView滚动超过条目位置的时候显示出来。

四、创建悬浮View需要的的条件:
  1. 要知道条目的位置。

  1. 要知道条目的类型。

    /**
     * 接口定义
     */
    public interface IStick {
        /**
         * 悬浮的位置
         */
        int getStickPosition();

        /**
         * 悬浮的类型
         */
        int getStickViewType();

    }

五、FrameLayout + RecyclerView实现代码

/**
 * 悬浮布局封装
 */
public class StickFrameLayout extends FrameLayout {
    private RecyclerView mRecyclerView;
    // 悬浮根布局
    private FrameLayout mStickyLayout;
    // 要悬浮的布局
    private View mStickView;
    // 偏移量
    private int mOffset = 0;

    public StickFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StickFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 1. 加载布局完成之后
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 添加滚动监听
        addOnScrollListener();
        // 添加悬浮根布局
        addStickyLayout();
    }


    /**
     * 添加滚动监听
     */
    private void addOnScrollListener() {
        mRecyclerView = (RecyclerView) getChildAt(0);
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                StickFrameLayout.this.onScrolled();
            }
        });
    }

    /**
     * 滚动监听事件处理
     */
    private void onScrolled() {
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (adapter == null || layoutManager == null || adapter.getItemCount() <= 0) {
            return;
        }
        // 判断是不是实现了悬浮
        if (adapter instanceof IStick) {
            IStick stick = (IStick) adapter;
            int stickPosition = stick.getStickPosition();
            if (mStickView == null) {
                // 根据类型创建ViewHolder
                mStickyLayout.setTag(R.id.view_position, stickPosition);
                RecyclerView.ViewHolder viewHolder = adapter.onCreateViewHolder(mStickyLayout, stick.getStickViewType());
                // 根据位置绑定View
                adapter.onBindViewHolder(viewHolder, stickPosition);
                mStickView = viewHolder.itemView;
                mStickyLayout.addView(mStickView);
            }
            //这是是处理第一次打开时,吸顶布局已经添加到StickyLayout,但StickyLayout的高依然为0的情况。
            if (mStickyLayout.getChildCount() > 0 && mStickyLayout.getHeight() == 0) {
                mStickyLayout.requestLayout();
            }
            //设置StickyLayout显示或者隐藏。
            int firstVisibleItemPosition = findFirstVisibleItemPosition(mRecyclerView);
            View topView = layoutManager.findViewByPosition(stickPosition);

            // 1. 判断要不要偏移
            changeOffset(mOffset);
            // 2. 大于悬浮的位置都显示
            if (firstVisibleItemPosition >= stickPosition) {
                mStickyLayout.setVisibility(View.VISIBLE);
            } else if (topView != null) {
                // 3. 偏移大于悬浮到顶部的距离就显示
                boolean isShow = mOffset >= topView.getTop();
                if (isShow) {
                    mStickyLayout.setVisibility(View.VISIBLE);
                } else {
                    mStickyLayout.setVisibility(View.GONE);
                }
            } else {
                mStickyLayout.setVisibility(View.GONE);
            }
        }
    }

    /**
     * 手动设置显示
     *
     * @param visible
     */
    public void setStickyVisibility(int visible) {
        if (mStickyLayout != null) {
            mStickyLayout.setVisibility(visible);
        }
    }

    /**
     * 找第一个可见条目的位置
     */
    private int findFirstVisibleItemPosition(RecyclerView recyclerView) {
        int firstVisibleItem = -1;
        RecyclerView.LayoutManager layout = recyclerView.getLayoutManager();
        if (layout != null) {
            if (layout instanceof GridLayoutManager) {
                firstVisibleItem = ((GridLayoutManager) layout).findFirstVisibleItemPosition();
            } else if (layout instanceof LinearLayoutManager) {
                firstVisibleItem = ((LinearLayoutManager) layout).findFirstVisibleItemPosition();
            } else if (layout instanceof StaggeredGridLayoutManager) {
                int[] firstPositions = new int[((StaggeredGridLayoutManager) layout).getSpanCount()];
                ((StaggeredGridLayoutManager) layout).findFirstVisibleItemPositions(firstPositions);
                firstVisibleItem = getMin(firstPositions);
            }
        }
        return firstVisibleItem;
    }

    private int getMin(int[] arr) {
        int min = arr[0];
        for (int x = 1; x < arr.length; x++) {
            if (arr[x] < min)
                min = arr[x];
        }
        return min;
    }

    /**
     * 添加悬浮根布局
     */
    private void addStickyLayout() {
        mStickyLayout = new FrameLayout(getContext());
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT);
        mStickyLayout.setLayoutParams(lp);
        super.addView(mStickyLayout, lp);
    }

    /**
     * 设置偏移量
     */
    public void setStickOffset(int offset) {
        changeOffset(offset);
    }

    /**
     * 改变偏移量
     */
    private void changeOffset(int offset) {
        if (mOffset != offset) {
            if (mStickyLayout != null) {
                mOffset = offset;
                LayoutParams lp = (LayoutParams) mStickyLayout.getLayoutParams();
                lp.topMargin = offset;
                mStickyLayout.setLayoutParams(lp);
            }
        }
    }


    @Override
    protected int computeVerticalScrollOffset() {
        if (mRecyclerView != null) {
            try {
                Method method = View.class.getDeclaredMethod("computeVerticalScrollOffset");
                method.setAccessible(true);
                return (int) method.invoke(mRecyclerView);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return super.computeVerticalScrollOffset();
    }


    @Override
    protected int computeVerticalScrollRange() {
        if (mRecyclerView != null) {
            try {
                Method method = View.class.getDeclaredMethod("computeVerticalScrollRange");
                method.setAccessible(true);
                return (int) method.invoke(mRecyclerView);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return super.computeVerticalScrollRange();
    }

    @Override
    protected int computeVerticalScrollExtent() {
        if (mRecyclerView != null) {
            try {
                Method method = View.class.getDeclaredMethod("computeVerticalScrollExtent");
                method.setAccessible(true);
                return (int) method.invoke(mRecyclerView);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return super.computeVerticalScrollExtent();
    }

    @Override
    public void scrollBy(int x, int y) {
        if (mRecyclerView != null) {
            mRecyclerView.scrollBy(x, y);
        } else {
            super.scrollBy(x, y);
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        if (mRecyclerView != null) {
            mRecyclerView.scrollTo(x, y);
        } else {
            super.scrollTo(x, y);
        }
    }
}
  • 核心代码是滚动的处理,onScrolled()方法。

  • 用ViewHolder创建悬浮的View,给悬浮条目的Parent打个位置的Tag,就能知道要创建哪个位置的条目。

  • 提供一些常用的方法,如顶部位置的偏移。

adapter关键代码:

     @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 如果是多条目,viewType就是布局ID
        View view;
        if (mSupport != null) {
            Object tagPosition = parent.getTag(R.id.view_position);
            int layoutId = mSupport.getLayoutId(mData.get(mPosition));
            // 如果是滚动布局
            if (tagPosition != null) {
                int position = (int) tagPosition;
                layoutId = mSupport.getLayoutId(mData.get(position));
            }
            view = LayoutInflater.from(mContext).inflate(layoutId, parent, false);
        } else {
            view = LayoutInflater.from(mContext).inflate(mLayoutId, parent, false);
        }

        QuickViewHolder holder = new QuickViewHolder(view);
        return holder;
    }

注意:adapter关键代码是在我自己项目通用适配器添加的,你们根据自己项目的适配器添加。

源码地址:

https: //github.com/wenkency/CommAdapter

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/367706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

移动硬盘无法识别?恢复硬盘,问题已解决

移动硬盘和U盘比较&#xff0c;它的体积是比较大的&#xff0c;但是相应的存储位置就会大点。它作为可移动设备&#xff0c;对于存储大型的数据&#xff0c;还是非常方便的。 有时候用户会发现移动硬盘出现一些问题&#xff0c;移动硬盘与电脑连接后&#xff0c;在电脑桌面右下…

java自定义注解实现数据字典映射

一 &#xff1a;前言 在我们开发过程中&#xff0c;我们从前端页面接收的数据字典一般都是key&#xff08;大多数为数字&#xff09;&#xff0c;但我们在页面显示的时候&#xff0c;想用其value值。如果我们每使用一次就要去写一些重复的代码去查询&#xff0c;这样会使我们的…

SharePoint Online CDN简介

前言 可能很多人并不了解CDN这个概念&#xff0c;不过作为Web从业人员着实不该&#xff0c;CDN就是内容分发网络&#xff0c;说白了就是第三方帮你托管静态资源&#xff0c;你可以在全球任何位置快速访问到对应的节点的资源。 正文 我们提到的SharePoint CDN&#xff0c;其实更…

高精度加减乘除

高精度加法 对于给定的两个特别大的数我们用两个字符串来接收 s1和s2。 例如&#xff1a;对于两个数 56215455和95425453&#xff0c;即 s1 "56215455" &#xff0c; s2 "95425453"。 对于这两个数&#xff0c;分别用两个列表 a和b来接收(例如&#x…

LeetCode-78. 子集

题目来源 78. 子集 题目思路 其实子集也是一种组合问题&#xff0c;因为它的集合是无序的&#xff0c;子集{1,2} 和 子集{2,1}是一样的。 那么既然是无序&#xff0c;取过的元素不会重复取&#xff0c;写回溯算法的时候&#xff0c;for就要从startIndex开始&#xff0c;而在这…

华为OD机试题,用 Java 解【比赛评分】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

开源单点登录MaxKey和JeeSite 单点登录集成指南

1. JeeSite介绍 JeeSite 隶属于济南卓源软件有限公司&#xff0c;是一个 Java 快速开发平台&#xff0c; 基于经典技术组合&#xff08;Spring Boot、Shiro、MyBatis、BeetlBootstrap or TSVue3&#xff09;在线代码生成工具&#xff0c; 支持 Spring Cloud 架构&#xff0c;分…

MYSQL 索引失效的十个场景(一)

一、查询条件包含or&#xff0c;可能导致索引失效 新建一个student表&#xff0c;它有一个普通索引userId&#xff0c;结构如下&#xff1a; CREATE TABLE student (id varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,name varchar(50) COLLATE utf8mb4_unicode_ci DEFAUL…

移动端适配的理解和各种方案解析(详解)

前言&#xff1a;最近在弄移动端项目&#xff0c;记录一下移动端的应用方案。对各个方案的解决理解。 目录 1.什么是移动端适配 2.理解视口viewport 2.1PC端的视口 2.2移动端的视口 2.2.0 PC端的网页在移动端显示的问题 2.2.1 布局视口 2.2.2 视觉视口 (visual viewport) …

一看就懂的Semaphore源码解析,诸佬们快来看看吧

前言&#xff1a;一位朋友问到了我Semaphore类相关的知识&#xff0c;简单看了一下源码复习了一下&#xff0c;写下本篇文章做一个回顾。 希望能够加深自己的印象以及帮助到其他的小伙伴儿们&#x1f609;&#x1f609;。 如果文章有什么需要改进的地方还请大佬不吝赐教&#x…

华为OD机试题,用 Java 解【航天器】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

CSCode 配置一条龙 CPP/CC

下载 官⽹下载地址&#xff1a;Download Visual Studio Code - Mac, Linux, Windows 下载太慢&#xff0c;推荐⽂章&#xff1a;解决VsCode下载慢问题_wang13679201813的博客-CSDN博客_vscode下载慢 安装 无脑下一步 推荐插件 免配置&#xff1a; 1. Remote - SSH - 远程…

Exception has occurred: ModuleNotFoundErrorNo module named ‘urllib3‘【已解决】

问题描述 实际上只是想要测试一下torch是否安装成功&#xff0c;输出相应版本。谁知道就报错了。 Exception has occurred: ModuleNotFoundError No module named urllib3 解决方案 &#xff08;1&#xff09;使用pip或者conda卸载urllib3 pip uninstall urllib3conda unin…

离散无记忆与有记忆信源的序列熵

本专栏包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;information-theory】&#xff0c;需要的朋友们自取。或者公众号【AIShareLab】回复 信息论 也可获取。 文章目录离散无记忆信源的…

以高能低碳技术融入PC全生命周期,英特尔联合业界推出绿色商用电脑

双碳既是关系到地球上每个人的大话题&#xff0c;也是IT系统和产品降本增效的重要手段。 英特尔将高能低碳新理念融入从PC定义设计到回收循环的全生命周期 4 大关键环节&#xff0c;值得参考。 碳达峰、碳中和这个“双碳”的话题貌似与技术开发者个人距离很远。其实&#xff0c…

骨传导耳机是怎么传声的,选择骨传导耳机的时候需要注意什么?

​骨传导耳机之所以能够成为当下最火的耳机&#xff0c;骨传导技术将声音转化为震动感&#xff0c;通过骨头进行传播&#xff0c;不会堵塞耳朵&#xff0c;就不会影响到周围环境音。这种技术也让骨传导耳机比传统入耳式耳机更安全&#xff0c;无需入耳式设计&#xff0c;避免了…

小猫小狗玩数学-第14届蓝桥杯STEMA测评Scratch真题精选

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第102讲。 蓝桥杯选拔赛现已更名为STEMA&#xff0c;即STEM 能力测试&#xff0c;是蓝桥杯大赛组委会与美国普林斯顿多…

蓝牙运动耳机哪个好,比较好的运动蓝牙耳机

很多想选择蓝牙运动耳机的朋友都不知道应该如何选择&#xff0c;运动首先需要注意的就是耳机的防水能力以及耳机佩戴舒适度&#xff0c;在运动当中会排出大量的汗水&#xff0c;耳机防水等级做到越高&#xff0c;可以更好地保护耳机不受汗水浸湿&#xff0c;下面就分享五款适合…

智能电视“套娃式”收费背后的自我救赎

配图来自Canva可画 近年来随着智能化浪潮的迅速铺开&#xff0c;与以前只能看电视的智能电视相比&#xff0c;现在的智能电视还能打游戏、听音乐&#xff0c;用户还可在电视上自行下载、安装、卸载应用软件&#xff0c;功能大大丰富了。但随着智能电视功能的逐渐增多&#xff…

我们应该如何优雅的处理 React 中受控与非受控

引言 大家好&#xff0c;我是19组清风。有段时间没有和大家见面了&#xff0c;最近因为有一些比较重要的事情&#xff08;陪女朋友和换了新公司&#xff09;在忙碌所以销声匿迹了一小段时间&#xff0c; 后续会陆陆续续补充之前构建 & 编译系列中缺失的部分&#xff0c;提…