我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android实现滑动折叠Header全流程详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android实现滑动折叠Header全流程详解

前言

上一篇文章直接通过安卓自定义view的知识手撕了一个侧滑栏,做的还不错,很有成就感。这篇文章的控件没有上一篇的复杂,比较简单,通过一个内容滚动造成header折叠的控件学习一下滑动事件冲突问题、更改view节点以及CoordinatorLayout事件传递(超低仿),基本都是一个引子,希望学完这个控件,要继续省略学习下涉及的内容。

需求

这里就是希望做一个滚动通过内容能够折叠header的控件,在XML内写的控件能够有滚动效果,header暂时默认实现。

核心思想:

1、两部分,一个header和一个可以滚动的区域

2、header有两种状态,一个是完全展开状态,一个是折叠状态

3、在滚动区域向下滚动的时候,header会先滚动到折叠状态,header折叠后滚动区域才开始滚动

4、在滚动区域向上滚动的时候,滚动区域先滚动,滚动区域到顶了才开始展开header

5、低仿CoordinatorLayout,滚动区域效果通过自定义layoutParas向header传递

效果图

编写代码

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.forEach
import androidx.core.widget.NestedScrollView
import kotlin.math.abs

class ScrollingCollapseTopLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): ViewGroup(context, attributeSet, defStyleAttr) {
    //外部滑动距离
    private var mScrollHeight = 0f
    //上次纵坐标
    private var mLastY = 0f
    //当前控件宽高
    private var mHeight = 0
    private var mWidth = 0
    //两个部分
    private val header: Header = Header(context).apply {
        //设置header垂直方向,宽度铺满,高度自适应
        orientation = LinearLayout.VERTICAL
        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
    }
    //NestedScrollView只允许一个子view(和ScrollView一样),这里放一个垂直的LinearLayout
    private val scrollArea: NestedScrollView = NestedScrollView(context).apply {
        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        addView(LinearLayout(context).apply {
            setBackgroundColor(Color.LTGRAY)
            orientation = LinearLayout.VERTICAL
            layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        })
    }
    //XML里面的view
    private val xmlViews: ArrayList<View> = ArrayList()
    //获取XML内view结束,没执行onMeasure
    override fun onFinishInflate() {
        super.onFinishInflate()
        //在这里获得所有子view,拦截添加到scrollArea去
        if (xmlViews.size == 0) {
            forEach { view ->
                xmlViews.add(view)
            }
        }
        //更换view的节点
        removeAllViewsInLayout()
        addView(header)
        addView(scrollArea)
        //把当前控件全部view放到NestedScrollView内的LinearLayout内去
        (scrollArea.getChildAt(0) as ViewGroup).also { linear->
            for(view in xmlViews) {
                linear.addView(view)
            }
        }
    }
    //在onSizeChanged才能获得正确的宽高,会在onMeasure后得到,这里只是学一下
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mHeight = h
        mWidth = w
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //测量header
        header.onScroll(mScrollHeight.toInt())
        header.measure(widthMeasureSpec,
            MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),MeasureSpec.AT_MOST))
        //先measure一下获得实际高度,再减去滑动的距离,也可以把header.measuredHeight写成全局变量
        if (header.measuredHeight != 0) {
            val scrolledHeight = header.measuredHeight + mScrollHeight
            val headerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(scrolledHeight.toInt(),
                MeasureSpec.getMode(MeasureSpec.EXACTLY))
            //再次测量的目的是后面滚动部分要占满剩余高度
            header.measure(widthMeasureSpec, headerHeightMeasureSpec)
        }
        //测量滑动区域
        val leftHeight = MeasureSpec.getSize(heightMeasureSpec) - header.measuredHeight
        scrollArea.measure(widthMeasureSpec,
            MeasureSpec.makeMeasureSpec(leftHeight, MeasureSpec.EXACTLY))
        Log.e("TAG", "onMeasure: leftHeight=$leftHeight")
        Log.e("TAG", "onMeasure: scrollArea.height=${scrollArea.height}")
        Log.e("TAG", "onMeasure: scrollArea.measuredHeight=${scrollArea.measuredHeight}")
        //直接占满宽高
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
            MeasureSpec.getSize(heightMeasureSpec))
    }
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //简单布局下,上下两部分
        header.layout(l, t, r, t + header.measuredHeight)
        scrollArea.layout(l, t + header.measuredHeight, r,b)
    }
    //事件冲突使用外部拦截
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var isIntercepted = false
        ev?.let {
            when(ev.action) {
                //不拦截down事件
                MotionEvent.ACTION_DOWN -> mLastY = ev.y
                MotionEvent.ACTION_MOVE -> {
                    val dY = ev.y - mLastY
                    //如果折叠了,优先滚动折叠栏
                    val canScrollTop = scrollArea.canScrollVertically(-1)
                    val canScrollBottom = scrollArea.canScrollVertically(1)
                    //可以滚动
                    isIntercepted = if (canScrollTop || canScrollBottom) {
                        //手指向上移动时,没折叠前要拦截
                        val scrollUp = dY < 0 &&
                                mScrollHeight + dY > -header.collapsingArea.height.toFloat()
                        //手指向下移动时,没展开前且到顶了要拦截
                        val scrollDown = dY > 0 &&
                                mScrollHeight + dY < 0f &&
                                !canScrollTop
                        scrollUp || scrollDown
                    }else {
                        //不能滚动
                        true
                    }
                }
                //不拦截up事件
                //MotionEvent.ACTION_UP ->
            }
        }
        return isIntercepted
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            when(ev.action) {
                //MotionEvent.ACTION_DOWN ->
                MotionEvent.ACTION_MOVE -> {
                    //累加滑动值,请求重新布局
                    val dY = ev.y - mLastY
                    if (mScrollHeight + dY <= 0 &&
                        mScrollHeight + dY >= -header.collapsingArea.height) {
                            mScrollHeight += dY
                            requestLayout()
                    }
                    mLastY = ev.y
                }
                //MotionEvent.ACTION_UP ->
            }
        }
        return super.onTouchEvent(ev)
    }
    //这里就做一个简单的折叠header,
    @Suppress("MemberVisibilityCanBePrivate")
    inner class Header @JvmOverloads constructor(
        context: Context,
        attributeSet: AttributeSet? = null,
        defStyleAttr: Int = 0,
    ): LinearLayout(context, attributeSet, defStyleAttr){
        //两个区域
        val defaultArea: TextView
        val collapsingArea: TextView
        init {
            //添加两个header区域
            defaultArea = makeTextView(context, "Default area", 80)
            collapsingArea = makeTextView(context, "Collapsing area", 300)
            addView(defaultArea)
            addView(collapsingArea)
        }
        //低配Behavior.onNestedPreScroll,这里就处理下ScrollingHideTopLayout传过来的距离
        @SuppressLint("SetTextI18n")
        fun onScroll(scrollHeight: Int) {
            val expandHeight = collapsingArea.height + scrollHeight
            //这里就改一下背景色的透明度吧
            if (abs(expandHeight) <= collapsingArea.height) {
                val alpha = expandHeight.toFloat() / collapsingArea.height * 255
                defaultArea.text = "Default area:${alpha.toInt()}"
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    collapsingArea.setBackgroundColor(Color.argb(alpha.toInt(),88,88,88))
                }
            }
        }
        //创建TextView
        private fun makeTextView(context: Context, textStr: String, height: Int): TextView {
            //简单点height和textSize应该用dp和sp的,前面文章有
            return TextView(context).apply {
                layoutParams =
                    ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
                text = textStr
                gravity = Gravity.CENTER
                textSize = 13f
                setBackgroundColor(Color.GRAY)
            }
        }
    }
}

