Android采用Scroller实现底部二楼效果

需求

在移动应用开发中,有时我们希望实现一种特殊的布局效果,即“底部二楼”效果。这个效果类似于在列表底部拖动时出现额外的内容区域,用户可以继续向上拖动查看更多内容。这种效果可以用于展示广告、推荐内容或其他信息。

效果

实现后的效果如下:

  1. 当用户滑动到列表底部时,可以继续向上拖动,显示出隐藏的底部内容区域。
  2. 底部内容区域可以包含任意视图,如RecyclerView等。
  3. 滑动到一定阈值后,可以自动回弹到初始位置或完全展示底部内容。

实现思路

为了实现这一效果,我们可以自定义一个ScrollerLayout,并使用Scroller类来处理滑动和回弹动画。主要思路如下:

  1. 创建自定义的ScrollerLayout继承自LinearLayout
  2. ScrollerLayout中,遍历所有子视图,找到其中的RecyclerView,并为其添加滚动监听器。
  3. RecyclerView滚动到顶部时,允许整个布局继续向上滑动,展示底部内容区域。
  4. 使用Scroller类实现平滑滚动和回弹效果。

实现代码

ScrollerLayout.kt

package com.yxlh.androidxy.demo.ui.scroller

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.LinearLayout
import android.widget.Scroller
import androidx.recyclerview.widget.RecyclerView
import com.yxlh.androidxy.R

//github.com/yixiaolunhui/AndroidXY
class ScrollerLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr) {

    private val mScroller = Scroller(context)
    private var lastY = 0
    private var downY = 0
    private var contentHeight = 0
    private var isRecyclerViewAtTop = false
    private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop

    init {
        orientation = VERTICAL
        post {
            setupRecyclerViews()
        }
    }

    private fun setupRecyclerViews() {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child is RecyclerView) {
                child.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                        isRecyclerViewAtTop = !recyclerView.canScrollVertically(-1)
                    }
                })
            }
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        val bottomBar = getChildAt(0)
        contentHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child is RecyclerView) {
                contentHeight += child.measuredHeight
            }
        }
        bottomBar.layout(0, measuredHeight - bottomBar.measuredHeight, measuredWidth, measuredHeight)
        for (i in 1 until childCount) {
            val child = getChildAt(i)
            if (child is RecyclerView) {
                child.layout(0, measuredHeight, measuredWidth, measuredHeight + contentHeight)
            }
        }
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        val isTouchChildren = isTouchInsideChild(ev)
        Log.d("121212", "onInterceptTouchEvent isTouchChildren=$isTouchChildren")
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                downY = ev.y.toInt()
                lastY = downY
            }

            MotionEvent.ACTION_MOVE -> {
                val currentY = ev.y.toInt()
                val dy = currentY - downY

                if (isRecyclerViewAtTop && dy > touchSlop) {
                    lastY = currentY
                    return true
                }
            }
        }
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                if (!isTouchInsideChild(event)) return false
                if (!mScroller.isFinished) {
                    mScroller.abortAnimation()
                }
                lastY = event.y.toInt()
                return true
            }

            MotionEvent.ACTION_MOVE -> {
                if (!isTouchInsideChild(event)) return false
                val currentY = event.y.toInt()
                val dy = lastY - currentY
                val scrollY = scrollY + dy

                if (scrollY < 0) {
                    scrollTo(0, 0)
                } else if (scrollY > contentHeight) {
                    scrollTo(0, contentHeight)
                } else {
                    scrollBy(0, dy)
                }

                lastY = currentY
                return true
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                val threshold = contentHeight / 2
                if (scrollY > threshold) {
                    showNavigation()
                } else {
                    closeNavigation()
                }
                return true
            }
        }
        return false
    }

    private fun isTouchInsideChild(event: MotionEvent): Boolean {
        val x = event.rawX.toInt()
        val y = event.rawY.toInt()
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (isViewUnder(child, x, y)) {
                return true
            }
        }
        return false
    }

    private fun isViewUnder(view: View?, x: Int, y: Int): Boolean {
        if (view == null) return false
        val location = IntArray(2)
        view.getLocationOnScreen(location)
        val viewX = location[0]
        val viewY = location[1]
        return x >= viewX && x < viewX + view.width && y >= viewY && y < viewY + view.height
    }

    fun showNavigation() {
        val dy = contentHeight - scrollY
        mScroller.startScroll(scrollX, scrollY, 0, dy, 500)
        invalidate()
    }

    private fun closeNavigation() {
        val dy = -scrollY
        mScroller.startScroll(scrollX, scrollY, 0, dy, 500)
        invalidate()
    }

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidateOnAnimation()
        }
    }
}

ScrollerActivity.kt

package com.yxlh.androidxy.demo.ui.scroller

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.yxlh.androidxy.R
import com.yxlh.androidxy.databinding.ActivityScrollerBinding
import kotlin.random.Random

