Android使用Scrolling機(jī)制實現(xiàn)Tab吸頂效果
一、前言
app 首頁中經(jīng)常要實現(xiàn)首頁頭卡共享,tab 吸頂,內(nèi)容區(qū)通過 ViewPager 切換的需求,以前往往是利用事件處理來完成,還有 Google 官方也提供了相關(guān)的庫如CoordinatorLayout,但是這些也有一定的弊端和滑動方面不如意的地方,瑕疵比較明顯,實際上很多大廠的吸頂效果都是自己寫的,同樣適配起來還是比較復(fù)雜。
這里我們利用 NestedScrolling 機(jī)制來實現(xiàn)。
當(dāng)然也有很多開源項目,發(fā)現(xiàn)存在的問題很多面,主要問題如下:
- 頭部和內(nèi)容區(qū)域不聯(lián)動
- 沒有中斷 RecyclerView 的 fling 效果,導(dǎo)致 RecyclerView 搶占 ViewPager 事件
- 僅僅只支持RecyclerView,不支持?jǐn)U展
- 侵入式設(shè)計太多,反射太多。(當(dāng)然,本篇方案解決 RecyclerView 中斷 fling 時用了侵入式設(shè)計)
- 嚴(yán)重依賴Adapter、ViewHolder等。
二、效果展示

