一攻冷、概述
這樣一個效果圖投慈,我們思考下如何實現(xiàn)
可以看到“Sticky View”滾動到頂部會“固定住”,列表下拉到第一條數(shù)據(jù)“Sticky View”又會一起往下滾動接箫。
有人說,這個不就是View的事件分發(fā)嗎朵诫?
假設(shè)我們按照傳統(tǒng)的事件分發(fā)去理解辛友,我們滑動的是下面的內(nèi)容區(qū)域View,但是滾動的卻是外部的ViewGroup剪返,那么肯定是ViewGroup攔截了子View的事件废累;但是,上面的效果圖脱盲,當(dāng)ViewGroup滑動到一定程度邑滨,子View又開始滑動了,而且中間的過程是沒有間斷的钱反。從正常的事件分發(fā)機(jī)制來講這個是不可能的掖看,因為當(dāng)ViewGroup攔截事件后,是沒辦法再次交還給子View去處理的(除非你手動干預(yù)了事件的分發(fā))面哥,關(guān)于這一點如果有不清楚的同學(xué)哎壳,可以先去了解下Android的事件分發(fā)機(jī)制。
那么有沒有其他方案去解決我們的問題呢幢竹?答案是耳峦,有。
Android在support.v4包中為我們引入兩個重要的接口:
- NestedScrollingParent
- NestedScrollingChild
有了上面這兩個類焕毫,我們就可以實現(xiàn)“NestedScrolling(嵌套滾動)”的無縫銜接蹲坷。
二驶乾、實現(xiàn)
上述效果圖,分為三個部分:頂部布局(ImageView)循签,中間的“Sticky View”(TextView)和底部的列表(RecyclerView)级乐。
RecyclerView已經(jīng)實現(xiàn)了NestedScrollingChild接口,所以本文的重點是實現(xiàn)NestedScrollingParent接口县匠。
(1) 布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.hiphonezhu.nestedscrolling.StickyLayout
android:id="@+id/stickyNavLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="centerCrop"
android:src="@drawable/bg" />
<TextView
android:id="@+id/tv_sticky"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_dark"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="Sticky View"
android:textColor="@android:color/white" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.hiphonezhu.nestedscrolling.StickyLayout>
</FrameLayout>
StickyLayout是直接繼承自LinearLayout风科,并且實現(xiàn)了NestedScrollingParent接口。
(2) 實現(xiàn)NestedScrollingParent接口
在具體實現(xiàn)之前乞旦,我們先看下這個接口的幾個方法贼穆。
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
我們需要重點關(guān)注下面幾個方法
onStartNestedScroll該方法返回true,代表當(dāng)前ViewGroup能接受內(nèi)部View的滑動參數(shù)(這個內(nèi)部View不一定是直接子View)兰粉,一般情況下建議直接返回true故痊,當(dāng)然你可以根據(jù)nestedScrollAxes:判斷垂直或水平方向才返回true。
-
onNestedPreScroll該方法會傳入內(nèi)部View移動的dx與dy玖姑,當(dāng)前ViewGroup可以消耗掉一定的dx與dy愕秫,然后通過最后一個參數(shù)consumed傳回給子View。例如焰络,當(dāng)前ViewGroup消耗掉一半dx與dy
scrollBy(dx/2, dy/2); consumed[0] = dx/2; consumed[1] = dy/2;
onNestedPreFling你可以捕獲對內(nèi)部View的fling事件戴甩,返回true表示攔截掉內(nèi)部View的事件
我們看下具體的代碼實現(xiàn)(僅是關(guān)鍵代碼):
public class StickyLayout extends LinearLayout implements NestedScrollingParent {
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
{
return true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
{
// dy > 0表示子View向上滑動;
// 子View向上滑動且父View的偏移量<ImageView高度
boolean hiddenTop = dy > 0 && getScrollY() < maxScrollY;
// 子View向下滑動(說明此時父View已經(jīng)往上偏移了)且父View還在屏幕外面, 另外內(nèi)部View不能在垂直方向往下移動了
/**
* ViewCompat.canScrollVertically(view, int)
* 負(fù)數(shù): 頂部是否可以滾動(官方描述: 能否往上滾動, 不太準(zhǔn)確吧~)
* 正數(shù): 底部是否可以滾動
*/
boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);
if (hiddenTop || showTop)
{
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
{
if (velocityY > 0 && getScrollY() < maxScrollY) // 向上滑動, 且當(dāng)前View還沒滑到頂
{
fling((int) velocityY, maxScrollY);
return true;
}
else if (velocityY < 0 && getScrollY() > 0) // 向下滑動, 且當(dāng)前View部分在屏幕外
{
fling((int) velocityY, 0);
return true;
}
return false;
}
}
- onNestedPreScroll中,判斷子View上滑(
dy>0
)并且StickyLayout
滾動到屏幕外的距離(getScrollY()
)< 最大滾動距離maxScrollY
闪彼,則隱藏頂部布局(ImageView
)甜孤;同理,如果子View下滑(dy < 0
)且StickyLayout
還在屏幕外面(getScrollY() > 0
)备蚓,同時內(nèi)部View不能在垂直方向往下移動了(可以借助ViewCompat.canScrollVertically
來實現(xiàn))课蔬。
ViewCompat.canScrollVertically(view, int) ,第二個int類型參數(shù)
負(fù)數(shù): 頂部是否可以往下滾動
正數(shù): 底部是否可以往上滾動
官方描述:“Negative to check scrolling up, positive to check scrolling down”郊尝,我覺得有誤人子弟的嫌疑二跋。
- onNestedPreFling中,如果向上滑動(
velocityY > 0
)且ImageView
沒有完全隱藏(getScrollY() < maxScrollY
)流昏,則使用fling方法扎即,“嘗試”(因為滑動距離取決于初始速度)將ImageView
完全隱藏;同理况凉,如果向下滑動(velocityY < 0
)且ImageView
部分在屏幕外(getScrollY() > 0
)谚鄙,則使用fling方法,“嘗試”(因為滑動距離取決于初始速度)將ImageView
完全顯示刁绒。
對于fling方法闷营,我們使用OverScroller的fling方法,另外邊界檢測,重寫了scrollTo方法:
public void fling(int velocityY, int maxY)
{
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, maxY);
invalidate();
}
@Override
public void scrollTo(int x, int y)
{
if (y < 0) // 不允許向下滑動
{
y = 0;
}
if (y > maxScrollY) // 防止向上滑動距離大于最大滑動距離
{
y = maxScrollY;
}
if (y != getScrollY())
{
super.scrollTo(x, y);
}
}
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
到這里,大家發(fā)現(xiàn)其實NestedScrolling機(jī)制其實并不復(fù)雜:
在滑動的時候,內(nèi)部View會把滑動的距離(dx與dy)傳入給NestedScrollingParent俩功,NestedScrollingParent可以決定對其是否消耗,消耗的值通過consumed[]再傳回給子View规哲。
三、寫在最后
由于本文的效果ImageView和Sticky View(TextView)與“狀態(tài)欄”有融合的效果诽表,所以具體源碼會比這個略微復(fù)雜些~
主要思路是:
布局中有一個一模一樣的Sticky View(TextView)唉锌,通過隱藏和顯示它來達(dá)到最終的效果,如果你有更好的想法可以聯(lián)系我竿奏。
具體請參考源碼:NestedScrolling