前言
最近在找工作,於是打開拉勾,看了看首頁,交互做的還是不錯的。先來看看拉勾效果
然後最終實現的效果
佈局是圖片直接用,所以會失真。
實現思路
首先這個是一個MD
的效果,可以使用自定義Behavior
來實現這個效果,仔細體驗會發現,這個交互是分三部分來實現的
頭部部分(比如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
,這裏用一個子FrameLayout
和NestedScrollView
代替。內容部分給個底部邊距?attr/actionBarSize
,抵消導航欄部分佔用的高度。
同時這裏定義了三個命名爲HeaderBehavior
,SearchBehavior
和ContentBehavior
三個Behavior
,也是接下來要實現的滾動效果
Behavior
HeaderBehavior
主要是處理往上滾動的,onNestedPreScroll
裏面進行translationY
設置來滑動。onInterceptTouchEvent
裏面判斷當手指鬆開的時候進行頭部展開和關閉的操作。ContentBehavior
用AppBarLayout
裏面的內部抽象類HeaderScrollingViewBehavior
來佈局,放在頭部部分的下面,由於HeaderScrollingViewBehavior
不是公共的,所以可以自己複製一份代碼出來,HeaderScrollingViewBehavior
主要是onMeasureChild
和layoutChild
兩個方法來進行大小和位置的分佈。然後在Behavior
的layoutDependsOn
裏面進行依賴頭部滑動,onDependentViewChanged
來處理內容部分的滾動SearchBehavior
和ContentBehavior
同理,用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