设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间11 现在时间11 缓存数据 以后别遇到像我这样的人敏感多疑 总是吵着让你陪我经常瞎想 总让你很累吧 但又希望碰到这样的人 因为这样的人真的真的很爱你

以后别遇到像我这样的人敏感多疑 总是吵着让你陪我经常瞎想 总让你很累吧 但又希望碰到这样的人 因为这样的人真的真的很爱你 -- 敏感多疑

查看: 731|回复: 1

Android使用Scrolling机制实现Tab吸顶效果

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
19
主题
19
精华
0
金钱
67
积分
38
注册时间
2023-9-30
最后登录
2025-5-31

发表于 2024-6-13 08:08:06 | 显示全部楼层 |阅读模式
目录


  • 一、前言
  • 二、效果展示
  • 三、实现逻辑

    • 3.1 布局设计的注意事项
    • 3.2 主要逻辑

      • 3.2.1 规划布局
      • 3.2.2 Scrolling 机制
      • 3.2.3 主要代码


  • 四、代码实现

    • 4.1 要点
    • 4.2 主要代码
    • 4.3 布局属性定义
    • 4.4 使用

  • 五、总结

一、前言

app 首页中经常要实现首页头卡共享,tab 吸顶,内容区通过 ViewPager 切换的需求,以前往往是利用事件处理来完成,还有 Google 官方也提供了相关的库如CoordinatorLayout,但是这些也有一定的弊端和滑动方面不如意的地方,瑕疵比较明显,实际上很多大厂的吸顶效果都是自己写的,同样适配起来还是比较复杂。
这里我们利用 NestedScrolling 机制来实现。
当然也有很多开源项目,发现存在的问题很多面,主要问题如下:

  • 头部和内容区域不联动
  • 没有中断 RecyclerView 的 fling 效果,导致 RecyclerView 抢占 ViewPager 事件
  • 仅仅只支持RecyclerView,不支持扩展
  • 侵入式设计太多,反射太多。(当然,本篇方案解决 RecyclerView 中断 fling 时用了侵入式设计)
  • 严重依赖Adapter、ViewHolder等。

二、效果展示

1.webp

其实这个页面中存在以下布局元素:
Head 部分是大卡片和TabLayout
Body部分使用ViewPager,然后通过ViewPager“装载”两个RecyclerView。

三、实现逻辑


3.1 布局设计的注意事项

对于实现布局,评价一个布局的好坏应该从以下几方面出发
布局规划:提前规划好最终的效果和布局的组成,以及要处理最大一些问题,如果处理不好,则可能出现做到一半无法做下去的问题。
耦合程度:应该尽可能避免太多的耦合,比如View与View之间的直接调用,如果有,那么应该着手从设计原则着手或者父子关系方面改良设计。
减少XML组合布局:很多自定义布局中Inflate xml布局,虽然这种也属于自定义View,但是封装在xml中的View很难让你去修改属性和样式,设置要做大量的自定义属性去适配。
通用性和可扩展性:通用性是此View要做到随处可用,即便不能也要在这个方向进行扩展,可扩展性的提高可以促进通用性。为了实现布局效果,一些开发者不仅仅自定义了父布局,而且还定义了各种子布局,这显然降低了扩展性和适用性。原则上,两者同时定义的问题应该在父布局中去处理,而不是从子View中去处理。
完成好于完美:对于性能和瑕疵问题,避免提前处理,除非阻碍开发。遵循“完成好于完美”的原则,先实现再完善,不断循环优化才是正确的方式。很多人自定义的时候担心性能和瑕疵问题,导致无法设计出最终效果,实际上很多自定义布局的瑕疵和性能都是在完成之后优化效果的,因此过多的提前布置,可能会让你做大量返工处理。
下面是本篇设计过程,希望对你有帮助

3.2 主要逻辑


3.2.1 规划布局

规划布局是非常重要的,这里我们规划布局为
HEAD部分和BODY两部分,至于吸顶的TabLayout,我们放到Head部分,让吸顶时让Head部分top 最大移动为HEAD高度减去TabLayout的高度。BODY部分可以使用ViewPager,也可以是其他布局,因为ViewPager使用较广,本文使用ViewPager。
  1. <Head>
  2.     <Card></Card>
  3.     <TabLayout></TabLayout>
  4. </Head>
  5. <Body>
  6.     <RecyclerView1/>
  7.     ....
  8.     <RecyclerViewN/>
  9. </Body>
