仿拉勾首頁之Behavior的學習

前言

最近在找工作,於是打開拉勾,看了看首頁,交互做的還是不錯的。先來看看拉勾效果

lagou.gif

然後最終實現的效果

lagou_my.gif

佈局是圖片直接用,所以會失真。

實現思路

首先這個是一個MD的效果,可以使用自定義Behavior來實現這個效果,仔細體驗會發現,這個交互是分三部分來實現的
image.png
頭部部分(比如banner之類的),內容部分(比如TabLayout+ViewPager),以及導航欄部分(實現漸變的效果)。這樣就是自定義三個Behavior

佈局

頭部部分的高度是固定的,用來算後面滑動的一個範圍;導航欄部分的搜索框邊距固定,用來說伸縮動畫的,dimens.xml定義如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="height">400dp</dimen>
    <dimen name="search_margin_left">50dp</dimen>
    <dimen name="search_margin_right">50dp</dimen>
</resources>

接下來就是佈局,詳細布局用圖片替換

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinator"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        app:layout_behavior="@string/HeaderBehavior"
        android:id="@+id/fl_head"
        android:layout_width="match_parent"
        android:layout_height="@dimen/height"
        >
        <ImageView
            android:id="@+id/iv_head"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/top"/>
    </FrameLayout>
    <RelativeLayout
        android:id="@+id/rlToolBar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:gravity="center_vertical"
        app:layout_behavior="@string/SearchBehavior">
        <ImageView
            android:id="@+id/ivArrow"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_centerVertical="true"
            android:scaleType="fitXY"
            android:visibility="gone"
            android:src="@drawable/arrow" />
        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_marginLeft="@dimen/search_margin_left"
            android:layout_marginRight="@dimen/search_margin_right"
            android:layout_centerVertical="true"
            app:cardBackgroundColor="@android:color/white"
            app:cardCornerRadius="15dp">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="Android應用開發"
                android:gravity="center"/>
        </android.support.v7.widget.CardView>
        <ImageView
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignParentRight="true"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_centerVertical="true"
            android:scaleType="fitXY"
            android:src="@drawable/code"
            android:tint="@android:color/white"/>
    </RelativeLayout>
    <LinearLayout
        app:layout_behavior="@string/ContentBehavior"
        android:id="@+id/llContent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="?attr/actionBarSize"
        android:orientation="vertical">
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <ImageView
                android:id="@+id/iv_tab"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:scaleType="fitXY"
                android:src="@drawable/tab"/>
        </FrameLayout>
        <android.support.v4.widget.NestedScrollView
            android:id="@+id/nsv"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <ImageView
                android:id="@+id/iv_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scaleType="fitXY"
                android:src="@drawable/content"/>
        </android.support.v4.widget.NestedScrollView>
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

三個部分,頭部FrameLayout,導航欄RelativeLayout透明覆蓋再頭部部分上面,內容部分LinearLayout放置如TabLayout+ViewPager,這裏用一個子FrameLayoutNestedScrollView代替。內容部分給個底部邊距?attr/actionBarSize,抵消導航欄部分佔用的高度。

同時這裏定義了三個命名爲HeaderBehaviorSearchBehaviorContentBehavior三個Behavior,也是接下來要實現的滾動效果

Behavior

  • HeaderBehavior主要是處理往上滾動的,onNestedPreScroll裏面進行translationY設置來滑動。onInterceptTouchEvent裏面判斷當手指鬆開的時候進行頭部展開和關閉的操作。

  • ContentBehaviorAppBarLayout裏面的內部抽象類HeaderScrollingViewBehavior來佈局,放在頭部部分的下面,由於HeaderScrollingViewBehavior不是公共的,所以可以自己複製一份代碼出來,HeaderScrollingViewBehavior主要是onMeasureChildlayoutChild兩個方法來進行大小和位置的分佈。然後在BehaviorlayoutDependsOn裏面進行依賴頭部滑動,onDependentViewChanged來處理內容部分的滾動

  • SearchBehaviorContentBehavior同理,用DrawableCompat來處理圖片顏色漸變,TransitionManager這個類來給margin伸縮一個動畫

在寫各個Behavior之前,會發現頭部部分和內容部分滾動的速度和範圍是不一樣的,所以這裏先定義下滾動的範圍

object Utils{
  //內容部分在頭部滾動的範圍
    fun getScrollHeight(ctx: Context):Float{
       val  mHeight = ctx.resources.getDimension(R.dimen.height).toInt()
       val actionBarSize = getActionBarHeight(ctx)
      //如果上面內嵌到了狀態欄,還要減去狀態欄高度,同時內容部分底部邊距也是狀態欄高度+導航欄高度
        return (mHeight - actionBarSize)/getScrollFriction()
    }
  //給定內容部分和頭部滾動位移的一個倍數,
    fun getScrollFriction():Float = 1.5f
    fun getActionBarHeight(ctx: Context): Int {
        var actionBarHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, ctx.resources.displayMetrics).toInt()
        val tv = TypedValue()
        if (ctx.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
            actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, ctx.resources.displayMetrics)
        }
        return actionBarHeight
    }
    fun getStatusBarHeight(ctx: Context):Int{
        var result = 0
        val resourceId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
        if (resourceId > 0) {
            result = ctx.resources.getDimensionPixelSize(resourceId)
        }
        return result
    }
    fun range(min:Float,max:Float,value:Float):Float{
        return Math.min(Math.max(min,value),max)
    }
}

