對於電商App,商品詳情無疑是很重要的一個模塊,觀察主流購物App的詳情界面,發現大部分都是作成了上下兩部分,上面展現商品規格信息,下面是H5商品詳情,或者是嵌套了一個包含H5詳情及評論列表的ViewPager界面,本文就是實現了一個兼容不一樣需求的上下滾動黏滯View-DragScrollDetailsLayout。git
DragScrollDetailsLayout GitHub連接 github
首先看一下實現效果圖web
固然,若是將Webview替換成其餘的ListView之類的也是支持的。app
適用場景:底部須要添加多個界面,而且須要滑動ide
適用場景:底部須要添加多個界面,可是不須要滑動函數
對於這個需求的場景,很容易想到能夠分紅上下兩部分來實現,只須要一個Vertical的LinearLayout,其他的就是處理滾動及動畫的問題,首先自定義ViewGroup內部先聲明兩個頂層子ViewmUpstairsView、 View mDownstairsView,而且採用一個變量CurrentTargetIndex標記當前處於操做那個View,佈局
public class DragScrollDetailsLayout extends LinearLayout { private View mUpstairsView; private View mDownstairsView; private View mCurrentTargetView; public enum CurrentTargetIndex { UPSTAIRS, DOWNSTAIRS; public static CurrentTargetIndex valueOf(int index) { return 1 == index ? DOWNSTAIRS : UPSTAIRS; } }
而後集中處理滾動事件,對於滾動與動畫主要有以下幾個問題須要解決:post
如何知道上面或者下面的View已經滾動的到頂部或者底部動畫
滾動到邊界時,如何攔截處理滑動spa
鬆手後如何處理後續的動效
首先來看第一個問題,如何知道上面或者下面的View滾動到了邊界,其實Android源碼中有個類ViewCompat,它有個函數canScrollVertically(View view, int offSet, MotionEvent ev)就能夠判斷當前View是否能夠向哪一個方向滾動,offset的正負值用來判斷向上仍是向下,固然,僅僅靠這個函數仍是不夠的,由於ViewGroup是能夠相互嵌套的,也許ViewGroup自己不能滾動,可是其內部的子View卻能夠滾動,這時候,就須要遞歸遍歷相關的View,好比對於ViewPager中嵌套了包含WebView或者List的Fragment。不過,並不是全部的子View都須要遍歷,只有與TouchEvent相關的View才須要判斷。所以還須要寫個函數判斷View是否在TouchEvent所在的區域,以下函數isTransformedTouchPointInView:
/*** * 判斷MotionEvent是否處於View上面 */ protected boolean isTransformedTouchPointInView(MotionEvent ev, View view) { float x = ev.getRawX(); float y = ev.getRawY(); int[] rect = new int[2]; view.getLocationInWindow(rect); float localX = x - rect[0]; float localY = y - rect[1]; return localX >= 0 && localX < (view.getRight() - view.getLeft()) && localY >= 0 && localY < (view.getBottom() - view.getTop()); }
以後咱們能夠利用該函數對View進行遞歸遍歷,判斷最上層的ViewGroup是否能夠上下滑動
private boolean canScrollVertically(View view, int offSet, MotionEvent ev) { if (!mChildHasScrolled && !isTransformedTouchPointInView(ev, view)) { return false; } if (ViewCompat.canScrollVertically(view, offSet)) { mChildHasScrolled = true; return true; } if (view instanceof ViewPager) { return canViewPagerScrollVertically((ViewPager) view, offSet, ev); } if (view instanceof ViewGroup) { ViewGroup vGroup = (ViewGroup) view; for (int i = 0; i < vGroup.getChildCount(); i++) { if (canScrollVertically(vGroup.getChildAt(i), offSet, ev)) { mChildHasScrolled = true; return true; } } } return false; }
知道View是否能夠上下滑動到邊界後,攔截事件的時機就比較清晰了,那麼接着看第二個問題,如何攔截滑動。
onInterceptTouchEvent在返回True以後,就不會再執行了,咱們只須要把握準確的攔截時機,好比若是處於上面的View,就要對上拉事件比較敏感,處於底部就要對下拉事件敏感,同時還要將無效的手勢歸零,好比,操做上面的View時,若是先是下拉,而且是無效的下拉,那麼就要將攔截點重置。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: mDownMotionX = ev.getX(); mDownMotionY = ev.getY(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.clear(); mChildHasScrolled=false; break; case MotionEvent.ACTION_MOVE: adjustValidDownPoint(ev); return checkCanInterceptTouchEvent(ev); default: break; } return false; }
checkCanInterceptTouchEvent主要用來判斷是否須要攔截,並不是不可滾動,就須要攔截事件,不可滾動只是一個必要條件而已,
private boolean checkCanInterceptTouchEvent(MotionEvent ev) { final float xDiff = ev.getX() - mDownMotionX; final float yDiff = ev.getY() - mDownMotionY; if (!canChildScrollVertically((int) yDiff,ev)) { mInitialInterceptY = (int) ev.getY(); if (Math.abs(yDiff) > mTouchSlop && Math.abs(yDiff) >= Math.abs(xDiff) && !(mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS && yDiff > 0 || mCurrentViewIndex == CurrentTargetIndex.DOWNSTAIRS && yDiff < 0)) { return true; } } return false; }
事件攔截以後,就是對Move事件進行處理
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: flingToFinishScroll(); recycleVelocityTracker(); break; case MotionEvent.ACTION_MOVE: scroll(ev); break; default: break; } return true; }
滾動比較簡單,直接調用scrollTo就能夠,同時爲了收集滾動速度,還能夠用VelocityTracker作一下記錄:
private void scroll(MotionEvent event) { if (mCurrentViewIndex == CurrentTargetIndex.UPSTAIRS) { if (getScrollY() <= 0 && event.getY() > mInitialInterceptY) { mInitialInterceptY = (int) event.getY(); } scrollTo(0, (int) (mInitialInterceptY - event.getY())); } else { if (getScrollY() >= mUpstairsView.getMeasuredHeight() && event.getY() < mInitialInterceptY) { mInitialInterceptY = (int) event.getY(); } scrollTo(0, (int) (mInitialInterceptY - event.getY() + mUpstairsView.getMeasuredHeight())); } mVelocityTracker.addMovement(event); }
在Up事件以後,還要簡單的處理一下一下收尾的滾動動畫,好比,滾動距離不夠要復原,不然,就滾動到目標視圖,這裏主要是根據Up事件的位置,計算須要滾動的距離,並經過Scroller來完成剩下的滾動。
private void flingToFinishScroll() { final int pHeight = mUpstairsView.getMeasuredHeight(); final int threshold = (int) (pHeight * mPercent); float scrollY = getScrollY(); if (CurrentTargetIndex.UPSTAIRS == mCurrentViewIndex) { if (scrollY <= 0) { scrollY = 0; } else if (scrollY <= threshold) { if (needFlingToToggleView()) { scrollY = pHeight - getScrollY(); mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS; } else scrollY = -getScrollY(); } else { scrollY = pHeight - getScrollY(); mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS; } } else if (CurrentTargetIndex.DOWNSTAIRS == mCurrentViewIndex) { if (pHeight - scrollY <= threshold) { if (needFlingToToggleView()) { scrollY = -getScrollY(); mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS; } else scrollY = pHeight - scrollY; } else if (scrollY < pHeight) { scrollY = -getScrollY(); mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS; } } mScroller.startScroll(0, getScrollY(), 0, (int) scrollY, mDuration); if (mOnSlideDetailsListener != null) { mOnSlideDetailsListener.onStatueChanged(mCurrentViewIndex); } postInvalidate(); }
以上就是經常使用商品詳情黏滯佈局的實現。最後附上GitHub連接 歡迎 star DragScrollDetailsLayout GitHub連接