class ScrollerActivity : AppCompatActivity() {

    private var binding: ActivityScrollerBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityScrollerBinding.inflate(layoutInflater)
        setContentView(binding?.root)

        //内容布局
        binding?.content?.layoutManager = LinearLayoutManager(this)
        binding?.content?.adapter = ColorAdapter(false)

        //底部布局
        binding?.bottomContent?.layoutManager = LinearLayoutManager(this)
        binding?.bottomContent?.adapter = ColorAdapter(true)

        binding?.content?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
                    binding?.scrollerLayout?.showNavigation()
                }
            }
        })
    }
}

class ColorAdapter(private var isColor: Boolean) : RecyclerView.Adapter<ColorAdapter.ColorViewHolder>() {

    private val colors = List(100) { getRandomColor() }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ColorViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_color, parent, false)
        return ColorViewHolder(view, isColor)
    }

    override fun onBindViewHolder(holder: ColorViewHolder, position: Int) {
        holder.bind(colors[position], position)
    }

    override fun getItemCount(): Int = colors.size

    private fun getRandomColor(): Int {
        val random = Random.Default
        return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256))
    }

    class ColorViewHolder(itemView: View, private var isColor: Boolean) : RecyclerView.ViewHolder(itemView) {
        fun bind(color: Int, position: Int) {
            if (isColor) {
                itemView.setBackgroundColor(color)
            }
            itemView.findViewById<TextView>(R.id.color_tv).text = "$position"
        }


    }
}

结束

通过上述代码,我们成功实现了底部二楼效果。在用户滑动到RecyclerView底部时,可以继续向上拖动以显示底部的内容区域。这种效果可以增强用户体验,增加更多的内容展示方式。通过自定义布局和使用Scroller类,我们可以轻松实现这种复杂的滑动效果。
详情:github.com/yixiaolunhui/AndroidXY

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代理设计模式,框架AOP思想

文章目录 引言&#x1f92a;代理对象(Proxy)如何开发一个代理对象开发中的业务层代码冗余问题开发静态代理类动态代理 引言&#x1f92a; 代理 (proxy) &#xff0c;举个生活中常见的现象&#xff0c;在之前网路还未走进大众的时代里&#xff0c;如果我们想买一些东西&#xf…

Java——构造器(构造方法)和 this

一、什么是构造器 构造器&#xff08;Constructor&#xff09;是Java类的一种特殊方法&#xff0c;用于初始化对象的状态。构造器在创建对象时被调用&#xff0c;可以对对象的成员变量进行初始化。 我之前的文章《Java——类和对象-CSDN博客》中也提到了构造器。 二、构造器…

pc repair

pc repair 修理电脑&#xff0c;换配件

【猫狗分类】Pytorch VGG16 实现猫狗分类1-数据清洗+制作标签文件

Pytorch 猫狗分类 用Pytorch框架&#xff0c;实现分类问题&#xff0c;好像是学习了一些基础知识后的一个小项目阶段&#xff0c;通过这个分类问题&#xff0c;可以知道整个pytorch的工作流程是什么&#xff0c;会了一个分类&#xff0c;那就可以解决其他的分类问题&#xff0…

马斯克在2024年特斯拉股东大会上的年度发言

马斯克表示&#xff0c;“如果市盈率是20或25倍&#xff0c;那就意味着&#xff0c;光是Optimus就能带来20万亿美元的市值。而自动驾驶汽车的市值可能在5到10万亿美元之间。因此&#xff0c;特斯拉的市值达到当今市值最高公司的10倍&#xff0c;是可以想象的&#xff0c;也是有…

一个在C#中集成Python的例子

一个在C#中集成Python的例子。在C#中可以执行Python脚本&#xff0c;在Python中也可以调用C#宿主中的功能&#xff08;clr.AddReference(Business)&#xff09;。 文件说明 Debug为执行目录 Mgr.exe为执行文件 Py\init.py为python初始化脚本 Py\Lib.zip为python需要的模块&…

大数据实训项目(小麦种子)-02、实训项目整体功能介绍与演示

文章目录 前言界面及功能描述实现功能描述技术选型界面展示首页界面功能1&#xff1a;HDFS&#xff0c;选择文件上传文件详细步骤 功能2&#xff1a;MapReduce预处理数据功能3&#xff1a;Hbase存储小麦种子数据并查询前10条记录功能4&#xff1a;Hive分析原始csv文件数据并ech…

【GO-OpenCV】go-cv快速配置

最近对golang实现目标检测心血来潮&#xff0c;尝试在没有sudo权限的平台配置go-cv,有所发现&#xff0c;索性多个平台都做尝试 安装Go语言&#xff08;Golang&#xff09; 通过包管理器安装&#xff08;适用于Debian/Ubuntu&#xff09;(有点慢) 更新包列表&#xff1a; sud…

简单的基于小波变换的图像压缩(Python)

