最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動(dòng)控件

 更新時(shí)間:2022年11月03日 08:42:03   作者:撿一晌貪歡  
這篇文章主要介紹了Android自定義view實(shí)現(xiàn)有header和footer的滾動(dòng)控件,可以在XML中當(dāng)Layout使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧

前言

上兩篇文章對(duì)安卓自定義view的事件分發(fā)做了一些應(yīng)用,但是對(duì)于自定義view來(lái)講,并不僅僅是事件分發(fā)這么簡(jiǎn)單,還有一個(gè)很重要的內(nèi)容就是view的繪制流程。接下來(lái)我這通過(guò)帶header和footer的Layout,來(lái)學(xué)習(xí)一下ViewGroup的自定義流程,并對(duì)其中的MeasureSpec、onMeasure以及onLayout加深理解。

需求

這里就是一個(gè)有header和footer的滾動(dòng)控件,可以在XML中當(dāng)Layout使用,核心思想如下:

1、由header、XML內(nèi)容、footer三部分組成

2、滾動(dòng)中間控件時(shí),上面有內(nèi)容時(shí)header不顯示,下面有內(nèi)容時(shí)footer不顯示

3、滑動(dòng)到header和footer最大值時(shí)不能滑動(dòng),釋放的時(shí)候需要回彈

4、完全顯示時(shí)隱藏footer

編寫代碼

編寫代碼這部分還真讓我頭疼了一會(huì),主要就是MeasureSpec的運(yùn)用,如何讓控件能夠超出給定的高度,如何獲得實(shí)際高度和控件高度,真是紙上得來(lái)終覺(jué)淺,絕知此事要躬行,看書(shū)那么多遍,實(shí)際叫自己寫起來(lái)真的費(fèi)勁,不過(guò)最終寫完,才真的敢說(shuō)自己對(duì)measure和layout有一定了解了。