复制代码
3.2.2 Scrolling 机制

其实在本篇之前,我们也通过Scrolling机制定义过,但要明白为什么要使用Scrolling机制?
Scrolling机制可以协同父子View、祖宗View的滑动,当然这个范围有点小。本篇我们要协同滑动,中间隔着ViewPager,人家可是爷孙关系。
Scrolling提供了祖宗树上可以互相通知的View
通用性强:Scrolling是通过support或者androidx库接入的,虽然当前发展到第三个版本了,但是毫不影响我们升级使用。

3.2.3 主要代码

继承Scrolling接口
  1. public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
  2.     private final int mFlingVelocity;  //fling 纵向速度计算
  3.     private int mHeadExpandedOffset;  // tab偏移,也就是为了方便tab吸顶
  4.     private float startEventX = 0;
  5.     private float startEventY = 0;
  6.     private float mSlopTouchScale = 0; //互动判断阈值
  7.     private boolean isTouchMoving = false;
  8.     private View mHeaderView = null;  //抽象调用head
  9.     private View mBodyView = null;  // 抽象调用body
  10.     private View mVerticalScrollView = null;
  11.     private VelocityTracker mVelocityTracker; //顺时力度跟踪

  12.   //辅助当前布局滑动类型判断,如水平滑动还是垂直滑动以及是不是手指触动的滑动,实现主要是为了兼容外部调用
  13. ///参考NestedScrollView实现的
  14.    private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
  15. .....
  16. }
复制代码
自定义布局参数,主要是为子View添加布局属性
  1.     public static class LayoutParams extends FrameLayout.LayoutParams {
  2.         public final static int TYPE_HEAD = 0;
  3.         public final static int TYPE_BODY = 1;
  4.         private int childLayoutType = TYPE_HEAD;

  5.         public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
  6.             super(c, attrs);
  7.             if (attrs == null) return;
  8.             final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
  9.             childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
  10.             a.recycle();
  11.         }

  12.         public LayoutParams(int width, int height) {
  13.             super(width, height);
  14.         }

  15.         public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
  16.             super(source);
  17.         }

  18.         public LayoutParams(@NonNull MarginLayoutParams source) {
  19.             super(source);
  20.         }
  21.     }
复制代码
测量
我们这里纵向排列即可
  1. @Override
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4.         int childCount = getChildCount();
  5.         int height = MeasureSpec.getSize(heightMeasureSpec);
  6.         int overScrollExtent = overScrollExtent();
  7.         for (int i = 0; i < childCount; i++) {
  8.             View child = getChildAt(i);
  9.             LayoutParams lp = (LayoutParams) child.getLayoutParams();
  10.             if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
  11.                 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
  12.                         getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
  13.                                 + 0, lp.width);
  14.                 final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
  15.                         getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
  16.                                 + 0, height - overScrollExtent);
  17.                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  18.             }
  19.         }
  20.     }
复制代码
核心方法,纵向滑动处理
  1.     private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
  2.         if (dy == 0) {
  3.             return;
  4.         }
  5.         if (!canNestedScrollView(mVerticalScrollView)) {
  6.             //这里要判断向上滑动问题,
  7.             // 如果当前布局可以向上滑动,优先滑动,不然头部可能出现露一半但无法向上滑动的问题
  8.             if (dy < 0) {
  9.                 return;
  10.             }
  11.             if (!allowScroll(dy)) {
  12.                 return;
  13.             }
  14.         }
  15.         int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  16.         int scrollOffset = computeVerticalScrollOffset();

  17.         int dyOffset = dy;
  18.         int targetOffset = scrollOffset + dy;
  19.         if (targetOffset >= maxOffset) {
  20.             dyOffset = maxOffset - scrollOffset;
  21.         }
  22.         if (targetOffset <= 0) {
  23.             dyOffset = 0 - scrollOffset;
  24.         }
  25.         if (!canScrollVertically(dyOffset)) {
  26.             return;
  27.         }
  28.         consumed[1] = dyOffset;
  29.         Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
  30.         scrollBy(0, dyOffset);
  31.     }