主要问题

NestedScrollView的使用

要想中间内容能够滚动,并且和当前控件造成滑动冲突,就只能引入新的滑动控件了,这里使用了NestedScrollView,和ScrollView类似。NestedScrollView只允许有一个子view,至于为什么可以看下源码,内容不多。我这是直接创建了一个NestedScrollView,并往里面加个一个垂直的LinearLayout,后面更改xml里面的view节点,往LinearLayout里面放。

修改xml内view的节点

上一篇文章里面,侧滑栏在xml里面的位置会影响绘制的层级,我是在onLayout里面通过移除再添加的方式做的,那如果要把view改到其他view里面去该怎么办。一开始我觉得很简单嘛,直接在onMeasure里面得到所有xml里面的view,再添加到其他viewgroup里面不就行了!想法很简单,试一下结果出我问题了。

第一个问题是view添加到其他viewgroup必须先移除,那我就直接就removeViewInLayout,结果就出了第二个问题OverStackError,大致就是一直measure,试了下是addView导致的,逻辑还是有问题。后面想想不应该在onMeasure里面实现的,应该在viewgroup加载xml里面子view时拦截处理的。

于是找了下api,发现viewgroup提供了一个onFinishInflate方法,会在加载xml里面view完成时调用,关键是它只会调用一次,onMeasure会调用多次,正好符合了我们的需求。修改节点就简单了,for循环一下就ok。

onSizeChanged函数

上面用到了onFinishInflate方法,找资料的时候看到自定义view里面常用重写的方法还有一个onSizeChanged函数。其实用的也多,主要是自定义view时用来获取控件宽高的,当控件的Size发生变化,如measure结束,onSizeChanged被调用,这时候才能拿到宽高,不然拿到的height和width就是0。

滑动事件冲突处理

我觉得滑动事件冲突的处理都应该根据实际情况去处理,知识的话可以去看看《安卓开发艺术探讨》里面的相关知识,主要解决办法就是内部拦截法和外部拦截法。我这就是简单的外部拦截法,本来想写复杂点,看看能不能多学点东西,结果根据需求,最后的代码很简单。

