Android 多層視差頭部背景的實現

Android開發小白,還在實習階段。請大佬們輕噴,謝謝!git

前言

由於公司在作電影院線的手機應用,有一個需求是作如圖的這種多層視差頭部背景(multi-layer parallax background),原生且不使用第三方庫。因此首先想到的就是直接使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout來實現最基礎的視差背景效果。github

平臺:Android Studio, 語言:Kotlinide

最終效果如圖,經反覆測試流暢無問題(若是後續測試有問題還會更新)。 佈局

demo演示

想法

  1. 首先使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout佈局來實現一個基本的帶摺疊效果的佈局,能夠自定義背景圖片的大小和佈局方式,想實現parallax只要添加「parallax」 屬性就能夠輕鬆實現。post

  2. 而後就是如何添加另外多出來的這一層背景佈局並給它不同的移動速度,讓咱們看起來是有三層(背景+背景層內容+下方具體內容)layout帶有三個不一樣的移動速度,形成多層視差效果。測試

  3. 背景層添加內容很容易,只要新建一個新的LinearLayout,構建好內容的佈局,在AppbarLayout中include進來就能夠了。由於這一部分的源碼本質其實是extend了一個FrameLayout,因此咱們能夠將多層內容重疊擺放在頭部位置。而後在MainActivity中獲取內容的id,這一步算完成了。ui

  4. 下面是最重要的一步,如何讓這一部分的layout有不同的上劃速度,而且在慣性滑動過程當中,也能夠隨時監測底部位置並更改本身自己的位置。this

    4.1.全部的觸摸事件都繞不開三個大佬,dispatchTouchEvent(),InterceptedTouchEvent()onTouchEvent()。因此果斷重寫CoordinatorLayout, 重寫 InterceptedTouchEvent()onTouchEvent()dispatchTouchEvent() 暫時不用管他。咱們在InterceptedTouchEvent()中截獲手指在屏幕上的動做,而後根據咱們的要求來分發事件。若是檢測到手指是向上劃的,就return true把事件傳遞給onTouchEvent()去處理。spa

    4.2 在新的CoordinatorLayout中,還要寫一個open function來使Acticity能夠將頭部背景的圖片傳遞過來,只有這樣咱們才能正常在新建的layout中處理圖片位置和獲取相關信息。這一點很重要,不然咱們無法在這個文件裏找到背景圖片的代碼位置(無法findViewById)。code

    fun getContent (content : LinearLayout, header:View, realcontent : View){
        this.content = content
        this.header = header
        this.realcontent = realcontent
        content.post {
            run{
                headerInitPosition = getViewPositionY(header).toFloat()
                headerContentInitPosition = getViewPositionY(content).toFloat()
                realContentInitPosition = getViewPositionY(realcontent).toFloat()
                System.out.println("init positions get : header-->$headerInitPosition, header content-->$headerContentInitPosition, real content-->$realContentInitPosition")
            }
        }
    }
    複製代碼

    4.3 在onTouchEvent()中,實時檢測底部的位置變化。這就須要咱們在4.2所定義的方法中將三層內容的信息所有傳遞過來,方便咱們在layout中檢測和更改。

    InterceptTouchEvent()

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        parent.requestDisallowInterceptTouchEvent(true)
        when(ev!!.action){
            MotionEvent.ACTION_DOWN -> {
                isTouched = true
                isDragging = false
                initX = ev.x
                initY = ev.y
            }
            MotionEvent.ACTION_MOVE -> {
                isDragging = true
                val draggedX = ev.x - initX
                val draggedY = ev.y - initY
    
                if (draggedY < 0){
                    return true
                }
            }
            MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL -> {}
        }
        return super.onInterceptTouchEvent(ev)
    }
    複製代碼

    onTouchEvent()

    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        when(ev!!.action){
            MotionEvent.ACTION_DOWN -> {}
            MotionEvent.ACTION_MOVE -> {
                val draggedX = ev.x - initX
                val draggedY = ev.y - initY
                println("$draggedY")
                println("content x: ${getViewPositionX(content)}, y: ${getViewPositionY(content)}")
                println("header image x: ${getViewPositionX(header)}, y: ${getViewPositionY(header)}")
                println("real content x: ${getViewPositionX(realcontent)}, y: ${getViewPositionY(realcontent)}")
            }
            MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL -> {}
        }
        return super.onTouchEvent(ev)
    }
    複製代碼

    4.4 由於有慣性滑動的存在,咱們不能在onTouchEvent中根據手指位置的移動來改變第二層layout的位置,因此在layout的onTouchEvent中咱們只觀察佈局原件們的位置變化,最終的動做仍是要在activity中完成。在Activity中,咱們用一個handler和runneble,使用postDelayed來自定義一個每1ms執行一次的檢測動做,來實時監測layout中各個原件的位置變化,來進行位置調整。

    val handler = Handler()
        val runnable: Runnable = object : Runnable {
            override fun run() {
                val changedY = getViewPositionY(real_content) - realContentInitPosition
                println("real content $changedY")
                val threshold = 450
                if (getViewPositionY(real_content)<=threshold){
                    val temp = threshold-toolbar_statusbar_height
                    val ratio = 1-(getViewPositionY(real_content)-toolbar_statusbar_height)/temp
                    top_title.alpha = ratio
                }else{
                    top_title.alpha = 0f
                }
                if (getViewPositionY(real_content).toFloat() == toolbar_statusbar_height){
                    top_title.alpha = 1f
                }
                content.scrollY = (changedY/5).toInt()
                handler.postDelayed(this, 1)
            }
        }
        handler.postDelayed(runnable,1)
    複製代碼

    4.5 既然在Activity中要處理佈局的位置變化,咱們就要先獲取佈局的初始位置並作出相應的位置調整,因爲activity中的佈局初始化比layout中的佈局初始化要早執行,因此咱們經過一個小的延時來在Activity中獲取到所需的layout的初始位置座標。

    val handler1 = Handler()
        val runnable1 = Runnable {
            realContentInitPosition = getViewPositionY(real_content).toFloat()
            toolbar_statusbar_height = toolbar.layoutParams.height + getStatusBarHeight()
        }
        handler1.postDelayed(runnable1,100)
    複製代碼

    另外,獲取位置座標的方法:(返回值即爲Y軸座標,return position[0]即返回x軸座標)

    fun getViewPositionY(view: View):Int{
        val position = IntArray(2)
        view.getLocationOnScreen(position)
        return position[1]
    }
    複製代碼
  5. 若是在處理touchEvent的時候,發現動做意外的被父控件攔截或者捕捉不到動做了,必定要在dispatchTouchEvent(),InterceptedTouchEvent()onTouchEvent() 中加上parent.requestDisallowInterceptTouchEvent(true)就OK了。還有若是發如今使用了自定義的新CoordinatorLayout以後,下部的NestedScrollView中的內容沒法滑動了,再新建一個class而後像這樣寫一個新的NestedScrollView就好了。

class CustomeNestedScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : NestedScrollView(context, attrs, defStyleAttr) {

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        parent.requestDisallowInterceptTouchEvent(true)
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        parent.requestDisallowInterceptTouchEvent(true)
        return super.onTouchEvent(ev)
    }
}
複製代碼
  1. 頂部標題的問題,因爲沒有使用behavior,因此仍是在本身創建的實時檢測循環里加入了改變頂部title透明度的代碼,根據谷歌官方CoordinatorLayout給的出現遮罩層的位置和出現toolbar的位置,來調整title的alpha值,就OK了。代碼也在4.4中有體現。

總結

不知道本身使用的postDelayed方法來一直不停的檢測位置變化的方法是否是正確,是否會形成對軟件運行流暢度的影響。若是各位有建議請提給我謝謝!

傳送門:Github -- Multi-Layer-Parallax-Background

bug :

谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout 佈局有一個bug,至今據我測試尚未修復,就是若是在調整了頭部背景的高度的時候,很容易在向下滑動的時候從頭部圖片滑動,若是手指離開屏幕布局進入慣性滑動fling階段,在慣性滑動沒有中止以前從新滑動屏幕(非頭部區域),佈局會產生抖動並且沒法控制。這是由於當開始從頭部滑動時,該動做被頭部layout處理,產生的fling也是由它產生的,咱們沒有辦法從外部中止這個fling,若是在這個時候觸摸屏幕並且觸摸點在非頭部背景區域,這個動做就會和以前的慣性滑動動做衝突。

查過解決方案,也嘗試過手動解決這個問題可是並無奏效。用反射的方法獲取父類的父類的父類中的overScroller和flingRunnable對象,在自定義的layout中用set方法手動注入咱們本身的scroller,這樣咱們就能夠控制慣性滑動的動做並隨時使用abortAnimation()中止fling。若是有大神有更好的辦法請賜教!

相關文章
相關標籤/搜索