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

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

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

前言

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

需求

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

核心思想:

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

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

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

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

5、低仿CoordinatorLayout,滾動區(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)容滾動造成header折疊的控件
 */
class ScrollingCollapseTopLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): ViewGroup(context, attributeSet, defStyleAttr) {
    //外部滑動距離
    private var mScrollHeight = 0f
    //上次縱坐標(biāo)
    private var mLastY = 0f
    //當(dāng)前控件寬高
    private var mHeight = 0
    private var mWidth = 0
    //兩個部分
    private val header: Header = Header(context).apply {
        //設(shè)置header垂直方向,寬度鋪滿,高度自適應(yīng)
        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內(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才能獲得正確的寬高,會在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)
        //測量header
        header.onScroll(mScrollHeight.toInt())
        header.measure(widthMeasureSpec,
            MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),MeasureSpec.AT_MOST))
        //先measure一下獲得實(shí)際高度,再減去滑動的距離,也可以把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)
        }
        //測量滑動區(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) {
        //簡單布局下,上下兩部分
        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)先滾動折疊欄
                    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){
        //兩個區(qū)域
        val defaultArea: TextView
        val collapsingArea: TextView
        init {
            //添加兩個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 {
            //簡單點(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)前控件造成滑動沖突,就只能引入新的滑動控件了,這里使用了NestedScrollView,和ScrollView類似。NestedScrollView只允許有一個子view,至于為什么可以看下源碼,內(nèi)容不多。我這是直接創(chuàng)建了一個NestedScrollView,并往里面加個一個垂直的LinearLayout,后面更改xml里面的view節(jié)點(diǎn),往LinearLayout里面放。

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

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

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

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

onSizeChanged函數(shù)

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

滑動事件沖突處理

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

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

模仿CoordinatorLayout

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

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

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

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

header折疊效果

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

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

相關(guān)文章

最新評論