2023 沃尔夫数学奖得主&#xff0c;给了杜克大学的Ingrid Daubechies&#xff08;多贝西&#xff09;教授 以色列沃尔夫基金会理事会成员 Michael Lin 教授在周二宣布: “Ingrid Daubechies is awarded the Wolf Prize for her work in the creation and development of wavel…

搭建k8s集群报错unknown command “\u00a0“ for “kubeadm init“

搭建k8s报错unknown command “\u00a0” for “kubeadm init” 网上搜了一下&#xff0c;是因为复制过来的命令前面包含了空格&#xff0c;将复制的命令放到idea可以清楚看到几个命令前面有空格&#xff0c;删除掉就好了&#xff0c;记录一下

设计模式-享元模式Flyweight(结构型)

享元模式(Flyweight) 享元模式是一种结构型模式&#xff0c;它主要用于减少创建对象的数量&#xff0c;减少内存占用。通过重用现有对象的方式&#xff0c;如果未找到匹配对象则新建对象。线程池、数据库连接池、常量池等池化的思想就是享元模式的一种应用。 图解 角色 享元工…

【团队成长】2024-24周周报-第一次组会人员分工48期推文预告

大家好&#xff01;我们是IndustryOR 团队&#xff0c;致力于分享业界落地的算法技术。欢迎关注微信公众号/知乎/CSDN【运筹匠心】 。 记录人&#xff1a;张哲铭&#xff0c;算法专家&#xff0c;某互联网大厂 【团队成长/个人成长】系列的推文会以 【工作周报】 的方式记录Ind…

【机器学习】人工智能与气候变化:利用深度学习与机器学习算法预测和缓解环境影响

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 目录 &#x1f525;引言 1.1 背景介绍 1.2 人工智能与机器学习的崛起 1.3 本文内容概述 &#x1f528;气候变化的挑战 2.1 现今气候变化带来的影响和挑战 2.2 引发关注的气候变化趋势和数据 &#x1f916;人工智能…

使用SpringBoot对接Kafka

Kafka是什么&#xff0c;以及如何使用SpringBoot对接Kafka 一、Kafka与流处理 我们先来看看比较正式的介绍&#xff1a;Kafka是一种流处理平台&#xff0c;由LinkedIn公司创建&#xff0c;现在是Apache下的开源项目。Kafka通过发布/订阅机制实现消息的异步传输和处理。它具有高…

VMware Workstation安装及使用详细教程

如何安装VMware Workstation的详细教程 一、准备工作 1. 下载VMware Workstation&#xff1a; 访问VMware官方网站&#xff0c;找到VMware Workstation的下载页面。根据您的操作系统&#xff08;Windows或macOS&#xff09;选择相应的版本进行下载。确保您的计算机满足VMwar…

牛客小白月赛96 解题报告 | 珂学家

前言 题解 A. 最少胜利题数 签到 n1 len(set(input())) n2 len(set(input()))if n1 < n2:n1, n2 n2, n1print (-1 if n1 6 else n1 - n2 1)B. 最少操作次数 思路: 分类讨论 只有-1,0,1,2这四种结果 特判 01, 10 n int(input()) s input()# 枚举 from collectio…

vue之一键部署的shell脚本和它的点.bat文件、海螺AI、ChatGPT

MENU 前言vite.config.ts的配置deploy文件夹的其他内容remote.shpwd.txtdeploy.bat 前言 1、在src同级新建deploy.bat文件&#xff1b; 2、在src同级新建deploy文件夹&#xff0c;文件夹中新建pwd.txt和remote.sh文件&#xff1b; 3、配置好后&#xff0c;直接双击deploy.bat文…

Java_FileIO流

存储数据的方案 有些数据想长久保存起来&#xff0c;咋整&#xff1f; 文件时非常重要的存储方式&#xff0c;在计算机硬盘中。 即便断电&#xff0c;或者程序终止了&#xff0c;存储在硬盘文件中的数据也不会丢失。 File File 是Java.io.包下的类&#xff0c;File类对象&…

C++ string字符串的使用和简单模拟实现

目录 前言 1. string简介 2. string的使用和简单模拟实现 2.1 string类的定义 2.2 string(),~string()和c_str() 2.2 size&#xff0c;重载符号[ ]&#xff0c;begin和end函数 2.3 push_back&#xff0c;reserve&#xff0c;append&#xff0c;运算符重载 2.4 insert和…

DDPM公式推导(三)

2 Background 扩散模型【53】是一种以 p θ ( x 0 ) : ∫ p θ ( x 0 : T ) d x 1 : T p_\theta\left(\mathbf{x}_0\right):\int p_\theta\left(\mathbf{x}_{0: T}\right) d \mathbf{x}_{1: T} pθ​(x0​):∫pθ​(x0:T​)dx1:T​ 形式的潜在变量模型&#xff0c;其中 x 1…