先看代碼,再講問(wèn)題吧!

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的滾動(dòng)控件
 * 核心思想:
 * 1、由header、container、footer三部分組成
 * 2、滾動(dòng)中間控件時(shí),上面有內(nèi)容時(shí)header不顯示,下面有內(nèi)容時(shí)footer不顯示
 * 3、滑動(dòng)到header和footer最大值時(shí)不能滑動(dòng),釋放的時(shí)候需要回彈
 * 4、完全顯示時(shí)隱藏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
    //上次事件的橫坐標(biāo)
    private var mLastY = 0f
    //總高度
    private var totalHeight = 0
    //是否全部顯示
    private var isAllDisplay = false
    //流暢滑動(dòng)
    private var mScroller = Scroller(context)
    init {
        //設(shè)置默認(rèn)的Header、Footer,這里是從構(gòu)造來(lái)的,如果外部設(shè)置需要另外處理
        header = header ?: makeTextView(context, "Header")
        footer = footer ?: makeTextView(context, "Footer")
        //添加對(duì)應(yīng)控件
        addView(header, 0)
        //這里還沒(méi)有加入XML中的控件
        //Log.e("TAG", "init: childCount=$childCount", )
        addView(footer, 1)
    }
    //創(chuàng)建默認(rèn)的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)
            //不設(shè)置isClickable的話,點(diǎn)擊該TextView會(huì)導(dǎo)致mFirstTouchTarget為null,
            //致使onInterceptTouchEvent不會(huì)被調(diào)用,只有ACTION_DOWN能被收到,其他事件都沒(méi)有
            //因?yàn)槭录蛄兄蠥CTION_DOWN沒(méi)有被消耗(返回true),整個(gè)事件序列被丟棄了
            //如果XML內(nèi)是TextView也會(huì)造成同樣情況,
            isFocusable = true
            isClickable = true
        }
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //父容器給當(dāng)前控件的寬高,默認(rèn)值盡量設(shè)大一點(diǎn)
        val width = getSizeFromMeasureSpec(1080, widthMeasureSpec)
        val height = getSizeFromMeasureSpec(2160, heightMeasureSpec)
        //對(duì)子控件進(jìn)行測(cè)量
        forEach { child ->
            //寬度給定最大值
            val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST)
            //高度不限定
            val childHeightMeasureSpec
                = MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)
            //進(jìn)行測(cè)量,不測(cè)量的話measuredWidth和measuredHeight會(huì)為0
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
            //Log.e("TAG", "onMeasure: child.measuredWidth=${child.measuredWidth}")
            //Log.e("TAG", "onLayout: child.measuredHeight=${child.measuredHeight}")
        }
        //設(shè)置測(cè)量高度為父容器最大寬高
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
            MeasureSpec.getSize(heightMeasureSpec))
    }
    private fun getSizeFromMeasureSpec(defaultSize: Int, measureSpec: Int): Int {
        //獲取MeasureSpec內(nèi)模式和尺寸
        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)
        //完全顯示內(nèi)容時(shí)不加載footer,header不算入內(nèi)容
        if (measuredHeight < curHeight - header!!.height) {
            //設(shè)置全部顯示flag
            isAllDisplay = false
            footer.layout(left, top + curHeight, right,top + curHeight + footer.measuredHeight)
            curHeight += footer.measuredHeight
        }
        //布局完成,滾動(dòng)一段距離,隱藏header
        scrollBy(0, header!!.height)
        //設(shè)置總高度
        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
        //更新點(diǎn)擊的縱坐標(biāo)
        mLastY = e.y
        //縱坐標(biāo)的可滑動(dòng)范圍,0 到 隱藏部分高度,全部顯示內(nèi)容時(shí)是header高度
        val scrollMax = if (isAllDisplay) {
            header!!.height
        }else {
            totalHeight - height
        }
        //限定滾動(dòng)范圍
        if ((scrollY + dy) <= scrollMax &&  (scrollY + dy) >= 0) {
            //觸發(fā)移動(dòng)
            scrollBy(0, dy.toInt())
        }
    }
    private fun stopMove() {
        //Log.e("TAG", "stopMove: height=$height, measuredHeight=$measuredHeight")
        //如果滑動(dòng)到顯示了header,就通過(guò)動(dòng)畫隱藏header,并觸發(fā)到達(dá)頂部回調(diào)
        if (scrollY < header!!.height) {
            mScroller.startScroll(0, scrollY, 0, header!!.height - scrollY)
            onReachHeadListener?.onReachHead()
        }else if(!isAllDisplay && scrollY > (totalHeight - height - footer!!.height)) {
            //如果滑動(dòng)到顯示了footer,就通過(guò)動(dòng)畫隱藏footer,并觸發(fā)到達(dá)底部回調(diào)
            mScroller.startScroll(0, scrollY,0,
                 (totalHeight - height- footer!!.height) - scrollY)
            onReachFootListener?.onReachFoot()
        }
        invalidate()
    }
    //流暢地滑動(dòng)
    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()
        }
    }
    //單位轉(zhuǎn)換
    @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()
    }
}

主要問(wèn)題

父容器給當(dāng)前控件的寬高

這里就是MeasureSpec的理解了,onMeasure中給了兩個(gè)參數(shù):widthMeasureSpec和heightMeasureSpec,里面包含了父控件給當(dāng)前控件的寬高,根據(jù)模式的不同可以取出給的數(shù)值,根據(jù)需要設(shè)定自身的寬高,需要注意setMeasuredDimension函數(shù)設(shè)定后,measuredWidth和measuredHeight才有值。

對(duì)子控件進(jìn)行測(cè)量

這里很容易忽略的是,當(dāng)繼承viewgroup的時(shí)候,我們要手動(dòng)去調(diào)用child的measure函數(shù),去測(cè)量child的寬高。一開(kāi)始我也沒(méi)注意到,當(dāng)我繼承LineaLayout的時(shí)候是沒(méi)問(wèn)題的,后面改成viewgroup后就出問(wèn)題了,看了下LineaLayout的源碼,里面的onMeasure函數(shù)中實(shí)現(xiàn)了對(duì)child的測(cè)量。