复制代码
核心事件处理,主要处理滑动,瞬时速度问题
  1.     @Override
  2.     public boolean dispatchTouchEvent(MotionEvent event) {
  3.         int scrollRange = computeVerticalScrollRange();
  4.         if (scrollRange <= getHeight()) {
  5.             return super.dispatchTouchEvent(event);
  6.         }
  7.         if (mVelocityTracker == null) {
  8.             mVelocityTracker = VelocityTracker.obtain();
  9.         }
  10.         int action = event.getAction();
  11.         switch (action) {
  12.             case MotionEvent.ACTION_DOWN:
  13.                 mVelocityTracker.addMovement(event);
  14.                 startEventX = event.getX();
  15.                 startEventY = event.getY();
  16.                 isTouchMoving = false;
  17.                 if (mVerticalScrollView instanceof RecyclerView) {
  18.                     /**
  19.                      *RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
  20.                      *调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
  21.                      */
  22.                     ((RecyclerView) mVerticalScrollView).stopScroll();
  23.                 } else if (mVerticalScrollView instanceof NestedScrollingChild) {
  24.                     mVerticalScrollView.stopNestedScroll();
  25.                 }
  26.                 break;
  27.             case MotionEvent.ACTION_MOVE:
  28.                 float currentX = event.getX();
  29.                 float currentY = event.getY();
  30.                 float dx = currentX - startEventX;
  31.                 float dy = currentY - startEventY;
  32.                 if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
  33.                     startEventX = currentX;
  34.                     startEventY = currentY;
  35.                     break;
  36.                 }
  37.                 View touchView = null;
  38.                 int offset = (int) -dy;
  39.                 if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
  40.                     touchView = findTouchView(currentX, currentY);
  41.                     //这里只关注头卡触摸事件即可
  42.                     isTouchMoving = touchView != null && touchView == getHeaderView();
  43.                 }
  44.                 if (isTouchMoving && !allowScroll(offset)) {
  45.                     isTouchMoving = false;
  46.                 }
  47.                 startEventX = currentX;
  48.                 startEventY = currentY;
  49.                 if (!isTouchMoving) {
  50.                     break;
  51.                 }
  52.                 mVelocityTracker.addMovement(event);
  53.                 int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  54.                 int scrollOffset = computeVerticalScrollOffset();
  55.                 int targetOffset = scrollOffset + offset;
  56.                 if (targetOffset >= maxOffset) {
  57.                     offset = maxOffset - scrollOffset;
  58.                 }
  59.                 if (targetOffset <= 0) {
  60.                     offset = 0 - scrollOffset;
  61.                 }
  62.                 if (offset != 0) {
  63.                     scrollBy(0, offset);
  64.                 }
  65.                 Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
  66.                 super.dispatchTouchEvent(event);
  67.                 return true;

  68.             case MotionEvent.ACTION_UP:
  69.             case MotionEvent.ACTION_CANCEL:
  70.             case MotionEvent.ACTION_OUTSIDE:
  71.                 mVelocityTracker.addMovement(event);
  72.                 if (isTouchMoving) {
  73.                     isTouchMoving = false;
  74.                     mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
  75.                     startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
  76.                     mVelocityTracker.recycle();
  77.                     mVelocityTracker = null;
  78.                 }
  79.                 break;
  80.         }

  81.         return super.dispatchTouchEvent(event);
  82.     }
复制代码
四、代码实现


4.1 要点
  1. 头部不联动问题:
  2. 我们需要处理在 dispatchTouchEvent 或者利用 onInteceptTouchEvent + onTouchEvent 处理,主要处理 VelocityTracker + fling 事件。接着我们判断滑动开始位置是不是在头部,因为按照布局设计,头部和RecyclerView不一样,头部是随着整体滑动,而RecyclerView是可以内部滑动的,直到无法滑动时,我们才能让父布局整体滑动,通过这种方式就能解决联动问题。
复制代码
  1. RecyclerView 中断 fling 效果问题:
  2. RecyclerView 没有在 stopNestedScroll () 方法中中断滑动,因此需要通过侵入方式,调用 stopScroll () 去完成,其实我们这里希望官方提供接口终止RecyclerView停止滑动,但是事实上没有,这个问题一定概率上造成RecyclerView减速滑动时,ViewPager也无法切换,当然很多其他开源方案都有类似的问题。
复制代码
  1. if (mVerticalScrollView instanceof RecyclerView) {
  2.       /**
  3.       * RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
  4.       * 调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
  5.      */
  6.      ((RecyclerView) mVerticalScrollView).stopScroll();
  7.   }
