知乎里的一些特效實現 無縫拖拽的 Layout

收藏待读

知乎里的一些特效實現 無縫拖拽的 Layout

1

前言

今年年初接觸回答頁面改版,由之前的左右滑動回答改為上下滑動回答,由於當時回答頁的代碼太過於龐大,所以第一次改版復用了之前的 UI 框架,外層 ViewPager + Fragment,內層是 WebView 嵌套 Hybrid 頁面。

問題出現了,WebView 可以滾動的時候,會持有整個 Touch 事件流程,導致當 webView 拖拽到底部,手指不脫離屏幕繼續拖拽的時候,無法將當前的拖拽操作給翻頁器,產生體驗上的割裂感。

接下來就是 UI 交互優化的歷程。

2

調研

1.NestedScrolling:

Support V4 提供了一套 API 來支持嵌入的滑動效果。NestedScrolling 提供了一套父 View 和子 View 滑動交互機制。要完成這樣的交互,父 View 需要實現 NestedScrollingParent 接口,而子 View 需要實現 NestedScrollingChild 接口。

作為一個可以嵌入 NestedScrollingChild 的父 View,需要實現 NestedScrollingParent,這個接口方法和 NestedScrollingChild 大致有一一對應的關係。同樣,也有一個 NestedScrollingParentHelper 輔助類來默默的幫助你實現和 Child 交互的邏輯。滑動動作是 Child 主動發起,Parent 就收滑動回調並作出響應。

從上面的 Child 分析可知,滑動開始的調用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回調,決定是否需要配合 Child 一起進行處理滑動,如果需要配合,還會回調 onNestedScrollAccepted()。

每次滑動前,Child 先詢問 Parent 是否需要滑動,即 dispatchNestedPreScroll(),這就回調到 Parent 的 onNestedPreScroll(),Parent 可以在這個回調中「劫持」掉 Child 的滑動,也就是先於 Child 滑動。

Child 滑動以後,會調用 onNestedScroll(),回調到 Parent 的 onNestedScroll(),這裡就是 Child 滑動後,剩下的給 Parent 處理,也就是 後於 Child 滑動。

最後,滑動結束,調用 onStopNestedScroll() 表示本次處理結束。

這個方案其實很不錯,但最後被 pass 了,因為由於工程的原因,我們的 webview 是被包裹起來的,不可以任意去繼承 NestedScrollingChild 並做定製修改。

2.自定義 ViewGroup

其實目前的問題是當子 View scroll 到頂部或者底部的時候,無法將 Touch 事件流交還給父布局。

因此這裡我採用的思路是通過我的 ViewGroup 去統一 dispatchTouchEvent 給我的子 View,條件就是,假如子 View 可以滾動,我就會構造一套完整的 touch 時間流分發給他。否則我會自己消化。

解決方案

step 1:

通過第二種方式的思路,我們第一步需要在我的 ViewGroup 攔截所有的 Touch 事件。所以…

@Override
publicbooleanonInterceptTouchEvent(MotionEvent ev){
  if(isParentDispatchTouchEvent) {
  returntrue;
}else{
returnsuper.onInterceptTouchEvent(ev);
}
}

上來我們就攔截出所有的 Touch 事件。

step2:

開始在 ViewGroup 的 onTouchEvent 處理所有相關的 Event。

