仿淘寶、京東拖拽商品詳情(可巢狀ViewPager、ListView、WebView、FragmentTabhost)

看書的小蝸牛發表於2017-01-13

對於電商App,商品詳情無疑是很重要的一個模組,觀察主流購物App的詳情介面,發現大部分都是做成了上下兩部分,上面展示商品規格資訊,下面是H5商品詳情,或者是巢狀了一個包含H5詳情及評論列表的ViewPager介面,本文就是實現了一個相容不同需求的上下滾動黏滯View控制元件。GitHub連結DragScrollDetailsLayout

實現效果圖

首先看一下實現效果圖

簡單的ScrollView+Webview

當然,如果將Webview替換成其他的ListView之類的也是支援的。

仿淘寶、京東拖拽商品詳情(可巢狀ViewPager、ListView、WebView、FragmentTabhost)
scrollview+webview.gif

ScrollView+ViewPager

適用場景:底部需要新增多個介面,並且需要滑動

仿淘寶、京東拖拽商品詳情(可巢狀ViewPager、ListView、WebView、FragmentTabhost)
scrollview+viewpager.gif

ScrollView+Fragmenttabhost

適用場景:底部需要新增多個介面,但是不需要滑動

仿淘寶、京東拖拽商品詳情(可巢狀ViewPager、ListView、WebView、FragmentTabhost)
scrollview+fragmenttabhost.gif

實現

對於這個需求的場景,很容易想到可以分成上下兩部分來實現,只需要一個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;
            }
        }複製程式碼

然後集中處理滾動事件,對於滾動與動畫主要有如下幾個問題需要解決:

  • 如何知道上面或者下面的View已經滾動的到頂部或者底部
  • 滾動到邊界時,如何攔截處理滑動
  • 鬆手後如何處理後續的動效

如何判斷滾動邊界

首先來看第一個問題,如何知道上面或者下面的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();
        }
        int distance = mInitialInterceptY - event.getY() >= 0 ? (int) (mInitialInterceptY - event.getY()) : 0;
        scrollTo(0, distance);
    } else {
        if (getScrollY() >= mUpstairsView.getMeasuredHeight() && event.getY() <= mInitialInterceptY) {
            mInitialInterceptY = (int) event.getY();
        }
        int distance = event.getY() <= mInitialInterceptY ? mUpstairsView.getMeasuredHeight()
                : (int) (mInitialInterceptY - event.getY() + mUpstairsView.getMeasuredHeight());
        scrollTo(0, distance);
    }
    mVelocityTracker.addMovement(event);
}  複製程式碼

收尾動畫

在Up事件之後,還要簡單的處理一下一下收尾的滾動動畫,比如,滾動距離不夠要復原,否則,就滾動到目標檢視,這裡主要是根據Up事件的位置,計算需要滾動的距離,並通過Scroller來完成剩下的滾動。

  private void flingToFinishScroll() {

    final int pHeight = mUpstairsView.getMeasuredHeight();
    final int threshold = (int) (pHeight * mPercent);
    float needFlingDistance = 0;
    if (CurrentTargetIndex.UPSTAIRS == mCurrentViewIndex) {
        if (getScrollY() <= 0) {
            needFlingDistance = 0;
        } else if (getScrollY() <= threshold) {
            if (needFlingToToggleView()) {
                needFlingDistance = pHeight - getScrollY();
                mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
            } else {
                needFlingDistance = -getScrollY();
            }
        } else {
            needFlingDistance = pHeight - getScrollY();
            mCurrentViewIndex = CurrentTargetIndex.DOWNSTAIRS;
        }
    } else if (CurrentTargetIndex.DOWNSTAIRS == mCurrentViewIndex) {
        if (pHeight <= getScrollY()) {
            needFlingDistance = 0;
        } else if (pHeight - getScrollY() < threshold) {
            if (needFlingToToggleView()) {
                needFlingDistance = -getScrollY();
                mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
            } else {
                needFlingDistance = pHeight - getScrollY();
            }
        } else {
            needFlingDistance = -getScrollY();
            mCurrentViewIndex = CurrentTargetIndex.UPSTAIRS;
        }
    }
    mScroller.startScroll(0, getScrollY(), 0, (int) needFlingDistance, mDuration);
    if (mOnSlideDetailsListener != null) {
        mOnSlideDetailsListener.onStatueChanged(mCurrentViewIndex);
    }
    postInvalidate();
}複製程式碼

以上就是常用商品詳情黏滯佈局的實現。最後附上GitHub連結 歡迎 star DragScrollDetailsLayout GitHub連結

相關文章