Android自定义view实现带header和footer的Layout

目录
  • 前言
  • 需求
  • 编写代码
  • 主要问题
    • 父容器给当前控件的宽高
    • 对子控件进行测量
    • 子控件的摆放
    • 控件总高度和控件高度
    • header和footer的初始化显示与隐藏
    • header和footer的动态显示与隐藏
    • 使用
    • 中间为TextView时不触发ACTION_MOVE事件
    • 结语

前言

上两篇文章对安卓自定义view的事件分发做了一些应用:Android自定义view实现左滑删除的RecyclerView详解、Android自定义view实现列表内左滑删除Item,但是对于自定义view来讲,并不仅仅是事件分发这么简单,还有一个很重要的内容就是view的绘制流程。接下来我这通过带header和footer的Layout,来学习一下ViewGroup的自定义流程,并对其中的MeasureSpec、onMeasure以及onLayout加深理解。

需求

这里就是一个有header和footer的滚动控件,可以在XML中当Layout使用,核心思想如下:

1、由header、XML内容、footer三部分组成

2、滚动中间控件时,上面有内容时header不显示,下面有内容时footer不显示

3、滑动到header和footer最大值时不能滑动,释放的时候需要回弹

4、完全显示时隐藏footer

编写代码

编写代码这部分还真让我头疼了一会,主要就是MeasureSpec的运用,如何让控件能够超出给定的高度,如何获得实际高度和控件高度,真是纸上得来终觉浅,绝知此事要躬行,看书那么多遍,实际叫自己写起来真的费劲,不过最终写完,才真的敢说自己对measure和layout有一定了解了。

先看代码,再讲问题吧!

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Scroller
import android.widget.TextView
import androidx.core.view.forEach
import kotlin.math.min
/**
 * 有header和footer的滚动控件
 * 核心思想:
 * 1、由header、container、footer三部分组成
 * 2、滚动中间控件时,上面有内容时header不显示,下面有内容时footer不显示
 * 3、滑动到header和footer最大值时不能滑动,释放的时候需要回弹
 * 4、完全显示时隐藏footer
 */
