仿美团详情滑动界面,并兼容 NestedScroll 嵌套

8,020
原文链接: blog.csdn.net

不论什么APP应该都会有个app产品的详情界面, 详情界面往往也比较炫,这篇主要介绍美团套餐详情的界面。(网上有用setOnTouchListener实现了此功能,但是不能支持多点滑动跟NestedScroll滑动嵌套)
NestedScrollView: 有现成的setOnScrollChangeListener可以监听滑动(ScrollView需要api23后才支持或需要自己额外添加,而且ScrollView在加速度滑动到顶部或底部时会边缘会显示些空白),NestedScrollView可以说是ScrollView代替品,并且有处理这种滑动超出界限时空白问题,并且NestedScrollView有多点触摸滑动且实现NestedScrollingParent,NestedScrollingChild能兼容NestedScroll滑动嵌套风格

更多文章请关注:blog.csdn.net/u012216274

一:需要实现的功能

1:先看效果图吧:
NestedScroll嵌套效果
下拉放大效果

上滑过程中,图片缓缓上滑(相对下边内容滑动缓慢),下滑动超出边界时,下拉放大手指离开地回弹

a:即可上滑也可下滑回弹的NestedScrollView,任何滑动效果只需监听OnScrollChangeListener的滑动偏移即可
b:上滑与下滑事件不会中断,支持多点触摸滑动
c:能兼容NestedScroll滑动嵌套

2:滑动界面中再添加Fragment模块
详情界面中可能还会需要添加些更多的功能,可能还会需要在滑动控件组中添加fragment切换
如效果图:
这里写图片描述

3:美团效果的未完善之处:
a:下拉滑动放大的时候不支持多点滑动(整个APP下拉刷新都不支持多点触摸,哎……)
b:NestedScroll滑动嵌套早已流行却没有支持该功能,而且该功能最大一个好处就是上滑下滑事件都不会断掉(美团界面上滑动后再下滑要放大图片,需要手指抬起,重新滑动)

二:功能实现

小技巧:如何查看别APP界面功能大致实现方式
a:可以打开”开发者选项” –>“显示布局边界”就可以看到别人的大致布局啦, 包括隐藏悬浮布局
b:如果要看些滑动时的特效,比如滑动时遮挡了就看不到布局了,这时候可以打开 “开发者选项” –>“调试GPU过度绘制”–>“显示过度绘制区域”就能看到被遮挡的界面,滑动时就可以知道遮挡的界面如何过度动画了!

1:定义能下滑并回弹的NestedScrollView

NestedScrollView的onTouchEvent源码,MotionEvent.ACTION_MOVE的逻辑处理有断核心处理滑动的代码overScrollByCompat方法,看源码可以了解,在向下滑动的时候,getScrollY <0,时会限制滑动,并画EdgeEffectCompat边缘阴影效果
所以要实现下拉回弹效果只需要改此处核心代码,并在ACTION_UP事件中处理回弹效果
如果你对滑动控件或控件定义不熟悉,可以先了解下
此处链接 Android自定义View,ViewGroup(一)的一些原理与细节,RecyclerView版侧滑删除

overScrollByCompat方法子类不能复写,并且onTouchEvent事件中代码也改不了,直接定义一个类,把google工程师的写的NestedScrollView代码全拷进去再改,反正就一个类, 实在想不出办法,有想到更好办法的欢迎多多指教

/**
* 加速度滑动时,不可越界,手指拖动时可以
 */
private boolean isFling;
/**
 * 支持向下滑动偏移
 */
static final int PULLDOWN_SCALE = 2;
/**
 * 向下越界滑动时的滑动比例
 */
static final int PULLDOWN_SCROLL = 3;

boolean overScrollByCompat(int deltaX, int deltaY,
                       int scrollX, int scrollY,
                           int scrollRangeX, int scrollRangeY,
                           int maxOverScrollX, int maxOverScrollY,
                           boolean isTouchEvent) {
    final int overScrollMode = getOverScrollMode();
    final boolean canScrollHorizontal =
            computeHorizontalScrollRange() > computeHorizontalScrollExtent();
    final boolean canScrollVertical =
            computeVerticalScrollRange() > computeVerticalScrollExtent();
    final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS
            || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
    final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS
            || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

    int newScrollX = scrollX + deltaX;
    if (!overScrollHorizontal) {
        maxOverScrollX = 0;
    }

    //此处修改原码,一般越界或下拉刷新滑动的滑动值都是按手指滑动的差值除一定的比例,防止界面防出屏幕显示范围
    //此处除以3,
    if (scrollY < 0 && deltaY < 0) {//偏移往下滑动
        deltaY /= PULLDOWN_SCROLL;
    }

    int newScrollY = scrollY + deltaY;
    if (!overScrollVertical) {
        maxOverScrollY = 0;
    }

    // Clamp values if at the limits and record
    final int left = -maxOverScrollX;
    final int right = maxOverScrollX + scrollRangeX;

    //可以超越边界下滑的主要代码就在这
    //在此处修改下滑时的边界值
    //此处修改原码
    int top;
    if (isFling) {//如果是加速度产生的滑动,不考虑可以滑出边界
        top = - maxOverScrollY;
    } else {
        top = -getPulldownScroll();
    }

    final int bottom = maxOverScrollY + scrollRangeY;

    boolean clampedX = false;
    if (newScrollX > right) {
        newScrollX = right;
        clampedX = true;
    } else if (newScrollX < left) {
        newScrollX = left;
        clampedX = true;
    }

    boolean clampedY = false;
    if (newScrollY > bottom) {//滑动越出界限
        newScrollY = bottom;
        clampedY = true;
    } else if (newScrollY < top) {
        newScrollY = top;
        clampedY = true;
    }

    if (clampedY && isFling) {//这里isFling时才回弹
        //Log.i("you", top + "  " + bottom + "  "+deltaY+" "+newScrollY+" "+getScrollRange());
        mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange());
    }
    onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
    return clampedX || clampedY;
}