复制代码
查找事件点所在的View,这里我们使用了下面方法,理论上我们不会子Head和Body部分做Matrix变换,因此Android内部通过矩阵判断View的逆矩阵方式我们可以不用。
  1. private View findTouchView(float currentX, float currentY) {

  2.         for (int i = 0; i < getChildCount(); i++) {
  3.             View child = getChildAt(i);
  4.             float childX = (child.getX() - getScrollX());
  5.             float childY = (child.getY() - getScrollY());
  6.             if (currentX < childX || currentX > (childX + child.getWidth())) {
  7.                 continue;
  8.             }
  9.             if (currentY < childY || currentY > (childY + child.getHeight())) {
  10.                 continue;
  11.             }
  12.             return child;
  13.         }
  14.         return null;
  15.     }
复制代码
捕获Scrolling Child,下面方法是捕获来自Child的滑动请求,如果没有达到吸顶状态,应该优先滑动父View
  1.     @Override
  2.     public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
  3.         if (axes == SCROLL_AXIS_VERTICAL) {
  4.             //只关注垂直方向的移动
  5.             int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  6.             int offset = computeVerticalScrollOffset();
  7.             if (offset <= maxOffset) {
  8.                 mVerticalScrollView = target;
  9.                 return true;
  10.             }
  11.         } else {
  12.             mVerticalScrollView = null;
  13.         }
  14.         return false;
  15.     }