@SuppressLint("SetTextI18n", "ViewConstructor")
class HeaderFooterView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0,
    var header: View? = null,
    var footer: View? = null
): ViewGroup(context, attributeSet, defStyleAttr){
    var onReachHeadListener: OnReachHeadListener? = null
    var onReachFootListener: OnReachFootListener? = null
    //上次事件的横坐标
    private var mLastY = 0f
    //总高度
    private var totalHeight = 0
    //是否全部显示
    private var isAllDisplay = false
    //流畅滑动
    private var mScroller = Scroller(context)
    init {
        //设置默认的Header、Footer,这里是从构造来的,如果外部设置需要另外处理
        header = header ?: makeTextView(context, "Header")
        footer = footer ?: makeTextView(context, "Footer")
        //添加对应控件
        addView(header, 0)
        //这里还没有加入XML中的控件
        //Log.e("TAG", "init: childCount=$childCount", )
        addView(footer, 1)
    }
    //创建默认的Header\Footer
    private fun makeTextView(context: Context, textStr: String): TextView {
        return TextView(context).apply {
            layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp2px(context, 30f))
            text = textStr
            gravity = Gravity.CENTER
            textSize = sp2px(context, 13f).toFloat()
            setBackgroundColor(Color.GRAY)
            //不设置isClickable的话,点击该TextView会导致mFirstTouchTarget为null,
            //致使onInterceptTouchEvent不会被调用,只有ACTION_DOWN能被收到,其他事件都没有
            //因为事件序列中ACTION_DOWN没有被消耗(返回true),整个事件序列被丢弃了
            //如果XML内是TextView也会造成同样情况,
            isFocusable = true
            isClickable = true
        }
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //父容器给当前控件的宽高,默认值尽量设大一点
        val width = getSizeFromMeasureSpec(1080, widthMeasureSpec)
        val height = getSizeFromMeasureSpec(2160, heightMeasureSpec)
        //对子控件进行测量
        forEach { child ->
            //宽度给定最大值
            val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST)
            //高度不限定
            val childHeightMeasureSpec
                = MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)
            //进行测量,不测量的话measuredWidth和measuredHeight会为0
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
            //Log.e("TAG", "onMeasure: child.measuredWidth=${child.measuredWidth}")
            //Log.e("TAG", "onLayout: child.measuredHeight=${child.measuredHeight}")
        }
        //设置测量高度为父容器最大宽高
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
            MeasureSpec.getSize(heightMeasureSpec))
    }
    private fun getSizeFromMeasureSpec(defaultSize: Int, measureSpec: Int): Int {
        //获取MeasureSpec内模式和尺寸
        val mod = MeasureSpec.getMode(measureSpec)
        val size = MeasureSpec.getSize(measureSpec)
        return when (mod) {
            MeasureSpec.EXACTLY -> size
            MeasureSpec.AT_MOST -> min(defaultSize, size)
            else -> defaultSize //MeasureSpec.UNSPECIFIED
        }
    }
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        var curHeight = 0
        //Log.e("TAG", "onLayout: childCount=${childCount}")
        forEach { child ->
            //footer最后处理
            if (indexOfChild(child) != 1) {
                //Log.e("TAG", "onLayout: child.measuredHeight=${child.measuredHeight}")
                child.layout(left, top + curHeight, right,
                    top + curHeight + child.measuredHeight)
                curHeight += child.measuredHeight
            }
        }
        //处理footer
        val footer = getChildAt(1)
        //完全显示内容时不加载footer,header不算入内容
        if (measuredHeight < curHeight - header!!.height) {
            //设置全部显示flag
            isAllDisplay = false
            footer.layout(left, top + curHeight, right,top + curHeight + footer.measuredHeight)
            curHeight += footer.measuredHeight
        }
        //布局完成,滚动一段距离,隐藏header
        scrollBy(0, header!!.height)
        //设置总高度
        totalHeight = curHeight
    }
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        //Log.e("TAG", "onInterceptTouchEvent: ev=$ev")
        ev?.let {
            when(ev.action) {
                MotionEvent.ACTION_DOWN -> mLastY = ev.y
                MotionEvent.ACTION_MOVE -> return true
            }
        }
        return super.onInterceptTouchEvent(ev)
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        //Log.e("TAG", "onTouchEvent: height=$height, measuredHeight=$measuredHeight")
        ev?.let {
            when(ev.action) {
                MotionEvent.ACTION_MOVE -> moveView(ev)
                MotionEvent.ACTION_UP -> stopMove()
            }
        }
        return super.onTouchEvent(ev)
    }
    private fun moveView(e: MotionEvent) {
        //Log.e("TAG", "moveView: height=$height, measuredHeight=$measuredHeight")
        val dy = mLastY - e.y
        //更新点击的纵坐标
        mLastY = e.y
        //纵坐标的可滑动范围,0 到 隐藏部分高度,全部显示内容时是header高度
        val scrollMax = if (isAllDisplay) {
            header!!.height
        }else {
            totalHeight - height
        }
        //限定滚动范围
        if ((scrollY + dy) <= scrollMax &&  (scrollY + dy) >= 0) {
            //触发移动
            scrollBy(0, dy.toInt())
        }
    }
    private fun stopMove() {
        //Log.e("TAG", "stopMove: height=$height, measuredHeight=$measuredHeight")
        //如果滑动到显示了header,就通过动画隐藏header,并触发到达顶部回调
        if (scrollY < header!!.height) {
            mScroller.startScroll(0, scrollY, 0, header!!.height - scrollY)
            onReachHeadListener?.onReachHead()
        }else if(!isAllDisplay && scrollY > (totalHeight - height - footer!!.height)) {
            //如果滑动到显示了footer,就通过动画隐藏footer,并触发到达底部回调
            mScroller.startScroll(0, scrollY,0,
                 (totalHeight - height- footer!!.height) - scrollY)
            onReachFootListener?.onReachFoot()
        }
        invalidate()
    }
    //流畅地滑动
    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()
        }
    }
    //单位转换
    @Suppress("SameParameterValue")
    private fun dp2px(context: Context, dpVal: Float): Int {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources
                .displayMetrics
        ).toInt()
    }
    @Suppress("SameParameterValue")
    private fun sp2px(context: Context, spVal: Float): Int {
        val fontScale = context.resources.displayMetrics.scaledDensity
        return (spVal * fontScale + 0.5f).toInt()
    }
    interface OnReachHeadListener{
        fun onReachHead()
    }
    interface OnReachFootListener{
        fun onReachFoot()
    }
}

主要问题

父容器给当前控件的宽高

这里就是MeasureSpec的理解了,onMeasure中给了两个参数:widthMeasureSpec和heightMeasureSpec,里面包含了父控件给当前控件的宽高,根据模式的不同可以取出给的数值,根据需要设定自身的宽高,需要注意setMeasuredDimension函数设定后,measuredWidth和measuredHeight才有值。

对子控件进行测量

这里很容易忽略的是,当继承viewgroup的时候,我们要手动去调用child的measure函数,去测量child的宽高。一开始我也没注意到,当我继承LineaLayout的时候是没问题的,后面改成viewgroup后就出问题了,看了下LineaLayout的源码,里面的onMeasure函数中实现了对child的测量。

