亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android實(shí)現(xiàn)滑動(dòng)折疊Header全流程詳解

 更新時(shí)間:2022年11月02日 11:30:34   作者:撿一晌貪歡  
這篇文章主要介紹了Android實(shí)現(xiàn)滑動(dòng)折疊Header,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧

前言

上一篇文章直接通過安卓自定義view的知識(shí)手撕了一個(gè)側(cè)滑欄,做的還不錯(cuò),很有成就感。這篇文章的控件沒有上一篇的復(fù)雜,比較簡(jiǎn)單,通過一個(gè)內(nèi)容滾動(dòng)造成header折疊的控件學(xué)習(xí)一下滑動(dòng)事件沖突問題、更改view節(jié)點(diǎn)以及CoordinatorLayout事件傳遞(超低仿),基本都是一個(gè)引子,希望學(xué)完這個(gè)控件,要繼續(xù)省略學(xué)習(xí)下涉及的內(nèi)容。

需求

這里就是希望做一個(gè)滾動(dòng)通過內(nèi)容能夠折疊header的控件,在XML內(nèi)寫的控件能夠有滾動(dòng)效果,header暫時(shí)默認(rèn)實(shí)現(xiàn)。

核心思想:

1、兩部分,一個(gè)header和一個(gè)可以滾動(dòng)的區(qū)域

2、header有兩種狀態(tài),一個(gè)是完全展開狀態(tài),一個(gè)是折疊狀態(tài)

3、在滾動(dòng)區(qū)域向下滾動(dòng)的時(shí)候,header會(huì)先滾動(dòng)到折疊狀態(tài),header折疊后滾動(dòng)區(qū)域才開始滾動(dòng)

4、在滾動(dòng)區(qū)域向上滾動(dòng)的時(shí)候,滾動(dòng)區(qū)域先滾動(dòng),滾動(dòng)區(qū)域到頂了才開始展開header

5、低仿CoordinatorLayout,滾動(dòng)區(qū)域效果通過自定義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
/**
 * 內(nèi)容滾動(dòng)造成header折疊的控件
 */
class ScrollingCollapseTopLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): ViewGroup(context, attributeSet, defStyleAttr) {
    //外部滑動(dòng)距離
    private var mScrollHeight = 0f
    //上次縱坐標(biāo)
    private var mLastY = 0f
    //當(dāng)前控件寬高
    private var mHeight = 0
    private var mWidth = 0
    //兩個(gè)部分
    private val header: Header = Header(context).apply {
        //設(shè)置header垂直方向,寬度鋪滿,高度自適應(yīng)
        orientation = LinearLayout.VERTICAL
        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
    }
    //NestedScrollView只允許一個(gè)子view(和ScrollView一樣),這里放一個(gè)垂直的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內(nèi)view結(jié)束,沒執(zhí)行onMeasure
    override fun onFinishInflate() {
        super.onFinishInflate()
        //在這里獲得所有子view,攔截添加到scrollArea去
        if (xmlViews.size == 0) {
            forEach { view ->
                xmlViews.add(view)
            }
        }
        //更換view的節(jié)點(diǎn)
        removeAllViewsInLayout()
        addView(header)
        addView(scrollArea)
        //把當(dāng)前控件全部view放到NestedScrollView內(nèi)的LinearLayout內(nèi)去
        (scrollArea.getChildAt(0) as ViewGroup).also { linear->
            for(view in xmlViews) {
                linear.addView(view)
            }
        }
    }
    //在onSizeChanged才能獲得正確的寬高,會(huì)在onMeasure后得到,這里只是學(xué)一下
    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)
        //測(cè)量header
        header.onScroll(mScrollHeight.toInt())
        header.measure(widthMeasureSpec,
            MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),MeasureSpec.AT_MOST))
        //先measure一下獲得實(shí)際高度,再減去滑動(dòng)的距離,也可以把header.measuredHeight寫成全局變量
        if (header.measuredHeight != 0) {
            val scrolledHeight = header.measuredHeight + mScrollHeight
            val headerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(scrolledHeight.toInt(),
                MeasureSpec.getMode(MeasureSpec.EXACTLY))
            //再次測(cè)量的目的是后面滾動(dòng)部分要占滿剩余高度
            header.measure(widthMeasureSpec, headerHeightMeasureSpec)
        }
        //測(cè)量滑動(dòng)區(qū)域
        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) {
        //簡(jiǎn)單布局下,上下兩部分
        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
                    //如果折疊了,優(yōu)先滾動(dòng)折疊欄
                    val canScrollTop = scrollArea.canScrollVertically(-1)
                    val canScrollBottom = scrollArea.canScrollVertically(1)
                    //可以滾動(dòng)
                    isIntercepted = if (canScrollTop || canScrollBottom) {
                        //手指向上移動(dòng)時(shí),沒折疊前要攔截
                        val scrollUp = dY < 0 &&
                                mScrollHeight + dY > -header.collapsingArea.height.toFloat()
                        //手指向下移動(dòng)時(shí),沒展開前且到頂了要攔截
                        val scrollDown = dY > 0 &&
                                mScrollHeight + dY < 0f &&
                                !canScrollTop
                        scrollUp || scrollDown
                    }else {
                        //不能滾動(dòng)
                        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 -> {
                    //累加滑動(dòng)值,請(qǐng)求重新布局
                    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)
    }
    //這里就做一個(gè)簡(jiǎn)單的折疊header,
    @Suppress("MemberVisibilityCanBePrivate")
    inner class Header @JvmOverloads constructor(
        context: Context,
        attributeSet: AttributeSet? = null,
        defStyleAttr: Int = 0,
    ): LinearLayout(context, attributeSet, defStyleAttr){
        //兩個(gè)區(qū)域
        val defaultArea: TextView
        val collapsingArea: TextView
        init {
            //添加兩個(gè)header區(qū)域
            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))
                }
            }
        }
        //創(chuàng)建TextView
        private fun makeTextView(context: Context, textStr: String, height: Int): TextView {
            //簡(jiǎn)單點(diǎn)height和textSize應(yīng)該用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的使用

要想中間內(nèi)容能夠滾動(dòng),并且和當(dāng)前控件造成滑動(dòng)沖突,就只能引入新的滑動(dòng)控件了,這里使用了NestedScrollView,和ScrollView類似。NestedScrollView只允許有一個(gè)子view,至于為什么可以看下源碼,內(nèi)容不多。我這是直接創(chuàng)建了一個(gè)NestedScrollView,并往里面加個(gè)一個(gè)垂直的LinearLayout,后面更改xml里面的view節(jié)點(diǎn),往LinearLayout里面放。

修改xml內(nèi)view的節(jié)點(diǎn)

上一篇文章里面,側(cè)滑欄在xml里面的位置會(huì)影響繪制的層級(jí),我是在onLayout里面通過移除再添加的方式做的,那如果要把view改到其他view里面去該怎么辦。一開始我覺得很簡(jiǎn)單嘛,直接在onMeasure里面得到所有xml里面的view,再添加到其他viewgroup里面不就行了!想法很簡(jiǎn)單,試一下結(jié)果出我問題了。

第一個(gè)問題是view添加到其他viewgroup必須先移除,那我就直接就removeViewInLayout,結(jié)果就出了第二個(gè)問題OverStackError,大致就是一直measure,試了下是addView導(dǎo)致的,邏輯還是有問題。后面想想不應(yīng)該在onMeasure里面實(shí)現(xiàn)的,應(yīng)該在viewgroup加載xml里面子view時(shí)攔截處理的。

于是找了下api,發(fā)現(xiàn)viewgroup提供了一個(gè)onFinishInflate方法,會(huì)在加載xml里面view完成時(shí)調(diào)用,關(guān)鍵是它只會(huì)調(diào)用一次,onMeasure會(huì)調(diào)用多次,正好符合了我們的需求。修改節(jié)點(diǎn)就簡(jiǎn)單了,for循環(huán)一下就ok。

onSizeChanged函數(shù)

上面用到了onFinishInflate方法,找資料的時(shí)候看到自定義view里面常用重寫的方法還有一個(gè)onSizeChanged函數(shù)。其實(shí)用的也多,主要是自定義view時(shí)用來獲取控件寬高的,當(dāng)控件的Size發(fā)生變化,如measure結(jié)束,onSizeChanged被調(diào)用,這時(shí)候才能拿到寬高,不然拿到的height和width就是0。

滑動(dòng)事件沖突處理

我覺得滑動(dòng)事件沖突的處理都應(yīng)該根據(jù)實(shí)際情況去處理,知識(shí)的話可以去看看《安卓開發(fā)藝術(shù)探討》里面的相關(guān)知識(shí),主要解決辦法就是內(nèi)部攔截法和外部攔截法。我這就是簡(jiǎn)單的外部攔截法,本來想寫復(fù)雜點(diǎn),看看能不能多學(xué)點(diǎn)東西,結(jié)果根據(jù)需求,最后的代碼很簡(jiǎn)單。

外部攔截法原理就是在onInterceptTouchEvent方法中,通過根據(jù)場(chǎng)景判斷是內(nèi)部滾動(dòng)還是外部滾動(dòng),外部滾動(dòng)就直接攔截,內(nèi)部是否能滾動(dòng)可以通過canScrollVertically/canScrollHorizontally方法判斷。我這邏輯很簡(jiǎn)單,首先判斷下內(nèi)部是否能滾動(dòng),內(nèi)部不能滾動(dòng)就直接交給外部處理;然后又分兩種情況,一個(gè)是手指向上移動(dòng)時(shí),沒折疊前要攔截,另一個(gè)就是手指向下移動(dòng)時(shí),沒展開前且到內(nèi)部頂了要攔截。無論真么處理,還是得根據(jù)情景,

模仿CoordinatorLayout

本來還想模仿CoordinatorLayout做一個(gè)滑動(dòng)狀態(tài)傳遞的,這里滾動(dòng)控件用的NestedScrollingChild,想讓當(dāng)前控件繼承NestedScrollingParent處理滑動(dòng)沖突,后面覺得還是簡(jiǎn)單點(diǎn)自己在onInterceptTouchEvent方法中處理能學(xué)點(diǎn)東西。當(dāng)然讀者有興趣可借機(jī)學(xué)習(xí)一下NestedScrollingChild和NestedScrollingParent。

對(duì)于CoordinatorLayout,我也是學(xué)習(xí)了一下其中原理,私以為大致就是CoordinatorLayout的LayoutParams內(nèi)有一個(gè)Behavior屬性,Behavior作用就是構(gòu)建兩個(gè)子控件的關(guān)聯(lián)關(guān)系(在CoordinatorLayout的onMeasure中),建立關(guān)聯(lián)關(guān)系后,當(dāng)一個(gè)view變化就會(huì)造成關(guān)聯(lián)的view跟著變化(CoordinatorLayout控制),當(dāng)然原理沒這么簡(jiǎn)單,還是要去看源碼。

本來我也想按這個(gè)邏輯模仿一下的,首先就是給當(dāng)前控件的LayoutParams加一個(gè)Behavior屬性,當(dāng)滾動(dòng)控件設(shè)置這個(gè)Behavior屬性時(shí),Header類在measure的時(shí)候就創(chuàng)建一個(gè)Behavior屬性的私有變量,當(dāng)前控件通過NestedScrollingChild接受滾動(dòng)事件,并交給Header類的Behavior屬性的私有變量去處理,一套邏輯下來,總感覺有脫褲子放屁的感覺,畢竟我這個(gè)控件就兩個(gè)子控件。CoordinatorLayout的目的是協(xié)調(diào)多 View 之間的聯(lián)動(dòng),重點(diǎn)在多,我這真沒必要。

其實(shí)說到底,CoordinatorLayout就是一個(gè)協(xié)調(diào)功能,關(guān)聯(lián)兩個(gè)控件,比如我這就是滾動(dòng)控件發(fā)出滾動(dòng)消息,當(dāng)前控件收到滾動(dòng)消息,傳遞到Header里面處理,就這么簡(jiǎn)單,多了倒是可以按上面邏輯處理。

header折疊效果

這里的header的折疊效果是從onMeasure里面得到的!在測(cè)量時(shí),根據(jù)滑動(dòng)值,修改header的heightMeasureSpec,把header的高度設(shè)置為原有高度減去滑動(dòng)高度,測(cè)量完header之后,把剩余的高度給到滑動(dòng)區(qū)域,onLayout的時(shí)候?qū)蓚€(gè)控件挨著就行?;瑒?dòng)的時(shí)候,請(qǐng)求重新layout,header和滾動(dòng)區(qū)域每次都會(huì)獲得不一樣的高度,看起來就有了折疊效果。

到此這篇關(guān)于Android實(shí)現(xiàn)滑動(dòng)折疊Header全流程詳解的文章就介紹到這了,更多相關(guān)Android Header內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論