复制代码
4.2 主要代码
  1. public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {    private final int mFlingVelocity;    private int mHeadExpandedOffset;    private float startEventX = 0;    private float startEventY = 0;    private float mSlopTouchScale = 0;    private boolean isTouchMoving = false;    private View mHeaderView = null;    private View mBodyView = null;    private View mVerticalScrollView = null;    private VelocityTracker mVelocityTracker;    private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);    public NestedPagerRecyclerViewLayout(@NonNull Context context) {        this(context, null);    }    public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) {        this(context, attrs, 0);    }    public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        if (attrs != null) {            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);            mHeadExpandedOffset = a.getDimensionPixelSize(R.styleable.NestedPagerRecyclerViewLayout_headExpandedOffset, 0);            a.recycle();        }        mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();        mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();        setClickable(true);    }    /**     * 头部余留偏移     *     * @param headExpandedOffset     */    public void setHeadExpandOffset(int headExpandedOffset) {        this.mHeadExpandedOffset = headExpandedOffset;    }    @Override
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4.         int childCount = getChildCount();
  5.         int height = MeasureSpec.getSize(heightMeasureSpec);
  6.         int overScrollExtent = overScrollExtent();
  7.         for (int i = 0; i < childCount; i++) {
  8.             View child = getChildAt(i);
  9.             LayoutParams lp = (LayoutParams) child.getLayoutParams();
  10.             if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
  11.                 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
  12.                         getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
  13.                                 + 0, lp.width);
  14.                 final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
  15.                         getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
  16.                                 + 0, height - overScrollExtent);
  17.                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  18.             }
  19.         }
  20.     }    public boolean canScrollVertically(int direction) {        final int offset = computeVerticalScrollOffset();        final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();        if (range == 0) return false;        if (direction < 0) {            return offset > 0;        } else {            return offset < range;        }    }    @Override    protected int computeVerticalScrollRange() {        int childCount = getChildCount();        if (childCount == 0) return super.computeVerticalScrollRange();        int range = getPaddingBottom() + getPaddingTop();        for (int i = 0; i < childCount; i++) {            View child = getChildAt(i);            LayoutParams lp = (LayoutParams) child.getLayoutParams();            range += child.getHeight() + lp.bottomMargin + lp.topMargin;        }        if (range < getHeight()) {            return super.computeVerticalScrollRange();        }        return range;    }    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        super.onLayout(changed, left, top, right, bottom);        mHeaderView = getChildView(LayoutParams.TYPE_HEAD);        mBodyView = getChildView(LayoutParams.TYPE_BODY);        int childLeft = getPaddingLeft();        int childTop = getPaddingTop();        if (mHeaderView != null) {            LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();            mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());            childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;        }        if (mBodyView != null) {            LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();            mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());        }    }    protected int overScrollExtent() {        return Math.max(mHeadExpandedOffset, 0);    }    private View getHeaderView() {        return mHeaderView;    }    private View getBodyView() {        return mBodyView;    }    private View findTouchView(float currentX, float currentY) {

  21.         for (int i = 0; i < getChildCount(); i++) {
  22.             View child = getChildAt(i);
  23.             float childX = (child.getX() - getScrollX());
  24.             float childY = (child.getY() - getScrollY());
  25.             if (currentX < childX || currentX > (childX + child.getWidth())) {
  26.                 continue;
  27.             }
  28.             if (currentY < childY || currentY > (childY + child.getHeight())) {
  29.                 continue;
  30.             }
  31.             return child;
  32.         }
  33.         return null;
  34.     }    private boolean hasHeader() {        int count = getChildCount();        for (int i = 0; i < count; i++) {            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();            if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {                return true;            }        }        return false;    }    public View getChildView(int layoutType) {        int count = getChildCount();        for (int i = 0; i < count; i++) {            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();            if (lp.childLayoutType == layoutType) {                return getChildAt(i);            }        }        return null;    }    private boolean hasBody() {        int count = getChildCount();        for (int i = 0; i < count; i++) {            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();            if (lp.childLayoutType == LayoutParams.TYPE_BODY) {                return true;            }        }        return false;    }    @Override    public void addView(View child) {        assertLayoutType(child);        super.addView(child);    }    private void assertLayoutType(View child) {        ViewGroup.LayoutParams lp = child.getLayoutParams();        assertLayoutParams(lp);    }    private void assertLayoutParams(ViewGroup.LayoutParams lp) {        if (hasHeader() && hasBody()) {            throw new IllegalStateException("header and body has already existed");        }        if (hasHeader()) {            if (!(lp instanceof LayoutParams)) {                throw new IllegalStateException("header should keep only one");            }            if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {                throw new IllegalStateException("header should keep only one");            }        }        if (hasBody()) {            if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {                throw new IllegalStateException("header should keep only one");            }        }    }    @Override    public void addView(View child, int index, ViewGroup.LayoutParams params) {        assertLayoutParams(params);        super.addView(child, index, params);    }    @Override    public void addView(View child, int index) {        assertLayoutType(child);        super.addView(child, index);    }    @Override    public void addView(View child, int width, int height) {        assertLayoutParams(new LinearLayout.LayoutParams(width, height));        super.addView(child, width, height);    }    @Override    public void onViewAdded(View child) {        super.onViewAdded(child);    }    @Override    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {        return p instanceof LayoutParams;    }    @Override    protected FrameLayout.LayoutParams generateDefaultLayoutParams() {        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);    }    @Override    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {        return new LayoutParams(getContext(), attrs);    }    @Override    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {        return new LayoutParams(lp);    }    @Override
  35.     public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
  36.         if (axes == SCROLL_AXIS_VERTICAL) {
  37.             //只关注垂直方向的移动
  38.             int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  39.             int offset = computeVerticalScrollOffset();
  40.             if (offset <= maxOffset) {
  41.                 mVerticalScrollView = target;
  42.                 return true;
  43.             }
  44.         } else {
  45.             mVerticalScrollView = null;
  46.         }
  47.         return false;
  48.     }    @Override    protected int computeVerticalScrollExtent() {        int computeVerticalScrollExtent = super.computeVerticalScrollExtent();        return computeVerticalScrollExtent;    }    @Override    public int getNestedScrollAxes() {        return parentHelper.getNestedScrollAxes();    }    @Override    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {        parentHelper.onNestedScrollAccepted(child, target, axes, type);    }    @Override    public void onStopNestedScroll(@NonNull View target, int type) {        if (mVerticalScrollView == target) {            Log.d("onNestedScroll", "::::onStopNestedScroll vertical");            parentHelper.onStopNestedScroll(target, type);        }    }    @Override    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {        Log.e("onNestedScroll", "::::onNestedScroll 11111");    }    @Override    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {        int scrollRange = computeVerticalScrollRange();        if (scrollRange <= getHeight()) {            return;        }        if (target == null) return;        if (mVerticalScrollView != target) {            return;        }        Log.e("onNestedScroll", "::::onNestedPreScroll 00000");        handleVerticalNestedScroll(dx, dy, consumed);    }    private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
  49.         if (dy == 0) {
  50.             return;
  51.         }
  52.         if (!canNestedScrollView(mVerticalScrollView)) {
  53.             //这里要判断向上滑动问题,
  54.             // 如果当前布局可以向上滑动,优先滑动,不然头部可能出现露一半但无法向上滑动的问题
  55.             if (dy < 0) {
  56.                 return;
  57.             }
  58.             if (!allowScroll(dy)) {
  59.                 return;
  60.             }
  61.         }
  62.         int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  63.         int scrollOffset = computeVerticalScrollOffset();

  64.         int dyOffset = dy;
  65.         int targetOffset = scrollOffset + dy;
  66.         if (targetOffset >= maxOffset) {
  67.             dyOffset = maxOffset - scrollOffset;
  68.         }
  69.         if (targetOffset <= 0) {
  70.             dyOffset = 0 - scrollOffset;
  71.         }
  72.         if (!canScrollVertically(dyOffset)) {
  73.             return;
  74.         }
  75.         consumed[1] = dyOffset;
  76.         Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
  77.         scrollBy(0, dyOffset);
  78.     }    @Override
  79.     public boolean dispatchTouchEvent(MotionEvent event) {
  80.         int scrollRange = computeVerticalScrollRange();
  81.         if (scrollRange <= getHeight()) {
  82.             return super.dispatchTouchEvent(event);
  83.         }
  84.         if (mVelocityTracker == null) {
  85.             mVelocityTracker = VelocityTracker.obtain();
  86.         }
  87.         int action = event.getAction();
  88.         switch (action) {
  89.             case MotionEvent.ACTION_DOWN:
  90.                 mVelocityTracker.addMovement(event);
  91.                 startEventX = event.getX();
  92.                 startEventY = event.getY();
  93.                 isTouchMoving = false;
  94.                 if (mVerticalScrollView instanceof RecyclerView) {
  95.                     /**
  96.                      *RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
  97.                      *调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
  98.                      */
  99.                     ((RecyclerView) mVerticalScrollView).stopScroll();
  100.                 } else if (mVerticalScrollView instanceof NestedScrollingChild) {
  101.                     mVerticalScrollView.stopNestedScroll();
  102.                 }
  103.                 break;
  104.             case MotionEvent.ACTION_MOVE:
  105.                 float currentX = event.getX();
  106.                 float currentY = event.getY();
  107.                 float dx = currentX - startEventX;
  108.                 float dy = currentY - startEventY;
  109.                 if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
  110.                     startEventX = currentX;
  111.                     startEventY = currentY;
  112.                     break;
  113.                 }
  114.                 View touchView = null;
  115.                 int offset = (int) -dy;
  116.                 if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
  117.                     touchView = findTouchView(currentX, currentY);
  118.                     //这里只关注头卡触摸事件即可
  119.                     isTouchMoving = touchView != null && touchView == getHeaderView();
  120.                 }
  121.                 if (isTouchMoving && !allowScroll(offset)) {
  122.                     isTouchMoving = false;
  123.                 }
  124.                 startEventX = currentX;
  125.                 startEventY = currentY;
  126.                 if (!isTouchMoving) {
  127.                     break;
  128.                 }
  129.                 mVelocityTracker.addMovement(event);
  130.                 int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
  131.                 int scrollOffset = computeVerticalScrollOffset();
  132.                 int targetOffset = scrollOffset + offset;
  133.                 if (targetOffset >= maxOffset) {
  134.                     offset = maxOffset - scrollOffset;
  135.                 }
  136.                 if (targetOffset <= 0) {
  137.                     offset = 0 - scrollOffset;
  138.                 }
  139.                 if (offset != 0) {
  140.                     scrollBy(0, offset);
  141.                 }
  142.                 Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
  143.                 super.dispatchTouchEvent(event);
  144.                 return true;

  145.             case MotionEvent.ACTION_UP:
  146.             case MotionEvent.ACTION_CANCEL:
  147.             case MotionEvent.ACTION_OUTSIDE:
  148.                 mVelocityTracker.addMovement(event);
  149.                 if (isTouchMoving) {
  150.                     isTouchMoving = false;
  151.                     mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
  152.                     startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
  153.                     mVelocityTracker.recycle();
  154.                     mVelocityTracker = null;
  155.                 }
  156.                 break;
  157.         }

  158.         return super.dispatchTouchEvent(event);
  159.     }    public boolean allowScroll(int dy) {        int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();        int scrollOffset = computeVerticalScrollOffset();        int dyOffset = dy;        int targetOffset = scrollOffset + dy;        if (targetOffset >= maxOffset) {            dyOffset = maxOffset - scrollOffset;        }        if (targetOffset <= 0) {            dyOffset = 0 - scrollOffset;        }        if (!canScrollVertically(dyOffset)) {            return false;        }        return true;    }    private void startFling(VelocityTracker velocityTracker, int x, int y) {        int xVolecity = (int) velocityTracker.getXVelocity();        int yVolecity = (int) velocityTracker.getYVelocity();        if (mVerticalScrollView instanceof NestedScrollingChild) {            Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);            ((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);        }    }    private boolean canNestedScrollView(View view) {        if (view == null) {            return false;        }        if (view instanceof RecyclerView) {            //显示区域最上面一条信息的position            RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();            if (manager == null) {                return true;            }            if (manager.getChildCount() == 0) {                return true;            }            int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();            return scrollOffset <= 0;        }        if (view instanceof NestedScrollingChild) {            return view.canScrollVertically(-1);        }        if (!(view instanceof ViewGroup) && (view instanceof View)) {            return true;        }        throw new IllegalArgumentException("不支持非NestedScrollingChild子类ViewGroup");    }    public static class LayoutParams extends FrameLayout.LayoutParams {
  160.         public final static int TYPE_HEAD = 0;
  161.         public final static int TYPE_BODY = 1;
  162.         private int childLayoutType = TYPE_HEAD;

  163.         public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
  164.             super(c, attrs);
  165.             if (attrs == null) return;
  166.             final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
  167.             childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
  168.             a.recycle();
  169.         }

  170.         public LayoutParams(int width, int height) {
  171.             super(width, height);
  172.         }

  173.         public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
  174.             super(source);
  175.         }

  176.         public LayoutParams(@NonNull MarginLayoutParams source) {
  177.             super(source);
  178.         }
  179.     }}
