仿蘑菇街,蜜芽宝贝,京东商品详情界面,与NestedScroll滑动

上一篇文章中有提到界面中嵌套NestedScrollView与Fragment并用,而NestedScrollView是不建议与ListView,RecyclerView嵌套的,虽然有解决滑动冲突的办法,但是ListView与RecyclerView的缓存机制就没有了,大量列表数据界面中这样嵌套还有何意义,但是Fragment中有列表数据需要用到RecyclerView时,如何解决?

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

本篇就讲述蘑菇街,蜜芽宝贝还有早期淘宝详情界面的实现方式,他们的界面效果都大至相同

一:功能点

1:效果图:
图片1

界面中全屏的ScrollView内容滑动,而ScrollView中嵌套水平的Horhorizonscrollview,Banner轮播图,滑动过程中渐渐改变Toolbar的背景, 这里采用NestedScrollView实现

图片2

界面上滑类似翻到下一页面,里面再嵌套TabLayout,ViewPager, 而ViewPager中又可以嵌套RecyclerView, ScrollView等滑动控件,滑动到下一页面时显示回到顶部按钮,点击可以跳到上一页面,也可以滑动到上一页面中

2:功能介绍
通过效果图可以看到需要最定义最外层滑动控件(有些类似纵向滑动的ViewPager),google自带的coordinatorlayout控件behavior也可以实现类似这种需求,但是全屏分页面滑动似乎行不通,所以需要定义最外层滑动控件来处理滑动与兼容上面所提到的所有滑动控件的滑动,并支持多点滑动
如果你对定义最上层控件处理滑动冲突与多点滑动不熟悉(后面贴出代码不再详细讲述如何解决滑动冲突问题)
这里链接:Android定义最上层 ViewGroup 并解决多层滑动嵌套冲突与多点触摸滑动

网上有很多种实现方式,但是看过之后觉得兼容与扩展性还是太差,或者有一大堆滑动问题,而真正实现此功能,应该能像Coordinatorlayout那样只需要定义子控件如何滑动或优先滑动规则就行,里面嵌套水平还是纵向还是多层嵌套都能自动适应才算是完美了。

在第一页内容时,上下滑动都是子控件优先,当子控件滑动到底部不能滑动时,最上层控件才执行滑动,并滑动或加速度滑动跳转到第二页面内容, 此时也还是子控件全部优先滑动,子控件不能下滑时,最上层控件执行滑动跳转到第一页面内容

二:功能实现

1:颜色值过渡动画
现在很多APP都喜欢做滑动改变Toolbar背景颜色效果,而这种颜色变化都是根据滑动偏移来计算的.这里就先介绍个颜色过渡类,根据滑动比例获取起始颜色与结束颜色所对应的过渡颜色

/**
* 构造函数
 *
 * @param startColor  起始颜色值
 * @param endColor    结束颜色值
 */
public ArgbAnimator(int startColor, int endColor) {
    this.startColor = startColor;
    this.endColor = endColor;
    startA = (startColor >> 24) & 0xff;
    startR = (startColor >> 16) & 0xff;
    startG = (startColor >> 8) & 0xff;
    startB = startColor & 0xff;

    endA = (endColor >> 24) & 0xff;
    endR = (endColor >> 16) & 0xff;
    endG = (endColor >> 8) & 0xff;
    endB = endColor & 0xff;
}

/**
 * 获取动画段中的颜色
 *
 * @param fraction 0-1.0f
 * @return
 */
public int getFractionColor(float fraction) {
    if (fraction <= 0)
        return startColor;
    if (fraction >= 1.0f)
        return endColor;
    return (int) ((startA + (int) (fraction * (endA - startA))) << 24)
            | (int) ((startR + (int) (fraction * (endR - startR))) << 16)
            | (int) ((startG + (int) (fraction * (endG - startG))) << 8)
            | (int) ((startB + (int) (fraction * (endB - startB))));
}

2:最外层ScrollLayout定义,这里直接继承ViewGroup
这里只贴出事件拦截与滑动功能的核心代码,

case MotionEvent.ACTION_MOVE:
    //这里根据android定义的水平与垂直滑动的标准
    if (mIsBeingDragged) {
        //Log.i("Log", "isBeingDrag...");
        return true;
    }
    if (mIsUnableToDrag) {
        //Log.i("Log", "mIsUnableToDrag...");
        return false;
    }
    int index = getPointerIndex(ev, mPointerId);
    float x = MotionEventCompat.getX(ev, index);
    float diffX = x - mLastMotionX;
    float mDiffX = Math.abs(diffX);
    float y = MotionEventCompat.getY(ev, index);
    float diffY = y - mLastMotionY;
    float mDiffY = Math.abs(diffY);
    int intDiffy = (int) diffY;

    if (diffY != 0 && (canScroll(intDiffy) || canScroll(this, false, intDiffy, (int) x, (int) y) )) {
        //注意canScroll只兼容4.0以上的滑动冲突,google工程师有明确标出,如果需要兼容4.0以下版本
        //可以用我写好的ScrollLayoutCompat类,判断如果4.0以下版本时,用isChildCanScroll判断是否能滑动
        mLastMotionX = x;
        mLastMotionY = y;
        mIsUnableToDrag = true;
        return false;
    }

    if (mDiffX > mTouchSlop && mDiffX * 0.5f > mDiffY) {
        //没办法,ViewPager要这样写,也许是x轴滑动优先要比Y轴滑动优先低些吧,但是为了兼容ViwPager不得不跟着这么写
        mIsUnableToDrag = true;
    } else if (mDiffY > mTouchSlop) {
        mIsBeingDragged = true;
        requestParentDisallowInterceptTouchEvent(true);
        mLastMotionY = diffY > 0
                ? mInitialMotionY + mTouchSlop : mInitialMotionY - mTouchSlop;
        mLastMotionX = x;
    }
    break;