其實這個頁面中存在以下布局元素:
Head 部分是大卡片和TabLayout
Body部分使用ViewPager,然后通過ViewPager“裝載”兩個RecyclerView。
三、實現(xiàn)邏輯
3.1 布局設(shè)計的注意事項
對于實現(xiàn)布局,評價一個布局的好壞應(yīng)該從以下幾方面出發(fā)
布局規(guī)劃:提前規(guī)劃好最終的效果和布局的組成,以及要處理最大一些問題,如果處理不好,則可能出現(xiàn)做到一半無法做下去的問題。
耦合程度:應(yīng)該盡可能避免太多的耦合,比如View與View之間的直接調(diào)用,如果有,那么應(yīng)該著手從設(shè)計原則著手或者父子關(guān)系方面改良設(shè)計。
減少XML組合布局:很多自定義布局中Inflate xml布局,雖然這種也屬于自定義View,但是封裝在xml中的View很難讓你去修改屬性和樣式,設(shè)置要做大量的自定義屬性去適配。
通用性和可擴(kuò)展性:通用性是此View要做到隨處可用,即便不能也要在這個方向進(jìn)行擴(kuò)展,可擴(kuò)展性的提高可以促進(jìn)通用性。為了實現(xiàn)布局效果,一些開發(fā)者不僅僅自定義了父布局,而且還定義了各種子布局,這顯然降低了擴(kuò)展性和適用性。原則上,兩者同時定義的問題應(yīng)該在父布局中去處理,而不是從子View中去處理。
完成好于完美:對于性能和瑕疵問題,避免提前處理,除非阻礙開發(fā)。遵循“完成好于完美”的原則,先實現(xiàn)再完善,不斷循環(huán)優(yōu)化才是正確的方式。很多人自定義的時候擔(dān)心性能和瑕疵問題,導(dǎo)致無法設(shè)計出最終效果,實際上很多自定義布局的瑕疵和性能都是在完成之后優(yōu)化效果的,因此過多的提前布置,可能會讓你做大量返工處理。
下面是本篇設(shè)計過程,希望對你有幫助
3.2 主要邏輯
3.2.1 規(guī)劃布局
規(guī)劃布局是非常重要的,這里我們規(guī)劃布局為
HEAD部分和BODY兩部分,至于吸頂?shù)腡abLayout,我們放到Head部分,讓吸頂時讓Head部分top 最大移動為HEAD高度減去TabLayout的高度。BODY部分可以使用ViewPager,也可以是其他布局,因為ViewPager使用較廣,本文使用ViewPager。
<Head>
<Card></Card>
<TabLayout></TabLayout>
</Head>
<Body>
<RecyclerView1/>
....
<RecyclerViewN/>
</Body>
3.2.2 Scrolling 機(jī)制
其實在本篇之前,我們也通過Scrolling機(jī)制定義過,但要明白為什么要使用Scrolling機(jī)制?
Scrolling機(jī)制可以協(xié)同父子View、祖宗View的滑動,當(dāng)然這個范圍有點小。本篇我們要協(xié)同滑動,中間隔著ViewPager,人家可是爺孫關(guān)系。
Scrolling提供了祖宗樹上可以互相通知的View
通用性強:Scrolling是通過support或者androidx庫接入的,雖然當(dāng)前發(fā)展到第三個版本了,但是毫不影響我們升級使用。
3.2.3 主要代碼
繼承Scrolling接口
public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity; //fling 縱向速度計算
private int mHeadExpandedOffset; // tab偏移,也就是為了方便tab吸頂
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0; //互動判斷閾值
private boolean isTouchMoving = false;
private View mHeaderView = null; //抽象調(diào)用head
private View mBodyView = null; // 抽象調(diào)用body
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker; //順時力度跟蹤
//輔助當(dāng)前布局滑動類型判斷,如水平滑動還是垂直滑動以及是不是手指觸動的滑動,實現(xiàn)主要是為了兼容外部調(diào)用
///參考NestedScrollView實現(xiàn)的
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
.....
}
自定義布局參數(shù),主要是為子View添加布局屬性
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
測量
我們這里縱向排列即可
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height - overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
核心方法,縱向滑動處理
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
if (dy == 0) {
return;
}
if (!canNestedScrollView(mVerticalScrollView)) {
//這里要判斷向上滑動問題,
// 如果當(dāng)前布局可以向上滑動,優(yōu)先滑動,不然頭部可能出現(xiàn)露一半但無法向上滑動的問題
if (dy < 0) {
return;
}
if (!allowScroll(dy)) {
return;
}
}
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
核心事件處理,主要處理滑動,瞬時速度問題
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
if (mVerticalScrollView instanceof RecyclerView) {
/**
*RecyclerView 雖然繼承了NestedScrollingChild,但是沒有在stopNestedScroll中停止
*調(diào)用stopScroll,導(dǎo)致滑動狀態(tài)事件自動捕獲,造成ViewPager切換問題,這里使用stopScroll()侵入式調(diào)用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
} else if (mVerticalScrollView instanceof NestedScrollingChild) {
mVerticalScrollView.stopNestedScroll();
}
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
//這里只關(guān)注頭卡觸摸事件即可
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (isTouchMoving && !allowScroll(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset != 0) {
scrollBy(0, offset);
}
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(event);
}
四、代碼實現(xiàn)
4.1 要點
頭部不聯(lián)動問題:
我們需要處理在 dispatchTouchEvent 或者利用 onInteceptTouchEvent + onTouchEvent 處理,主要處理 VelocityTracker + fling 事件。接著我們判斷滑動開始位置是不是在頭部,因為按照布局設(shè)計,頭部和RecyclerView不一樣,頭部是隨著整體滑動,而RecyclerView是可以內(nèi)部滑動的,直到無法滑動時,我們才能讓父布局整體滑動,通過這種方式就能解決聯(lián)動問題。
RecyclerView 中斷 fling 效果問題:
RecyclerView 沒有在 stopNestedScroll () 方法中中斷滑動,因此需要通過侵入方式,調(diào)用 stopScroll () 去完成,其實我們這里希望官方提供接口終止RecyclerView停止滑動,但是事實上沒有,這個問題一定概率上造成RecyclerView減速滑動時,ViewPager也無法切換,當(dāng)然很多其他開源方案都有類似的問題。
if (mVerticalScrollView instanceof RecyclerView) {
/**
* RecyclerView 雖然繼承了NestedScrollingChild,但是沒有在stopNestedScroll中停止
* 調(diào)用stopScroll,導(dǎo)致滑動狀態(tài)事件自動捕獲,造成ViewPager切換問題,這里使用stopScroll()侵入式調(diào)用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
}
查找事件點所在的View,這里我們使用了下面方法,理論上我們不會子Head和Body部分做Matrix變換,因此Android內(nèi)部通過矩陣判斷View的逆矩陣方式我們可以不用。
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
捕獲Scrolling Child,下面方法是捕獲來自Child的滑動請求,如果沒有達(dá)到吸頂狀態(tài),應(yīng)該優(yōu)先滑動父View
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只關(guān)注垂直方向的移動
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
} else {
mVerticalScrollView = null;
}
return false;
}
4.2 主要代碼
public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity;
private int mHeadExpandedOffset;
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0;
private boolean isTouchMoving = false;
private View mHeaderView = null;
private View mBodyView = null;
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker;
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
public NestedPagerRecyclerViewLayout(@NonNull Context context) {
this(context, null);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
mHeadExpandedOffset = a.getDimensionPixelSize(R.styleable.NestedPagerRecyclerViewLayout_headExpandedOffset, 0);
a.recycle();
}
mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();
mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
setClickable(true);
}
/**
* 頭部余留偏移
*
* @param headExpandedOffset
*/
public void setHeadExpandOffset(int headExpandedOffset) {
this.mHeadExpandedOffset = headExpandedOffset;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height - overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range;
}
}
@Override
protected int computeVerticalScrollRange() {
int childCount = getChildCount();
if (childCount == 0) return super.computeVerticalScrollRange();
int range = getPaddingBottom() + getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
range += child.getHeight() + lp.bottomMargin + lp.topMargin;
}
if (range < getHeight()) {
return super.computeVerticalScrollRange();
}
return range;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHeaderView = getChildView(LayoutParams.TYPE_HEAD);
mBodyView = getChildView(LayoutParams.TYPE_BODY);
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
if (mHeaderView != null) {
LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();
mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());
childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
if (mBodyView != null) {
LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();
mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());
}
}
protected int overScrollExtent() {
return Math.max(mHeadExpandedOffset, 0);
}
private View getHeaderView() {
return mHeaderView;
}
private View getBodyView() {
return mBodyView;
}
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
private boolean hasHeader() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {
return true;
}
}
return false;
}
public View getChildView(int layoutType) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == layoutType) {
return getChildAt(i);
}
}
return null;
}
private boolean hasBody() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
return true;
}
}
return false;
}
@Override
public void addView(View child) {
assertLayoutType(child);
super.addView(child);
}
private void assertLayoutType(View child) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
assertLayoutParams(lp);
}
private void assertLayoutParams(ViewGroup.LayoutParams lp) {
if (hasHeader() && hasBody()) {
throw new IllegalStateException("header and body has already existed");
}
if (hasHeader()) {
if (!(lp instanceof LayoutParams)) {
throw new IllegalStateException("header should keep only one");
}
if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {
throw new IllegalStateException("header should keep only one");
}
}
if (hasBody()) {
if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {
throw new IllegalStateException("header should keep only one");
}
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
assertLayoutParams(params);
super.addView(child, index, params);
}
@Override
public void addView(View child, int index) {
assertLayoutType(child);
super.addView(child, index);
}
@Override
public void addView(View child, int width, int height) {
assertLayoutParams(new LinearLayout.LayoutParams(width, height));
super.addView(child, width, height);
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new LayoutParams(lp);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只關(guān)注垂直方向的移動
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
} else {
mVerticalScrollView = null;
}
return false;
}
@Override
protected int computeVerticalScrollExtent() {
int computeVerticalScrollExtent = super.computeVerticalScrollExtent();
return computeVerticalScrollExtent;
}
@Override
public int getNestedScrollAxes() {
return parentHelper.getNestedScrollAxes();
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
parentHelper.onNestedScrollAccepted(child, target, axes, type);
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
if (mVerticalScrollView == target) {
Log.d("onNestedScroll", "::::onStopNestedScroll vertical");
parentHelper.onStopNestedScroll(target, type);
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e("onNestedScroll", "::::onNestedScroll 11111");
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return;
}
if (target == null) return;
if (mVerticalScrollView != target) {
return;
}
Log.e("onNestedScroll", "::::onNestedPreScroll 00000");
handleVerticalNestedScroll(dx, dy, consumed);
}
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
if (dy == 0) {
return;
}
if (!canNestedScrollView(mVerticalScrollView)) {
//這里要判斷向上滑動問題,
// 如果當(dāng)前布局可以向上滑動,優(yōu)先滑動,不然頭部可能出現(xiàn)露一半但無法向上滑動的問題
if (dy < 0) {
return;
}
if (!allowScroll(dy)) {
return;
}
}
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
if (mVerticalScrollView instanceof RecyclerView) {
/**
*RecyclerView 雖然繼承了NestedScrollingChild,但是沒有在stopNestedScroll中停止
*調(diào)用stopScroll,導(dǎo)致滑動狀態(tài)事件自動捕獲,造成ViewPager切換問題,這里使用stopScroll()侵入式調(diào)用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
} else if (mVerticalScrollView instanceof NestedScrollingChild) {
mVerticalScrollView.stopNestedScroll();
}
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
//這里只關(guān)注頭卡觸摸事件即可
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (isTouchMoving && !allowScroll(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset != 0) {
scrollBy(0, offset);
}
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(event);
}
public boolean allowScroll(int dy) {
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return false;
}
return true;
}
private void startFling(VelocityTracker velocityTracker, int x, int y) {
int xVolecity = (int) velocityTracker.getXVelocity();
int yVolecity = (int) velocityTracker.getYVelocity();
if (mVerticalScrollView instanceof NestedScrollingChild) {
Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);
((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);
}
}
private boolean canNestedScrollView(View view) {
if (view == null) {
return false;
}
if (view instanceof RecyclerView) {
//顯示區(qū)域最上面一條信息的position
RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
if (manager == null) {
return true;
}
if (manager.getChildCount() == 0) {
return true;
}
int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();
return scrollOffset <= 0;
}
if (view instanceof NestedScrollingChild) {
return view.canScrollVertically(-1);
}
if (!(view instanceof ViewGroup) && (view instanceof View)) {
return true;
}
throw new IllegalArgumentException("不支持非NestedScrollingChild子類ViewGroup");
}
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
}
4.3 布局屬性定義
作為布局文件,增加屬性,標(biāo)記View類型
<declare-styleable name="NestedPagerRecyclerViewLayout">
<attr name="layoutScrollNestedType" format="flags">
<flag name="Head" value="0"/>
<flag name="Body" value="1"/>
</attr>
<attr name="headExpandedOffset" format="dimension|reference" />
</declare-styleable>
下面是使用時的布局demo,需要設(shè)置layoutScrollNestedType
4.4 使用
布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.smartian.widget.NestedPagerRecyclerViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/NestedScrollChildLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
app:headExpandedOffset="45dp">
<LinearLayout
android:id="@+id/head"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
app:layoutScrollNestedType="Head">
<TextView
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:background="@color/colorAccent"
android:gravity="center"
android:text="top Head" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="45dp">
<TextView
android:id="@+id/tab1"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab1" />
<View
android:layout_width="1dip"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
<TextView
android:id="@+id/tab2"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab2" />
</LinearLayout>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layoutScrollNestedType="Body" />
</com.smartian.widget.NestedPagerRecyclerViewLayout>
至此,我們的方案基本實現(xiàn)了,使用方式如下
public class MyNestedScrollViewActivity extends Activity implements View.OnClickListener {
private ViewPager viewPager;
private NestedPagerRecyclerViewLayout scrollChildLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_nested_scrolling_child_layout);
scrollChildLayout = findViewById(R.id.NestedScrollChildLayout);
scrollChildLayout.setHeadExpandOffset((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,45,getResources().getDisplayMetrics()));
viewPager = findViewById(R.id.body);
findViewById(R.id.tab1).setOnClickListener(this);
findViewById(R.id.tab2).setOnClickListener(this);
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return 2;
}
@Override
public boolean isViewFromObject(@NonNull View view, Object object) {
return view==object;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.addView((View) object);
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View layoutView = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_recycler_view, container, false);
RecyclerView recyclerView = layoutView.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext()));
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter(container.getContext(), position%2==0?getData():getData2());
recyclerView.setAdapter(adapter);
container.addView(layoutView);
return layoutView;
}
});
}
private List<String> getData() {
List<String> data = new ArrayList<>();
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
data.add("#ff99bb");
data.add("#ff77dd");
data.add("#ff33bb");
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
return data;
}
private List<String> getData2() {
List<String> data = new ArrayList<>();
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
data.add("#99bbff");
data.add("#77ddff");
data.add("#33bbff");
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
return data;
}
@Override
public void onClick(View v) {
int id = v.getId();
if(id==R.id.tab1){
viewPager.setCurrentItem(0,true);
}else if(id==R.id.tab2){
viewPager.setCurrentItem(1,true);
}
}
}
五、總結(jié)
ViewPager、RecyclerView 和Tab吸頂效果實現(xiàn)有一定的難度,其實也有很多實現(xiàn),但是通用性和易用性都有些問題,因此,即便的是最完美的方案也需要經(jīng)常調(diào)整,因此這類效果很難作為庫的方式輸出,通過本篇的文章,其實提供了一個現(xiàn)成的模板。
以上就是Android使用Scrolling機(jī)制實現(xiàn)Tab吸頂效果的詳細(xì)內(nèi)容,更多關(guān)于Android Scrolling吸頂?shù)馁Y料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Android Material設(shè)計中陰影效果的實現(xiàn)方法
這篇文章主要介紹了Android Material設(shè)計中陰影效果的實現(xiàn)方法,包括自定義陰影的輪廓和裁剪等,需要的朋友可以參考下2016-04-04
Android自定義谷歌風(fēng)格ProgressBar
這篇文章主要為大家詳細(xì)介紹了Android自定義谷歌風(fēng)格ProgressBar的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02
Android Studio導(dǎo)入項目非常慢的解決方法
這篇文章主要為大家詳細(xì)介紹了Android Studio導(dǎo)入項目非常慢的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
Android AIDL實現(xiàn)與服務(wù)相互調(diào)用方式
這篇文章主要介紹了Android AIDL實現(xiàn)與服務(wù)相互調(diào)用方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03