對(duì)子控件的測(cè)量時(shí),MeasureSpec又有用了,比如說(shuō)我們希望XML中的內(nèi)容不限高度或者高度很大,這時(shí)候MeasureSpec.UNSPECIFIED就有用了,而寬度我們希望最大就是控件寬度,就可以給個(gè)MeasureSpec.AT_MOST,注意我們給子控件的MeasureSpec也是有兩部分的,需要通過(guò)makeMeasureSpec創(chuàng)建。

子控件的擺放

由于我們的footer和header是在構(gòu)造里面創(chuàng)建并添加到控件中的,這時(shí)候XML內(nèi)的view還沒(méi)加進(jìn)來(lái),所以需要注意下footer實(shí)際在控件中是第二個(gè),擺放的時(shí)候根據(jù)index要特殊處理一下。

其他控件我們根據(jù)左上右下的順序擺放就行了,注意onMeasure總對(duì)子控件measure了才有寬高。

控件總高度和控件高度

因?yàn)樾枨?,我們的控件要求是中間可以滾動(dòng),所以在onMeasure總,我們用到了MeasureSpec.UNSPECIFIED,這時(shí)候控件的高度和實(shí)際總高度就不一致了。這里我們需要在onLayout中累加到來(lái),實(shí)際擺放控件的時(shí)候也要用到這個(gè)高度,順勢(shì)而為了。

header和footer的初始化顯示與隱藏

這里希望在開(kāi)始的時(shí)候隱藏header,所以需要在onLayout完了的時(shí)候,向上滾動(dòng)控件,高度為header的高度。

根據(jù)需求,完全顯示內(nèi)容的時(shí)候,我們不希望顯示footer,這里也要在onLayout里面實(shí)現(xiàn),根據(jù)XML內(nèi)容的高度和控件高度一比較就知道需不需要layout footer了。

header和footer的動(dòng)態(tài)顯示與隱藏

這里就和前面兩篇文章類似了,就是在縱坐標(biāo)上滾動(dòng)控件,限定滾動(dòng)范圍,在ACTION_UP事件時(shí)判定滾動(dòng)后的狀態(tài),動(dòng)態(tài)去顯示和隱藏header和footer,思路很明確,邏輯可能復(fù)雜一點(diǎn)。

使用

這里簡(jiǎn)單說(shuō)下使用吧,就是作為L(zhǎng)ayout,中間可以放控件,中間控件可以指定特別大的高度,也可以wrap_content,但是內(nèi)容很高。

<?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特別長(zhǎng),滾動(dòng)起來(lái)header和footer可以拉出來(lái),釋放會(huì)縮回去。還可以在代碼中獲得控件增加觸底和觸頂?shù)幕卣{(diào)。

中間為TextView時(shí)不觸發(fā)ACTION_MOVE事件

上面XML布局中,如果不加clickable=true的話,控件中只會(huì)收到一個(gè)ACTION_DOWN事件,然后就沒(méi)有然后了,即使是dispatchTouchEvent中也沒(méi)有事件了。經(jīng)查,原來(lái)不設(shè)置isClickable的話,點(diǎn)擊該TextView會(huì)導(dǎo)致mFirstTouchTarget為null,致使onInterceptTouchEvent不會(huì)被調(diào)用,因?yàn)槭录蛄兄蠥CTION_DOWN沒(méi)有被消耗(未返回true),整個(gè)事件序列被丟棄了。

結(jié)語(yǔ)

實(shí)際上這個(gè)控件寫的并不是很好,拿去用的話還是不太行的,但是用來(lái)學(xué)習(xí)的話還是能理解很多東西。