对子控件的测量时,MeasureSpec又有用了,比如说我们希望XML中的内容不限高度或者高度很大,这时候MeasureSpec.UNSPECIFIED就有用了,而宽度我们希望最大就是控件宽度,就可以给个MeasureSpec.AT_MOST,注意我们给子控件的MeasureSpec也是有两部分的,需要通过makeMeasureSpec创建。

子控件的摆放

由于我们的footer和header是在构造里面创建并添加到控件中的,这时候XML内的view还没加进来,所以需要注意下footer实际在控件中是第二个,摆放的时候根据index要特殊处理一下。

其他控件我们根据左上右下的顺序摆放就行了,注意onMeasure总对子控件measure了才有宽高。

控件总高度和控件高度

因为需求,我们的控件要求是中间可以滚动,所以在onMeasure总,我们用到了MeasureSpec.UNSPECIFIED,这时候控件的高度和实际总高度就不一致了。这里我们需要在onLayout中累加到来,实际摆放控件的时候也要用到这个高度,顺势而为了。

header和footer的初始化显示与隐藏

这里希望在开始的时候隐藏header,所以需要在onLayout完了的时候,向上滚动控件,高度为header的高度。

根据需求,完全显示内容的时候,我们不希望显示footer,这里也要在onLayout里面实现,根据XML内容的高度和控件高度一比较就知道需不需要layout footer了。

header和footer的动态显示与隐藏

这里就和前面两篇文章类似了,就是在纵坐标上滚动控件,限定滚动范围,在ACTION_UP事件时判定滚动后的状态,动态去显示和隐藏header和footer,思路很明确,逻辑可能复杂一点。

使用

这里简单说下使用吧,就是作为Layout,中间可以放控件,中间控件可以指定特别大的高度,也可以wrap_content,但是内容很高。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.silencefly96.module_common.view.HeaderFooterView
        android:id="@+id/hhView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/teal_700"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <TextView
            android:text="@string/test_string"
            android:focusable="true"
            android:clickable="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
             />
    </com.silencefly96.module_common.view.HeaderFooterView>
</androidx.constraintlayout.widget.ConstraintLayout>

这里的test_string特别长,滚动起来header和footer可以拉出来,释放会缩回去。还可以在代码中获得控件增加触底和触顶的回调。

中间为TextView时不触发ACTION_MOVE事件

上面XML布局中,如果不加clickable=true的话,控件中只会收到一个ACTION_DOWN事件,然后就没有然后了,即使是dispatchTouchEvent中也没有事件了。经查,原来不设置isClickable的话,点击该TextView会导致mFirstTouchTarget为null,致使onInterceptTouchEvent不会被调用,因为事件序列中ACTION_DOWN没有被消耗(未返回true),整个事件序列被丢弃了。

结语

实际上这个控件写的并不是很好,拿去用的话还是不太行的,但是用来学习的话还是能理解很多东西。