// 1.初始記錄 Touch 坐標
intmDownY =event.getY();
intdeltaY =0;
// 2.默認子 View 持有事件流起始點 isHoldTouch = true,通過 isChildCanScroll 來判別當前子 View 是否可以滾動。
if(isHoldTouch && !isChildCanScroll(event, deltaY) && deltaY !=0) {
// 3.假如子 View 不可以滾動,當前 ViewGroup 需要阻斷 Touch 的下發,為了遵循 Touch 事件流的規範,當被外部阻斷時,需要對其下發 ACTION_CANEL。同時 isHoldTouch = false。
isHoldTouch =false;
MotionEvent cancelEvent = MotionEvent.obtain(event);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
getChildAt(0).dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
// 5.假如當我們在 ViewGroup 滾動過程中,滑動到了子 View 可滾動的狀態,這時候會將 ViewGroup 調整至滾動初始位置,然後對子 View 做一個 ACTION_DOWN 的操作,從而開始陸續分發子 View Touch 事件。同時 isHoldTouch = true。
if(!isHoldTouch && isChildCanScroll(event, deltaY) && deltaY !=0) {
setSheetTranslation(maxSheetTranslation);
isHoldTouch =true;
if(event.getAction() == MotionEvent.ACTION_MOVE) {
MotionEvent downEvent = MotionEvent.obtain(event);
downEvent.setAction(MotionEvent.ACTION_DOWN);
getChildAt(0).dispatchTouchEvent(downEvent);
downEvent.recycle();
}
}
if(isHoldTouch && deltaY !=0) {
// 6.當前判斷子 View 已經處於可分發 Touch 狀態,會陸續將 ACTION_MOVE 分發給他。從而實現子 View 的滾動。
event.offsetLocation(0, mSheetTranslation - mTouchParentViewOriginMeasureHeight);
getChildAt(0).dispatchTouchEvent(event);
}else{
// 4.當上面阻斷完 Touch 的下發以後,這裡我們開始自己消化 Touch 事件,也就是這裡會做一個 TranslationY 修改,從而達到 ViewGroup 做 Y軸方向的偏移.
setSheetTranslation(newSheetTranslation);
if(event.getAction() == MotionEvent.ACTION_UP ||event.getAction() == MotionEvent.ACTION_CANCEL) {
// 7.為了將這個結束事件後面分發給子 View
isHoldTouch =true;
}
}

step3:

判斷子 View 是否可以滾動

/**
* child can scroll
*@paramview
*@paramx
*@paramy
*@paramlockRect 是否開啟 允許 touch 脫離當前子 View 區域繼續生效。
*@return
*/
protectedbooleancanScrollUp(View view,floatx,floaty,booleanlockRect){
if(viewinstanceofWebView) {
returncanWebViewScrollUp();
}
if(viewinstanceofViewGroup) {
ViewGroup vg = (ViewGroup) view;
for(inti =0; i  childLeft && x  childTop && y 0&&
((CoordinatorLayout) view).getChildAt(0)instanceofAppBarLayout) {
AppBarLayout layout = (AppBarLayout) ((CoordinatorLayout) view).getChildAt(0);
OnNestOffsetChangedListener listener = mOnOffsetChangedListener.get(layout.hashCode());
if(listener !=null) {
if(listener.getOffsetY() 0) {
returntrue;
}
}
}
returnview.canScrollVertically(-1);
}
/**
* child can scroll
*@paramview
*@paramx
*@paramy
*@paramlockRect 是否開啟 允許 touch 脫離當前子 View 區域繼續生效。
*@return
*/
protectedbooleancanScrollDown(View view,floatx,floaty,booleanlockRect){
if(viewinstanceofWebView) {
returncanWebViewScrollDown();
}
if(viewinstanceofViewGroup) {
ViewGroup vg = (ViewGroup) view;
for(inti =0; i  childLeft && x  childTop && y 0&&
((CoordinatorLayout) view).getChildAt(0)instanceofAppBarLayout) {
AppBarLayout layout = (AppBarLayout) ((CoordinatorLayout) view).getChildAt(0);
OnNestOffsetChangedListener listener = mOnOffsetChangedListener.get(layout.hashCode());
if(listener !=null) {
if(listener.getOffsetY() 0) {
returntrue;
}
}
}
returnview.canScrollVertically(1);
}

這部分核心在於遞歸查詢當前 MotionEvent 當前坐標下的所有子 View 有沒有可以滾動的 View,從而根據 view.canScrollVertically 來進行判斷。

優勢

1.支持絕大多數的 View 嵌套滾動。

2.成本低,只需要在你想要嵌套滾動的 View 上麵包一層這個 Layout。

功能

1.支持嵌套滾動,無縫拖拽

2.支持 BottomSheet (使用方法詳見下方)

3.支持 Appbarlayout

3.支持拖拽阻尼 (使用方法詳見下方)

效果

知乎里的一些特效實現 無縫拖拽的 Layout

normal

知乎里的一些特效實現 無縫拖拽的 Layout

view+recyclerview

知乎里的一些特效實現 無縫拖拽的 Layout

webview + recyclerview

知乎里的一些特效實現 無縫拖拽的 Layout

question

篇幅原因,部分效果圖未展示。

3

使用示例

normal use

android:id="@+id/wrapper"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/container_rv"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#fff"
android:overScrollMode="always">
// 設置手指下拉阻尼
mNestedTouchScrollingLayout.setDampingDown(2.0f/5);
// 設置手指上拉阻尼
mNestedTouchScrollingLayout.setDampingUp(3.0f/5);
mNestedTouchScrollingLayout.registerNestScrollChildCallback(newNestedTouchScrollingLayout.INestChildScrollChange() {
// 當前 Layout 偏移距離
@Override
publicvoidonNestChildScrollChange(floatdeltaY){
}
// finger 脫離屏幕 Layout 偏移量,以及當前 Layout 的速度
@Override
publicvoidonNestChildScrollRelease(finalfloatdeltaY,finalintvelocityY){
mNestedTouchScrollingLayout.recover(0,newRunnable() {
@Override
publicvoidrun(){
Log.i("NestedTouchScrollingLayout ---> ","deltaY : "+ deltaY +" velocityY : "+ velocityY);
}
});
}
// 手指抬起時機
@Override
publicvoidonFingerUp(floatvelocityY){
}
// 橫向拖拽
@Override
publicvoidonNestChildHorizationScroll(MotionEvent event,floatdeltaX,floatdeltaY){
}
});

bottomsheet use

android:id="@+id/wrapper"
android:layout_marginTop="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:background="#fff"
android:id="@+id/container_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
// 臨界速度,根據業務而定
publicstaticintmVelocityYBound =1300;
// 規定 sheetView 彈起方向
mNestedTouchScrollingLayout.setSheetDirection(NestedTouchScrollingLayout.SheetDirection.BOTTOM);
mNestedTouchScrollingLayout.registerNestScrollChildCallback(newNestedTouchScrollingLayout.INestChildScrollChange() {
@Override
publicvoidonNestChildScrollChange(floatdeltaY){
}
@Override
publicvoidonNestChildScrollRelease(finalfloatdeltaY,finalintvelocityY){
inttotalYRange = mNestedTouchScrollingLayout.getMeasuredHeight();
inthelfLimit = (totalYRange - DisplayUtils.dpToPixel(BottomSheetActivity.this,400)) /2;
inthideLimit = totalYRange - DisplayUtils.dpToPixel(BottomSheetActivity.this,400) /2;
inthelfHeight = totalYRange - DisplayUtils.dpToPixel(BottomSheetActivity.this,400);
if(velocityY > mVelocityYBound && velocityY >0) {
if(Math.abs(deltaY) > helfHeight) {
mNestedTouchScrollingLayout.hiden();
}else{
mNestedTouchScrollingLayout.peek(mNestedTouchScrollingLayout.getMeasuredHeight() - DisplayUtils.dpToPixel(BottomSheetActivity.this,400));
}
}elseif(velocityY < -mVelocityYBound && velocityY <0) {
if(Math.abs(deltaY)  hideLimit) {
mNestedTouchScrollingLayout.hiden();
}elseif(Math.abs(deltaY) > helfLimit) {
mNestedTouchScrollingLayout.peek(mNestedTouchScrollingLayout.getMeasuredHeight() - DisplayUtils.dpToPixel(BottomSheetActivity.this,400));
}else{
mNestedTouchScrollingLayout.expand();
}
}
}
@Override
publicvoidonFingerUp(floatvelocityY){
}
@Override
publicvoidonNestChildHorizationScroll(MotionEvent event,floatdeltaX,floatdeltaY){
}
});

引入

方式 1:

repositories {
// ...
maven { url"https://jitpack.io"}
}
dependencies {
implementation'com.github.JarvisGG:NestedTouchScrollingLayout:v1.2.0'
}

方式 2:

repositories {
// ...
jcenter()
}
dependencies {
implementation'com.jarvis.library.NestedTouchScrollingLayout:library:1.2.0'
}

源碼:

https://github.com/JarvisGG/NestedTouchScrollingLayout

有需要安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、混合式開發(ReactNative+Weex)和一線互聯網公司關於android面試的題目匯總可以加:936332305 / 鏈接:點擊鏈接加入【安卓開發架構】:https://jq.qq.com/?_wv=1027&k=515xp64

知乎里的一些特效實現 無縫拖拽的 Layout

原文 : 簡書

相關閱讀

免责声明:本文内容来源于簡書,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。