Imitation Taobao, Jingdong to achieve product drag

For App electricity supplier, product listings no doubt a very important module, observation of the mainstream shopping App detail screen and found most of them are made of two parts, the display of goods specifications above, the following is H5 Product Details, or embedded H5 contains a set of details and a list of comments ViewPager interface, this paper is to achieve a compatible scroll up and down the different needs of viscous View- DragScrollDetailsLayout .

DragScrollDetailsLayout GitHub link

Achieve renderings

First look at the implementation renderings

Simple ScrollView + Webview

Of course, if you replace Webview into the other ListView like also supported.

ScrollView+ViewPager

Application scenario: a plurality of interfaces need to be added at the bottom, and requires the slide

ScrollView+Fragmenttabhost

Application scenario: a plurality of interfaces need to be added at the bottom, but does not need the slide

achieve

For the needs of this scene, it is easy to think of can be divided into two parts to implement, requires only a Vertical LinearLayout, the rest is to deal with the problem of scrolling and animation, custom interior ViewGroup first to declare two top sub ViewmUpstairsView, View mDownstairsView, and uses a variable mark is currently in operation that 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;
            }
        }

Then focus on scroll events, for scrolling and animation have the following main problems to be solved:

  • How do I know above or below the View has scrolled to the top or bottom
  • When you scroll to the border, how to deal with a sliding interception
  • How to let go of the movable handle subsequent effect

How to determine scroll border

首先来看第一个问题,如何知道上面或者下面的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();
}

以上就是常用商品详情黏滞布局的实现。

自己是从事了七年开发的Android工程师,不少人私下问我,2019年Android进阶该怎么学,方法有没有?

没错,年初我花了一个多月的时间整理出来的学习资料,希望能帮助那些想进阶提升Android开发,却又不知道怎么进阶学习的朋友。【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

资料获取方式:加入Android架构交流QQ群聊:513088520 ,进群即领取资料!!!

点击链接加入群聊【Android移动架构总群】:加入群聊

资料大全

Guess you like

Origin blog.csdn.net/weixin_45136073/article/details/92837221