外部拦截法原理就是在onInterceptTouchEvent方法中,通过根据场景判断是内部滚动还是外部滚动,外部滚动就直接拦截,内部是否能滚动可以通过canScrollVertically/canScrollHorizontally方法判断。我这逻辑很简单,首先判断下内部是否能滚动,内部不能滚动就直接交给外部处理;然后又分两种情况,一个是手指向上移动时,没折叠前要拦截,另一个就是手指向下移动时,没展开前且到内部顶了要拦截。无论真么处理,还是得根据情景,

模仿CoordinatorLayout

本来还想模仿CoordinatorLayout做一个滑动状态传递的,这里滚动控件用的NestedScrollingChild,想让当前控件继承NestedScrollingParent处理滑动冲突,后面觉得还是简单点自己在onInterceptTouchEvent方法中处理能学点东西。当然读者有兴趣可借机学习一下NestedScrollingChild和NestedScrollingParent。

对于CoordinatorLayout,我也是学习了一下其中原理,私以为大致就是CoordinatorLayout的LayoutParams内有一个Behavior属性,Behavior作用就是构建两个子控件的关联关系(在CoordinatorLayout的onMeasure中),建立关联关系后,当一个view变化就会造成关联的view跟着变化(CoordinatorLayout控制),当然原理没这么简单,还是要去看源码。

本来我也想按这个逻辑模仿一下的,首先就是给当前控件的LayoutParams加一个Behavior属性,当滚动控件设置这个Behavior属性时,Header类在measure的时候就创建一个Behavior属性的私有变量,当前控件通过NestedScrollingChild接受滚动事件,并交给Header类的Behavior属性的私有变量去处理,一套逻辑下来,总感觉有脱裤子放屁的感觉,毕竟我这个控件就两个子控件。CoordinatorLayout的目的是协调多 View 之间的联动,重点在多,我这真没必要。

其实说到底,CoordinatorLayout就是一个协调功能,关联两个控件,比如我这就是滚动控件发出滚动消息,当前控件收到滚动消息,传递到Header里面处理,就这么简单,多了倒是可以按上面逻辑处理。

header折叠效果

这里的header的折叠效果是从onMeasure里面得到的!在测量时,根据滑动值,修改header的heightMeasureSpec,把header的高度设置为原有高度减去滑动高度,测量完header之后,把剩余的高度给到滑动区域,onLayout的时候将两个控件挨着就行。滑动的时候,请求重新layout,header和滚动区域每次都会获得不一样的高度,看起来就有了折叠效果。

到此这篇关于Android实现滑动折叠Header全流程详解的文章就介绍到这了,更多相关Android Header内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android实现滑动折叠Header全流程详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android实现滑动折叠Header全流程详解

这篇文章主要介绍了Android实现滑动折叠Header,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2022-11-13

Android编程实现滑动按钮功能详解

本文实例讲述了Android编程实现滑动按钮功能。分享给大家供大家参考,具体如下: 首先效果图:然后是分别建立三个文件,第一个是main.class,第二个是SlipButton.class,第三个是 onchangeListener.cl
2022-06-06

Android实现左右滑动效果的方法详解

本示例演示在Android中实现图片左右滑动效果。关于滑动效果,在Android中用得比较多,本示例实现的滑动效果是使用ViewFlipper来实现的,当然也可以使用其它的View来实现。接下来就让我们开始实现这种效果。为了方便大家理解,我
2022-06-06

Android编程之滑动按钮事件实例详解

本文实例讲述了Android滑动按钮事件。分享给大家供大家参考,具体如下: 今天纪录一下滑动按钮功能。。 首先效果图:然后是分别建立三个文件,第一个是main.class,第二个是SlipButton.class,第三个是 onchange
2022-06-06

解析Android中实现滑动翻页之ViewFlipper的使用详解

1)View切换的控件—ViewFlipper介绍 ViewFilpper类继承于ViewAnimator类。而ViewAnimator类继承于FrameLayout。 查看ViewAnimator类的源码可以看出此类的作用主要是为其中的V
2022-06-06

SpringCloudGateway自动装配实现流程详解

SpringCloudGateway旨在为微服务架构提供一种简单有效的、统一的API路由管理方式。SpringCloudGateway作为SpringCloud生态系中的网关,它不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全、监控/埋点和限流等
2022-11-13

JavaScript使用canvas实现flappybird全流程详解

这篇文章主要介绍了JavaScript使用canvas实现flappybird流程,canvas是HTML5提供的一种新标签,它可以支持JavaScript在上面绘画,控制每一个像素,它经常被用来制作小游戏,接下来我将用它来模仿制作一款叫flappybird的小游戏
2023-03-03

Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解

这篇文章主要为大家介绍了Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-29

Redis实现延迟队列的全流程详解

Redisson是Redis服务器上的分布式可伸缩Java数据结构,这篇文中主要为大家介绍了Redisson实现的优雅的延迟队列的方法,需要的可以参考一下
2023-03-14

详解Android中实现ListView左右滑动删除条目的方法

使用Scroller实现绚丽的ListView左右滑动删除Item效果 这里来给大家带来使用Scroller的小例子,同时也能用来帮助初步解除的读者更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用我们就能实现很多滑动的效
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录