基本的關係定義好後,接下來就是實現Behavior

HeaderBehavior的代碼:

class HeaderBehavior: CoordinatorLayout.Behavior<View> {
    //滾動的View
    private var mChildView: View? =null
    //過了頭部滑動高度後,重新調用onStartNestedScroll的時候用
    private var axes:Int = ViewCompat.SCROLL_AXIS_VERTICAL
    private var type:Int = 0

    //手指鬆開後,通過`Scroller`類來實現頭部的展示和關閉的操作
    private var mScroller: Scroller
    //滾動的高度
    private var mHeight:Float = 0f
    private var mScrollRunnable:ScrollerRunnable?= null
    //是不是一開始頭部就隱藏了
    private var mStartHeaderHidden:Boolean = true 
    //滑動速度
    private var velocityY:Float = 0f
    constructor(ctx: Context,attributeSet: AttributeSet):super(ctx,attributeSet){
        mScroller = Scroller(ctx)
        mHeight = Utils.getScrollHeight(ctx)
    }
    //什麼方向可以滾動,並且滑動到了頭部高度就不攔截
    override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
        this.mChildView = child
        this.axes = axes
        this.type = type
        return (axes == ViewCompat.SCROLL_AXIS_VERTICAL) && !isClose()
    }
    //滾動監聽
    override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
        //降低移動距離,防止抖動,ScrollFriction太大還是會有抖動。。。。
        val tempDy = dy.toFloat()/(Utils.getScrollFriction()*2)
        child.translationY = Utils.range(-mHeight,0f,child.translationY - tempDy)
        consumed[1] = dy

    }
    //當頭部關閉了,就不攔截事件了
    override fun onNestedPreFling(coordinatorLayout: CoordinatorLayout, child: View, target: View, velocityX: Float, velocityY: Float): Boolean {
        this.velocityY = velocityY
        return !isClose()
    }
    override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: View, ev: MotionEvent): Boolean {
        when(ev.action){
            MotionEvent.ACTION_DOWN -> {
                mStartHeaderHidden = isClose()
            }
            MotionEvent.ACTION_MOVE -> {
              //如果滑動中到了頭部關閉之後就重新給事件分發下去
                if(isClose()&&!mStartHeaderHidden) parent.onStartNestedScroll(parent,child,axes,type)
            }
            MotionEvent.ACTION_UP -> {
              //手指鬆開的處理
                handlerActionUp()
            }
        }
        return super.onInterceptTouchEvent(parent, child, ev)
    }

    private fun createRunnable(){
        if(mScrollRunnable==null) mScrollRunnable = ScrollerRunnable(mScroller,mChildView!!,(mHeight+0.5f).toInt())
    }
    private fun handlerActionUp(){
        createRunnable()
        if(velocityY > 10000){
            mScrollRunnable?.scrollToClose()
            return
        }
        if(Math.abs(mChildView!!.translationY) < (mHeight /2)){
            mScrollRunnable?.scrollToOpen()
        }else{
            mScrollRunnable?.scrollToClose()
        }
    }
    open fun isClose():Boolean{
        return mChildView!=null && mChildView!!.translationY.toInt() <= - mHeight.toInt()
    }
    open fun scrollToOpen(){
        createRunnable()
        mScrollRunnable?.scrollToOpen()
    }
}
open class ScrollerRunnable(private var scroller:Scroller,
                       private var childView:View,
                       private var height:Int):Runnable{
    open fun scrollToOpen(){
        val scrollY = childView.translationY.toInt()
        scroller.startScroll(0,scrollY,0,-scrollY)
        startScroll()
    }
    open fun scrollToClose(){
        val currY = childView.translationY.toInt()
        val scrollY = height - Math.abs(currY)
        scroller.startScroll(0,currY,0,-scrollY)
        startScroll()
    }
    private fun startScroll(){
        if(scroller.computeScrollOffset()){
            childView.postDelayed(this,16)
        }
    }
    override fun run() {
        if(scroller.computeScrollOffset()){
            childView.translationY = scroller.currY.toFloat()
            childView.postDelayed(this,16)
        }
    }
}

ContentBehavior的代碼:

class ContentBehavior:HeaderScrollingViewBehavior {
    constructor(ctx: Context, attrs: AttributeSet): super(ctx,attrs)
    //依賴頭部部分的滾動上面
    override fun layoutDependsOn(parent: CoordinatorLayout?, child: View?, dependency: View): Boolean {
        return dependency.id == R.id.fl_head
    }
   //HeaderScrollingViewBehavior裏面的onMeasureChild使用
    override fun findFirstDependency(views: MutableList<View>): View? {
        for (i in views.indices) {
            val view = views[i]
            if (view.id == R.id.fl_head) {
                return view
            }
        }
        return null
    }
  //結合依賴進行監聽
    override fun onDependentViewChanged(parent: CoordinatorLayout?, child: View?, dependency: View?): Boolean {
        offsetChild(child!!, dependency!!)
        return super.onDependentViewChanged(parent, child, dependency)
    }

    private fun offsetChild(child: View, dependency: View) {
        //-0.5f滑動慢,最後一下沒監聽到
        child.translationY = (dependency.translationY - 0.5f)  * Utils.getScrollFriction()
    }
}

SearchBehavior的代碼如下:

class SearchBehavior:CoordinatorLayout.Behavior<View> {
    private val mMarginLeft:Int
    private val mMarginRight:Int
    private var mHeight:Float
    private var mExpend:Boolean = true
    private var mContext:Context
    private var mSet: AutoTransition
    private val mAnimDuration:Long = 300
    constructor(ctx: Context, attributeSet: AttributeSet):super(ctx,attributeSet){
        this.mMarginLeft = ctx.resources.getDimension(R.dimen.search_margin_left).toInt()
        this.mMarginRight = ctx.resources.getDimension(R.dimen.search_margin_right).toInt()
        this.mHeight = Utils.getScrollHeight(ctx)
        mContext = ctx
        mSet = AutoTransition()
        mSet.duration = mAnimDuration
    }

    override fun layoutDependsOn(parent: CoordinatorLayout?, child: View?, dependency: View): Boolean {
        return dependency.id == R.id.fl_head
    }
    override fun onDependentViewChanged(parent: CoordinatorLayout?, child: View?, dependency: View): Boolean {
        if(child is ViewGroup){
            //-0.5f滑動慢,最後一下沒監聽到
            val currY = dependency.translationY- 0.5f
            val alpha = Utils.range(0f,1f,Math.abs(currY)/mHeight*2)
            //背景顏色
            child.setBackgroundColor(Color.argb(alpha.toInt()*255,255,255,255))
            //二維碼圖標變化
            if(child.getChildAt(2) is ImageView){
                val ivCode = (child.getChildAt(2) as ImageView)
                val codeDrawable = ivCode.drawable
                DrawableCompat.setTint(codeDrawable,
                        if(alpha <= 0.2f) Color.WHITE else Color.argb(alpha.toInt()*255,144,144,144))
                ivCode.setImageDrawable(codeDrawable)
            }
            val expend = Math.abs(currY) >= mHeight
            //箭頭展示,動畫結束後顯示
            val ivArrow = child.getChildAt(0)
            if(!expend){
                ivArrow.visibility = View.GONE
            }else{
              //動畫結束後再顯示
                ivArrow.postDelayed({ivArrow.visibility = View.VISIBLE },mAnimDuration)
            }
            //根據margin做伸縮變化
            toggle(child,expend,false)
        }
        return super.onDependentViewChanged(parent, child, dependency)
    }

    fun toggle(targetView: ViewGroup, expend:Boolean, force:Boolean){
        if(expend != mExpend||force){
            this.mExpend = expend
            val height = targetView.height
            if(height == 0 && !force){
                targetView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener{
                    override fun onPreDraw(): Boolean {
                        targetView.viewTreeObserver.removeOnPreDrawListener(this)
                        toggle(targetView,expend,true)
                        return true
                    }
                })
            }
            if(expend) expand(targetView.getChildAt(1) as CardView) else reduce(targetView.getChildAt(1) as CardView)
        }
    }
    private fun expand(targetView: ViewGroup){
        val layoutParams = targetView.layoutParams as RelativeLayout.LayoutParams
        layoutParams.width = RelativeLayout.LayoutParams.MATCH_PARENT
        layoutParams.setMargins(mMarginLeft, 0, mMarginRight, 0)
        targetView.layoutParams = layoutParams
        beginDelayedTransition(targetView)
    }

    private fun reduce(targetView: ViewGroup) {
        val layoutParams = targetView.layoutParams as RelativeLayout.LayoutParams
        layoutParams.width = RelativeLayout.LayoutParams.MATCH_PARENT
        layoutParams.setMargins(mMarginLeft - mMarginLeft*2/3,0, mMarginRight, 0)
        targetView.layoutParams = layoutParams
        beginDelayedTransition(targetView)
    }
    private fun beginDelayedTransition(view: ViewGroup) {
        TransitionManager.beginDelayedTransition(view, mSet)
    }
}

最終的實現效果如上,詳細代碼MDStudy-Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章