到此这篇关于Android自定义view实现带header和footer的Layout的文章就介绍到这了,更多相关Android带header和footer的Layout内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android自定义view实现有header和footer作为layout使用的滚动控件

    目录 前言 需求 编写代码 主要问题 前言 上两篇文章对安卓自定义view的事件分发做了一些应用,但是对于自定义view来讲,并不仅仅是事件分发这么简单,还有一个很重要的内容就是view的绘制流程.接下来我这通过带header和footer的Layout,来学习一下ViewGroup的自定义流程,并对其中的MeasureSpec.onMeasure以及onLayout加深理解. 需求 这里就是一个有header和footer的滚动控件,可以在XML中当Layout使用,核心思想如下: 1.由he

  • Android自定义View实现带数字的进度条实例代码

    第一步.效果展示 图1.蓝色的进度条 图2.红色的进度条 图3.多条颜色不同的进度条 图4.多条颜色不同的进度条 第二步.自定义ProgressBar实现带数字的进度条 0.项目结构 如上图所示:library项目为自定义的带数字的进度条NumberProgressBar的具体实现,demo项目为示例项目以工程依赖的方式引用library项目,然后使用自定义的带数字的进度条NumberProgressBar来做展示 如上图所示:自定义的带数字的进度条的library项目的结构图 如上图所示:de

  • Android自定义View实现带4圆角或者2圆角的效果

    1 问题 实现任意view经过自定义带4圆角或者2圆角的效果 2 原理 1) 实现view 4圆角 我们只需要把左边的图嵌入到右边里面去,最终显示左边的图就行. 2) 实现view上2圆角 我们只需要把左边的图嵌入到右边里面去,最终显示左边的图就行. 安卓源码里面有这样的类 package android.graphics; /** * <p>Specialized implementation of {@link Paint}'s * {@link Paint#setXfermode(Xfe

  • Android自定义View实现圆环带数字百分比进度条

    分享一个自己制作的Android自定义View.是一个圆环形状的反映真实进度的进度条,百分比的进度文字跟随已完成进度的圆弧转动.以下是效果图: 这个自定义View可以根据需要设定圆环的宽度和百分比文字的大小. 先说一下思路:这个View一共分为三部分:第一部分也就是灰色的圆环部分,代表未完成的进度:第二部分是蓝色的圆弧部分,代表已经完成的进度:第三部分是红色的百分比的数字百分比文本,显示当前确切的完成进度. 下面是View的编写思路: ①:定义三个画笔,分别画灰色圆环,蓝色圆弧,红色文字: ②:

  • Android自定义view实现侧滑栏详解

    目录 前言 需求 效果图 编写代码 主要问题 前言 上一篇文章学了下自定义View的onDraw函数及自定义属性,做出来的滚动选择控件还算不错,就是逻辑复杂了一些.这篇文章打算利用自定义view的知识,直接手撕一个安卓侧滑栏,涉及到自定义LayoutParams.带padding和margin的measure和layout.利用requestLayout实现动画效果等,有一定难度,但能重新学到很多知识! 需求 这里类似旧版QQ(我特别喜欢之前的侧滑栏),有两层页面,滑动不是最左侧才触发的,而是从

  • Android自定义view实现滚动选择控件详解

    目录 前言 需求 编写代码 主要问题 前言 上篇文章通过一个有header和footer的滚动控件(Viewgroup)学了下MeasureSpec.onMeasure以及onLayout,接下来就用一个滚动选择的控件(View)来学一下onDraw的使用,并且了解下在XML自定义控件参数. 需求 这里就是一个滚动选择文字的控件,还是挺常见的,之前用别人的,现在选择手撕一个,核心思想如下: 1.有三层不同大小及透明度的选项,选中项放在中间 2.接受一个列表的数据,静态时显示三个值,滚动时显示四个

  • Android自定义view实现阻尼效果的加载动画

    效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动.衰减振动.[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来.这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运

  • Android自定义View实现验证码

    本文章是基于鸿洋的Android 自定义View (一)的一些扩展,以及对Android自定义View构造函数详解里面内容的一些转载. 首先我们定义一个declare-styleable标签declare-styleable标签的作用是给自定义控件添加自定义属性用的例如这样 (我们定义了文字的颜色,大小,长度,跟背景的颜色) <declare-styleable name="CustomTitleView"> <attr name="titleColor&q

  • Android自定义VIew实现卫星菜单效果浅析

     一 概述: 最近一直致力于Android自定义VIew的学习,主要在看<android群英传>,还有CSDN博客鸿洋大神和wing大神的一些文章,写的很详细,自己心血来潮,学着写了个实现了类似卫星效果的一个自定义的View,分享到博客上,望各位指点一二.写的比较粗糙,见谅.(因为是在Linux系统下写的,效果图我直接用手机拍的,难看,大家讲究下就看个效果,勿喷). 先来看个效果图,有点不忍直视: 自定义VIew准备: (1)创建继承自View的类; (2)重写构造函数; (3)定义属性. (

  • Android自定义View实现开关按钮

    前言:Android自定义View对于刚入门乃至工作几年的程序员来说都是非常恐惧的,但也是Android进阶学习的必经之路,平时项目中经常会有一些苛刻的需求,我们可以在GitHub上找到各种各样的效果,能用则用,不能用自己花功夫改改也能草草了事.不过随着工作经验和工作性质,越来越觉得自定义View是时候有必要自己花点功夫研究一下. 一.经过这两天的努力,自己也尝试着写了一个Demo,效果很简单,就是开关按钮的实现. 可能有的人会说这效果so easy,找UI切三张图就完事了,何必大费周折自定义.

  • Android 自定义View步骤

    例子如下:Android 自定义View 密码框 例子 1 良好的自定义View 易用,标准,开放. 一个设计良好的自定义view和其他设计良好的类很像.封装了某个具有易用性接口的功能组合,这些功能能够有效地使用CPU和内存,并且十分开放的.但是,除了开始一个设计良好的类之外,一个自定义view应该: l 符合安卓标准 l 提供能够在Android XML布局中工作的自定义样式属性 l 发送可访问的事件 l 与多个Android平台兼容. Android框架提供了一套基本的类和XML标签来帮您创

随机推荐