當(dāng)CoordinatorLayout中嵌套AppBarLayout 時墩剖,如果AppBarLayout 過大時掏婶。在滑動過程中管怠,AppBarLayout 會產(chǎn)生一個fling的動作棍现,如果這個時候向下滑動AppBarLayout 那么AppBarLayout 會同時具有一個向上和向下的fling狀態(tài)淮椰,那么就會出現(xiàn)抖動
問題現(xiàn)象:
- 用手指輕輕滑動CoordinatorLayout部分, 上滑, 快速抬起手指, 形成一個fling操作角塑。其實就是向上滑動一下蔫磨!
- 這時, 整個CoordinatorLayout部分會向上移動(fling),在停止移動之前圃伶,在下面的區(qū)域(也就是xml布局中的include_recycler_view)來一個反向的滑動(fling) , 這時整個頁面就會開始或大或小的抖動, 非常明顯堤如。
問題分析:
- CoordinatorLayout向上fling滾動無法被外部中斷
- CoordinatorLayout和子View的聯(lián)動時通過CoordinatorLayout.Behavior實現(xiàn)的蒲列,AppBarLayout使用的Behavior繼承了HeaderBehavior<AppBarLayout>。
- 問題就在這里煤惩。HeaderBehavior的onTouchEvent中使用Scroller實現(xiàn)了fling操作嫉嘀,但是沒有通過NestedScrolling API對外開放,也就說一旦HeaderBehavior的fling動作形成魄揉,無法由外部主動中斷剪侮。
- RecyclerView向下fling滾動
- 與AppBarLayout同層級的RecyclerView可以通過升級過的NestedScrolling API對AppBarLayout產(chǎn)生影響,比如RecyclerView向下fling時滑動到item 0之后洛退,如果AppBarLayout可以滑動時會給AppBarLayout施加一個同樣向下的fling動作,以此形成一個連貫的下滑fling瓣俯。
- 那么問題來了。當(dāng)HeaderBehavior產(chǎn)生的向上的fling沒有結(jié)束時兵怯,RecyclerView又送來向下的fling彩匕,抖動就產(chǎn)生了。
- 分析一下HeaderBehavior
- 當(dāng)檢測到down事件時, 取消了mScroller的運行(如果它正在scroll的話)媒区。這里因為要訪問父類(其實是父類的父類)的 mScroller變量驼仪。
- 然后通過反射拿到mScroller變量,在onInterceptTouchEvent攔截事件中袜漩,當(dāng)手指離開的時候绪爸,則停止overScroller動畫效果。
- 然后通過反射拿到flingRunnable變量宙攻,在onInterceptTouchEvent攔截事件中奠货,當(dāng)手指離開的時候,則需要remove所有的flingRunnable座掘。
AppBarLayout.Behavio說明
AppBarLayout簡單說明
- AppBarLayout是一個vertical的LinearLayout递惋,實現(xiàn)了很多material的概念,主要是跟滑動相關(guān)的溢陪。AppBarLayout的子view需要提供layout_scrollFlags參數(shù)萍虽。AppBarLayout和CoordinatorLayout強相關(guān),一般作為CoordinatorLayout的子類形真,配套使用贩挣。 按我的理解,AppBarLayout內(nèi)部有2種view没酣,一種可滑出(屏幕)王财,另一種不可滑出,根據(jù)app:layout_scrollFlags區(qū)分裕便。一般上邊放可滑出的下邊放不可滑出的绒净。
AppBarLayout.Behavior部分方法說明
- onInterceptTouchEvent():是否攔截觸摸事件
- onTouchEvent():處理觸摸事件
- layoutDependsOn():確定使用Behavior的View要依賴的View的類型
- onDependentViewChanged():當(dāng)被依賴的View狀態(tài)改變時回調(diào)
- onDependentViewRemoved():當(dāng)被依賴的View移除時回調(diào)
- onMeasureChild():測量使用Behavior的View尺寸
- onLayoutChild():確定使用Behavior的View位置
- onStartNestedScroll():嵌套滑動開始(ACTION_DOWN),確定Behavior是否要監(jiān)聽此次事件
- onStopNestedScroll():嵌套滑動結(jié)束(ACTION_UP或ACTION_CANCEL)
- onNestedScroll():嵌套滑動進行中偿衰,要監(jiān)聽的子 View的滑動事件已經(jīng)被消費
- onNestedPreScroll():嵌套滑動進行中挂疆,要監(jiān)聽的子 View將要滑動改览,滑動事件即將被消費(但最終被誰消費,可以通過代碼控制)
- onNestedFling():要監(jiān)聽的子 View在快速滑動中
- onNestedPreFling():要監(jiān)聽的子View即將快速滑動
源碼
需要注意在在support包中27版本及以下和28版本及以上變量名稱不同缤言。27版本及以下mFlingRunnable宝当,但是在28版本及以上flingRunnable,27版本及以下mScroller胆萧,但是在28版本及以上scroller庆揩,
public class FixAppBarLayoutBehavior extends AppBarLayout.Behavior {
private static final String TAG = "FixBehavior";
private static final int TYPE_FLING = 1;
private boolean isFlinging;
private boolean shouldBlockNestedScroll;
public FixAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
LogUtil.d(TAG, "onInterceptTouchEvent:" + child.getTotalScrollRange());
shouldBlockNestedScroll = isFlinging;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//手指觸摸屏幕的時候停止fling事件
stopAppbarLayoutFling(child);
break;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
/**
* 反射獲取私有的flingRunnable 屬性,考慮support 28以后變量名修改的問題
* @return Field
*/
private Field getFlingRunnableField() throws NoSuchFieldException {
Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {
headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("mFlingRunnable");
}else {
return null;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
// 可能是28及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("flingRunnable");
} else {
return null;
}
}
}
/**
* 反射獲取私有的scroller 屬性跌穗,考慮support 28以后變量名修改的問題
* @return Field
*/
private Field getScrollerField() throws NoSuchFieldException {
Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {
headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("mScroller");
}else {
return null;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
// 可能是28及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("scroller");
}else {
return null;
}
}
}
/**
* 停止appbarLayout的fling事件
* @param appBarLayout
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
//通過反射拿到HeaderBehavior中的flingRunnable變量
try {
Field flingRunnableField = getFlingRunnableField();
Runnable flingRunnable;
if (flingRunnableField != null) {
flingRunnableField.setAccessible(true);
flingRunnable = (Runnable) flingRunnableField.get(this);
if (flingRunnable != null) {
LogUtil.d(TAG, "存在flingRunnable");
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
}
Field scrollerField = getScrollerField();
if (scrollerField != null) {
scrollerField.setAccessible(true);
OverScroller overScroller = (OverScroller) scrollerField.get(this);
if (overScroller != null && !overScroller.isFinished()) {
overScroller.abortAnimation();
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target,
int nestedScrollAxes, int type) {
LogUtil.d(TAG, "onStartNestedScroll");
stopAppbarLayoutFling(child);
return super.onStartNestedScroll(parent, child, directTargetChild, target,
nestedScrollAxes, type);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
AppBarLayout child, View target,
int dx, int dy, int[] consumed, int type) {
LogUtil.d(TAG, "onNestedPreScroll:" + child.getTotalScrollRange()
+ " ,dx:" + dx + " ,dy:" + dy + " ,type:" + type);
//type返回1時订晌,表示當(dāng)前target處于非touch的滑動,
//該bug的引起是因為appbar在滑動時蚌吸,CoordinatorLayout內(nèi)的實現(xiàn)NestedScrollingChild2接口的滑動
//子類還未結(jié)束其自身的fling
//所以這里監(jiān)聽子類的非touch時的滑動锈拨,然后block掉滑動事件傳遞給AppBarLayout
if (type == TYPE_FLING) {
isFlinging = true;
}
if (!shouldBlockNestedScroll) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed, int
dxUnconsumed, int dyUnconsumed, int type) {
LogUtil.d(TAG, "onNestedScroll: target:" + target.getClass() + " ,"
+ child.getTotalScrollRange() + " ,dxConsumed:"
+ dxConsumed + " ,dyConsumed:" + dyConsumed + " " + ",type:" + type);
if (!shouldBlockNestedScroll) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
View target, int type) {
LogUtil.d(TAG, "onStopNestedScroll");
super.onStopNestedScroll(coordinatorLayout, abl, target, type);
isFlinging = false;
shouldBlockNestedScroll = false;
}
private static class LogUtil{
static void d(String tag, String string){
Log.i(tag,string);
}
}
}
然后將上次Behavior 賦值給對應(yīng)的AppBarLayout例如:
<android.support.design.widget.CoordinatorLayout 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">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
app:layout_behavior="****.FixAppBarLayoutBehavior"http://重點*****
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:title="">
...伸縮標(biāo)題內(nèi)容
</android.support.design.widget.CollapsingToolbarLayout>
......置頂標(biāo)題
</android.support.design.widget.AppBarLayout>
.....界面內(nèi)容content
</android.support.design.widget.CoordinatorLayout>