复制代码
4.3 布局属性定义

作为布局文件,增加属性,标记View类型
  1.   <declare-styleable name="NestedPagerRecyclerViewLayout">
  2.         <attr name="layoutScrollNestedType" format="flags">
  3.             <flag name="Head" value="0"/>
  4.             <flag name="Body" value="1"/>
  5.         </attr>
  6.         <attr name="headExpandedOffset" format="dimension|reference" />
  7.     </declare-styleable>
复制代码
下面是使用时的布局demo,需要设置layoutScrollNestedType

4.4 使用

布局文件
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.smartian.widget.NestedPagerRecyclerViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"
  4.     android:id="@+id/NestedScrollChildLayout"
  5.     android:layout_width="match_parent"
  6.     android:layout_height="match_parent"
  7.     android:focusable="true"
  8.     android:focusableInTouchMode="true"
  9.     app:headExpandedOffset="45dp">

  10.     <LinearLayout
  11.         android:id="@+id/head"
  12.         android:layout_width="match_parent"
  13.         android:layout_height="200dp"
  14.         android:orientation="vertical"
  15.         app:layoutScrollNestedType="Head">

  16.         <TextView
  17.             android:layout_width="match_parent"
  18.             android:layout_height="0dip"
  19.             android:layout_weight="1"
  20.             android:background="@color/colorAccent"
  21.             android:gravity="center"
  22.             android:text="top Head" />

  23.         <LinearLayout
  24.             android:layout_width="match_parent"
  25.             android:layout_height="45dp">

  26.             <TextView
  27.                 android:id="@+id/tab1"
  28.                 android:layout_width="0dip"
  29.                 android:layout_height="45dp"
  30.                 android:layout_weight="1"
  31.                 android:background="@android:color/white"
  32.                 android:gravity="center"
  33.                 android:text="我是tab1" />

  34.             <View
  35.                 android:layout_width="1dip"
  36.                 android:layout_height="match_parent"
  37.                 android:background="@color/colorAccent" />

  38.             <TextView
  39.                 android:id="@+id/tab2"
  40.                 android:layout_width="0dip"
  41.                 android:layout_height="45dp"
  42.                 android:layout_weight="1"
  43.                 android:background="@android:color/white"
  44.                 android:gravity="center"
  45.                 android:text="我是tab2" />
  46.         </LinearLayout>
  47.     </LinearLayout>

  48.     <android.support.v4.view.ViewPager
  49.         android:id="@+id/body"
  50.         android:layout_width="match_parent"
  51.         android:layout_height="match_parent"
  52.         android:background="@color/colorPrimary"
  53.         app:layoutScrollNestedType="Body" />

  54. </com.smartian.widget.NestedPagerRecyclerViewLayout>