/**
* 子控件是否能够滑动,递归查询,这里也是滑动冲突与优先滑动的解决方法
 * @param v
 * @param checkV
 * @param dy
 * @param x
 * @param y
 * @return
 */
protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
    if (v instanceof ViewGroup) {
        final ViewGroup group = (ViewGroup) v;
        final int scrollX = v.getScrollX();
        final int scrollY = v.getScrollY();
        final int count = group.getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            // 这里只能兼容到4.0或以上版本,fucking compat,如果要兼容4.0以下滑动,请参考写好的ScrollLayoutCompat类
            //根据如果小于4.0版本用ScrollLayoutCompat.canScroll方法
            //  fucking compat
            //  fucking compat
            // This will not work for transformed views in Honeycomb+,google源码注释明确指出
            final View child = group.getChildAt(i);
            if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
                    && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
                    && canScroll(child, true, dy, x + scrollX - child.getLeft(),
                    y + scrollY - child.getTop())) {
                return true;
            }
        }
    }
    return checkV && ViewCompat.canScrollVertically(v, -dy);
}

从4.0以后google在控件的水平与纵向滑动规则上都统一拉,所以上层控件只需要要优先判断子控件是否能滑动来决定是否需要拦截事件就可以了,与NestedScroll有些不同,NestedScroll滑动机制是子控件滑动顺便带动上层控件滑动

再是onTouchEvent(MotionEvent event)中的核心代码,这里就只需要处理手指滑动与加速度滑动功能了

case MotionEvent.ACTION_MOVE:
    int index = getPointerIndex(event, mPointerId);
    float x = MotionEventCompat.getX(event, index);
    float y = MotionEventCompat.getY(event, index);
    float diffY = y - mLastMotionY;
    float mDiffY = Math.abs(diffY);
    if (!mIsBeingDragged) {//如果滑动尚未产生
        if(mDiffY > mTouchSlop) {
            mIsBeingDragged = true;
            requestParentDisallowInterceptTouchEvent(true);
            mLastMotionY = diffY > 0
                    ? mInitialMotionY + mTouchSlop : mInitialMotionY - mTouchSlop;
            mLastMotionX = x;
        }
        ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }
    }
    if (mIsBeingDragged) {
        int intDiffy = (int) diffY;
        if (performDragY(intDiffy)) {//这里就处理界面跟着手指滑动
            scrollBy(0, -intDiffy);
        }
        mLastMotionY = y;
        mLastMotionX = x;
    }
    break;

3:一些小细节
a:最小加速度单位,这里参考ViewPager源码写法
final float density = context.getResources().getDisplayMetrics().density;
mMinVelocity = (int) (MIN_FLING_VELOCITY * density);
//mMinVelocity = configuration.getScaledMinimumFlingVelocity();
//这里额外定义最小速度单位,用configuration获取的最小单位值比较小,滑动会太敏感

b:前面博客文章讲view生命周期时没有讲述onLayout(boolean changed, int l, int t, int r, int b)方法changed参数,这里作个解释

当界面有滑动偏移并且布局有改变高度时(比如弹出软键盘强制改变布局大小,前面博客文章有讲述),如果不修改滑动偏移值,界面会布局错乱,解决的方法就是通过布局重新测量改变后,根据此参数来判断是否需要重新滑动到计算的新的滑动位置中

/**
* 当线性布局一样布局,  滑动的高度则为第一个item的高度
 * @param changed
 * @param l
 * @param t
 * @param r
 * @param b
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    if (changed) {
        scrollLayoutChanged();
    }
}

/**
 * 高度有变化时需要调整滑动偏移位置
  */
 private void scrollLayoutChanged() {
     if (isClosed()) {
         scrollTo(0, scrollHeight);
     }
 }


小结:这种界面滑动的设计风格比较适用于详情界面中小模块比较多的界面,但是这种也不好的体验就是在上层控件与子控件切换滑动优先时,事件会断掉,要重新滑动,不符合NestedScroll滑动风格,而且现在美团,豌豆荚,淘宝都已经取消了这种效果。无论用哪种滑动风格,滑动控件定义与事件传递原理都是不会变的

最外层控件定义的好,里面嵌套什么控件都不怕有冲突

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

后面再介绍新的 美团(休闲娱乐详情),淘宝商品详情界面的功能实现
最后附上源码:http://download.csdn.net/detail/u012216274/9835296

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值