/**
 * 允许下拉的高度
 * @return
 */
int getPulldownScroll() {
    return (getHeight() + getPaddingTop() + getPaddingBottom()) / PULLDOWN_SCALE;
}


核心滑动功能处理好后就是修改画边缘阴影效果的代码和手指离开地的回弹效果(可以参考源码onTouchEvent中的ACTION_MOVE与ACTION_UP事件中的代码)

ensureGlows();
final int pulledToY = oldY + deltaY;
//此处修改源码,滑动超出下拉滑动的边界时才画边缘阴影效果
if (pulledToY < -getPulldownScroll()) {
    mEdgeGlowTop.onPull((float) deltaY / getHeight(),
            ev.getX(activePointerIndex) / getWidth());
    if (!mEdgeGlowBottom.isFinished()) {
        mEdgeGlowBottom.onRelease();
    }
}

//此处修改源码
if (getScrollY() < 0) {//越界时不考虑加速度
    Log.i("you", "it is scrollback...");
    scrollBack();//这里执行回弹效果
} else {
    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
        isFling = true;
        //Log.i("you", "it is fling...");
        flingWithNestedDispatch(-initialVelocity);
    } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
            getScrollRange())) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

/**
 * 回弹
 */
private boolean scrollBack() {
    int dy = getScrollY();
    if (getScrollY() < 0) {
        mScroller.startScroll(0, dy, 0, -dy, computeScrollDuration(dy, 0));
        invalidate();
        return true;
    }
    return false;
}

2:界面中上滑与下滑的效果
直接上代码介绍,定义好了可以下滑偏移的NestedScrollViewr后,要实现功能就非常简单了,界面上所有的滑动效果只需要在onScrollChange方法中实现

sv_root.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
    @Override
    public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        if (scrollY >= 0) {//往上滑动
            int height = rl_top.getHeight();
            if (height != ll_content.getPaddingTop()) {//如果滑动时高度有误先矫正高度
                ViewGroup.LayoutParams layoutParams = rl_top.getLayoutParams();
                layoutParams.height = ll_content.getPaddingTop();
                rl_top.setLayoutParams(layoutParams);
            }
            boolean overTitle = scrollY >= height;

            //立即抢购有一个悬浮布局
            //这里也是采用内容标题滑动到顶部时,显示与内容标题一样的悬浮布局

            ll_float.setVisibility(overTitle ? View.VISIBLE : View.GONE);
            ll_title.setVisibility(overTitle ? View.INVISIBLE : View.VISIBLE);

            //rl_top为图片内容区域,滑动区域实际paddingtop该内容区域大小,就可以显示该内容区域
            //当界面向上滑动时, 该区域也跟着上滑,只不过上滑的移动位置为滑动内容中的1/3

            rl_top.setVisibility(overTitle ? View.GONE : View.VISIBLE);
            rl_top.scrollTo(0, scrollY / 3);
        } else {//下拉滑动

            //当滑动下拉偏移时,图片内容区域则是只是改变控件高度大小,高度即为本身高度+下滑偏移的高度

            rl_top.scrollTo(0, 0);//不能有滑动偏移
            ViewGroup.LayoutParams layoutParams = rl_top.getLayoutParams();
            layoutParams.height = ll_content.getPaddingTop() - scrollY;
            rl_top.setLayoutParams(layoutParams);
        }
    }
});

3:嵌套Fragment
ScrollView中嵌套Fragment其实很简单,只要Fragment的控件不与ScrollView滑动有冲突就行
直接看布局代码

<LinearLayout
   android:id="@+id/ll_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingTop="210dp">

    ......//此处为ScrollView中的滑动内容

    <FrameLayout
        android:id="@+id/fl_fragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

只需要将加载Fragment的容器FrameLayout高度设置wrap_content, Fragment中的布局也设置成wrap_content,就可以了,可以为任意高度,反正内容都是在ScrollView中滑动,哈哈!!!


小结:ScrollView不建议嵌套RecyclerView,ListView,如果详情界面滑动跟此篇文章介绍的类似,但嵌套的Fragment需要显示大量列表数据需要用到ListView或RecyclerView时,该如何处理?下篇文章再介绍。

更多文章请关注:blog.csdn.net/u012216274

最后附上源码地址:download.csdn.net/detail/u012…