到此這篇關(guān)于Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動(dòng)控件的文章就介紹到這了,更多相關(guān)Android滾動(dòng)控件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Flutter沉浸式狀態(tài)欄/AppBar導(dǎo)航欄/仿咸魚(yú)底部凸起導(dǎo)航欄效果

    Flutter沉浸式狀態(tài)欄/AppBar導(dǎo)航欄/仿咸魚(yú)底部凸起導(dǎo)航欄效果

    這篇文章主要介紹了Flutter沉浸式狀態(tài)欄/AppBar導(dǎo)航欄/仿咸魚(yú)底部凸起導(dǎo)航欄效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-04-04
  • Grow heap (frag case) 堆內(nèi)存過(guò)大的深入解析

    Grow heap (frag case) 堆內(nèi)存過(guò)大的深入解析

    本篇文章是對(duì)Grow heap (frag case) 堆內(nèi)存過(guò)大的問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-06-06
  • Android自定義Gradle插件的詳細(xì)過(guò)程

    Android自定義Gradle插件的詳細(xì)過(guò)程

    Groovy語(yǔ)言是一種jvm語(yǔ)言,最終也是編譯成class文件然后在jvm上執(zhí)行,所以所有的Java語(yǔ)言的特性Groovy都支持,我們可以完全混寫Java和Groovy,對(duì)Android自定義Gradle插件相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧
    2021-07-07
  • Android自定義ProgressDialog進(jìn)度等待框

    Android自定義ProgressDialog進(jìn)度等待框

    這篇文章主要介紹了Android自定義ProgressDialog進(jìn)度等待框,通過(guò)本文大家可以嘗試?yán)肁ndroid自定義ProgressDialog,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-01-01
  • 簡(jiǎn)單實(shí)用的Android studio 調(diào)試技巧

    簡(jiǎn)單實(shí)用的Android studio 調(diào)試技巧

    這篇文章主要介紹了簡(jiǎn)單實(shí)用的Android studio 調(diào)試技巧的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2016-07-07
  • Android ListView實(shí)現(xiàn)上拉加載更多和下拉刷新功能

    Android ListView實(shí)現(xiàn)上拉加載更多和下拉刷新功能

    這篇文章主要為大家詳細(xì)介紹了Android ListView實(shí)現(xiàn)上拉加載更多和下拉刷新功能,介紹了ListView刷新原理及實(shí)現(xiàn)方法,感興趣的小伙伴們可以參考一下
    2016-05-05
  • Android框架組件Lifecycle的使用詳解

    Android框架組件Lifecycle的使用詳解

    這篇文章主要介紹了Android框架組件Lifecycle的使用詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-07-07
  • Android開(kāi)發(fā)中編寫藍(lán)牙相關(guān)功能的核心代碼講解

    Android開(kāi)發(fā)中編寫藍(lán)牙相關(guān)功能的核心代碼講解

    這篇文章主要介紹了Android開(kāi)發(fā)中編寫藍(lán)牙功能的核心部分講解,包括掃描和配對(duì)以及修改藍(lán)牙設(shè)備可見(jiàn)性等操作,需要的朋友可以參考下
    2016-02-02
  • Android APP與媒體存儲(chǔ)服務(wù)的交互

    Android APP與媒體存儲(chǔ)服務(wù)的交互

    本文介紹如何在 Android 中,開(kāi)發(fā)者的 APP 如何使用媒體存儲(chǔ)服務(wù)(包含MediaScanner、MediaProvider以及媒體信息解析等部分),包括如何把 APP 新增或修改的文件更新到媒體數(shù)據(jù)庫(kù)、如何在多媒體應(yīng)用中隱藏 APP 產(chǎn)生的文件、如何監(jiān)聽(tīng)媒體數(shù)據(jù)庫(kù)的變化等等。
    2013-10-10
  • Android 隱藏及切換顯示鍵盤

    Android 隱藏及切換顯示鍵盤

    這篇文章主要介紹了Android 隱藏及切換顯示鍵盤的相關(guān)資料,需要的朋友可以參考下
    2017-06-06

最新評(píng)論

思茅市| 噶尔县| 嘉定区| 九台市| 波密县| 十堰市| 抚宁县| 石门县| 耒阳市| 贞丰县| 甘德县| 新津县| 志丹县| 衡水市| 米林县| 崇州市| 台中县| 涡阳县| 固安县| 丽水市| 马边| 扎兰屯市| 金山区| 高碑店市| 合水县| 宜兰市| 延边| 渭源县| 贵溪市| 沈丘县| 巨鹿县| 瓮安县| 江孜县| 仪陇县| 临汾市| 上蔡县| 仁布县| 保亭| 鄂托克旗| 佛山市| 遵义县|