复制代码
至此,我们的方案基本实现了,使用方式如下
  1. public class MyNestedScrollViewActivity extends Activity implements View.OnClickListener {
  2.     private ViewPager viewPager;
  3.     private NestedPagerRecyclerViewLayout scrollChildLayout;
  4.     @Override
  5.     protected void onCreate(Bundle savedInstanceState) {
  6.         super.onCreate(savedInstanceState);
  7.         setContentView(R.layout.layout_nested_scrolling_child_layout);
  8.         scrollChildLayout = findViewById(R.id.NestedScrollChildLayout);
  9.         scrollChildLayout.setHeadExpandOffset((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,45,getResources().getDisplayMetrics()));
  10.         viewPager = findViewById(R.id.body);

  11.         findViewById(R.id.tab1).setOnClickListener(this);
  12.         findViewById(R.id.tab2).setOnClickListener(this);

  13.         viewPager.setAdapter(new PagerAdapter() {
  14.             @Override
  15.             public int getCount() {
  16.                 return 2;
  17.             }

  18.             @Override
  19.             public boolean isViewFromObject(@NonNull  View view, Object object) {
  20.                 return view==object;
  21.             }

  22.             @Override
  23.             public void destroyItem(@NonNull  ViewGroup container, int position, @NonNull  Object object) {
  24.                 container.addView((View) object);
  25.             }

  26.             @NonNull
  27.             @Override
  28.             public Object instantiateItem(@NonNull ViewGroup container, int position) {
  29.                 View layoutView = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_recycler_view, container, false);
  30.                 RecyclerView recyclerView = layoutView.findViewById(R.id.recycler_view);
  31.                 recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext()));
  32.                 SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter(container.getContext(), position%2==0?getData():getData2());
  33.                 recyclerView.setAdapter(adapter);
  34.                 container.addView(layoutView);
  35.                 return layoutView;
  36.             }
  37.         });

  38.     }
  39.     private List<String> getData() {
  40.         List<String> data = new ArrayList<>();
  41.         data.add("#ff9999");
  42.         data.add("#ffaa77");
  43.         data.add("#ff9966");
  44.         data.add("#ffcc55");
  45.         data.add("#ff99bb");
  46.         data.add("#ff77dd");
  47.         data.add("#ff33bb");
  48.         data.add("#ff9999");
  49.         data.add("#ffaa77");
  50.         data.add("#ff9966");
  51.         data.add("#ffcc55");
  52.         return data;
  53.     }
  54.     private List<String> getData2() {
  55.         List<String> data = new ArrayList<>();
  56.         data.add("#9999ff");
  57.         data.add("#aa77ff");
  58.         data.add("#9966ff");
  59.         data.add("#cc55ff");
  60.         data.add("#99bbff");
  61.         data.add("#77ddff");
  62.         data.add("#33bbff");
  63.         data.add("#9999ff");
  64.         data.add("#aa77ff");
  65.         data.add("#9966ff");
  66.         data.add("#cc55ff");
  67.         return data;
  68.     }
  69.     @Override
  70.     public void onClick(View v) {
  71.         int id = v.getId();
  72.         if(id==R.id.tab1){
  73.             viewPager.setCurrentItem(0,true);
  74.         }else if(id==R.id.tab2){
  75.             viewPager.setCurrentItem(1,true);
  76.         }
  77.     }
  78. }
复制代码
五、总结

ViewPager、RecyclerView 和Tab吸顶效果实现有一定的难度,其实也有很多实现,但是通用性和易用性都有些问题,因此,即便的是最完美的方案也需要经常调整,因此这类效果很难作为库的方式输出,通过本篇的文章,其实提供了一个现成的模板。
以上就是Android使用Scrolling机制实现Tab吸顶效果的详细内容,更多关于Android Scrolling吸顶的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
12
积分
4
注册时间
2023-12-12
最后登录
2023-12-12

发表于 2024-12-30 16:44:10 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表