欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

前言

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

需求

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

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

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

3、滑動到header和footer最大值時不能滑動,釋放的時候需要回彈

4、完全顯示時隱藏footer

編寫代碼

編寫代碼這部分還真讓我頭疼了一會,主要就是MeasureSpec的運(yùn)用,如何讓控件能夠超出給定的高度,如何獲得實(shí)際高度和控件高度,真是紙上得來終覺淺,絕知此事要躬行,看書那么多遍,實(shí)際叫自己寫起來真的費(fèi)勁,不過最終寫完,才真的敢說自己對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、滾動中間控件時,上面有內(nèi)容時header不顯示,下面有內(nèi)容時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
    //上次事件的橫坐標(biāo)
    private var mLastY = 0f
    //總高度
    private var totalHeight = 0
    //是否全部顯示
    private var isAllDisplay = false
    //流暢滑動
    private var mScroller = Scroller(context)
    init {
        //設(shè)置默認(rèn)的Header、Footer,這里是從構(gòu)造來的,如果外部設(shè)置需要另外處理
        header = header ?: makeTextView(context, "Header")
        footer = footer ?: makeTextView(context, "Footer")
        //添加對應(yīng)控件
        addView(header, 0)
        //這里還沒有加入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會導(dǎo)致mFirstTouchTarget為null,
            //致使onInterceptTouchEvent不會被調(diào)用,只有ACTION_DOWN能被收到,其他事件都沒有
            //因?yàn)槭录蛄兄蠥CTION_DOWN沒有被消耗(返回true),整個事件序列被丟棄了
            //如果XML內(nèi)是TextView也會造成同樣情況,
            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)
        //對子控件進(jìn)行測量
        forEach { child ->
            //寬度給定最大值
            val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST)
            //高度不限定
            val childHeightMeasureSpec
                = MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)
            //進(jìn)行測量,不測量的話measuredWidth和measuredHeight會為0
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
            //Log.e("TAG", "onMeasure: child.measuredWidth=${child.measuredWidth}")
            //Log.e("TAG", "onLayout: child.measuredHeight=${child.measuredHeight}")
        }
        //設(shè)置測量高度為父容器最大寬高
        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)容時不加載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
        }
        //布局完成,滾動一段距離,隱藏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)的可滑動范圍,0 到 隱藏部分高度,全部顯示內(nèi)容時是header高度
        val scrollMax = if (isAllDisplay) {
            header!!.height
        }else {
            totalHeight - height
        }
        //限定滾動范圍
        if ((scrollY + dy) <= scrollMax &&  (scrollY + dy) >= 0) {
            //觸發(fā)移動
            scrollBy(0, dy.toInt())
        }
    }
    private fun stopMove() {
        //Log.e("TAG", "stopMove: height=$height, measuredHeight=$measuredHeight")
        //如果滑動到顯示了header,就通過動畫隱藏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)) {
            //如果滑動到顯示了footer,就通過動畫隱藏footer,并觸發(fā)到達(dá)底部回調(diào)
            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()
        }
    }
    //單位轉(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()
    }
}

主要問題

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

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

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

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

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

子控件的擺放

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

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

控件總高度和控件高度

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

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

這里希望在開始的時候隱藏header,所以需要在onLayout完了的時候,向上滾動控件,高度為header的高度。

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

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

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

使用

這里簡單說下使用吧,就是作為Layout,中間可以放控件,中間控件可以指定特別大的高度,也可以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特別長,滾動起來header和footer可以拉出來,釋放會縮回去。還可以在代碼中獲得控件增加觸底和觸頂?shù)幕卣{(diào)。

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

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

結(jié)語

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

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

相關(guān)文章

最新評論