• 注册
  • 前端博客 前端博客 关注:0 内容:908

    Android NestedScrolling实现ViewPager列表切换

  • 查看作者
  • 打赏作者
  • 当前位置: 职业司 > 前端开发 > 前端博客 > 正文
    • 前端博客
    • 一、需求简介

      app首页中经常要实现首页头卡共享,tab吸顶,内容区通过ViewPager切换的需求,以前往往是利用事件处理来完成,还有Google官方也提供了相关的库,但是这些也有一定的弊端,适配起来还是比较复杂。这里我们利用NestedScrolling机制来实现,之前的博客中也有一篇类似的博客《Android 利用NestedScrolling联动机制为RecyclerView添加Header》。这个版本实际上在之前的版本上进行了一些扩展,使其能支持ViewPager。

      当然也有很多开源项目,发现存在的问题很多面,主要问题如下:

      • 头部和内容区域不联动
      • 没有中断RecyclerView 的fling效果,导致RecyclerView抢占ViewPager事件
      • 侵入式设计太多(当然,本篇方案解决RecyclerView中断fling时用了侵入式设计)

      二、效果展示

      三、代码实现

      要点:

      主要代码

      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
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int childCount = getChildCount();
      int height = MeasureSpec.getSize(heightMeasureSpec);
      int overScrollExtent = overScrollExtent();
      for (int i = 0; i < childCount; i++) {
      View child = getChildAt(i);
      LayoutParams lp = (LayoutParams) child.getLayoutParams();
      if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
      final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
      getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
      + 0, lp.width);
      final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
      getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
      + 0, height - overScrollExtent);
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      }
      }
      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) {
      for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      float childX = (child.getX() - getScrollX());
      float childY = (child.getY() - getScrollY());
      if (currentX < childX || currentX > (childX + child.getWidth())) {
      continue;
      }
      if (currentY < childY || currentY > (childY + child.getHeight())) {
      continue;
      }
      return child;
      }
      return null;
      }
      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
      public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
      if (axes == SCROLL_AXIS_VERTICAL) {
      //只关注垂直方向的移动
      int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
      int offset = computeVerticalScrollOffset();
      if (offset <= maxOffset) {
      mVerticalScrollView = target;
      return true;
      }
      } else {
      mVerticalScrollView = null;
      }
      return false;
      }
      @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) {
      if (dy == 0) {
      return;
      }
      if (!canNestedScrollView(mVerticalScrollView)) {
      //这里要判断向上滑动问题,
      // 如果当前布局可以向上滑动,优先滑动,不然头部可能出现露一半但无法向上滑动的问题
      if (dy < 0) {
      return;
      }
      if (!allowScroll(dy)) {
      return;
      }
      }
      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;
      }
      consumed[1] = dyOffset;
      Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
      scrollBy(0, dyOffset);
      }
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
      int scrollRange = computeVerticalScrollRange();
      if (scrollRange <= getHeight()) {
      return super.dispatchTouchEvent(event);
      }
      if (mVelocityTracker == null) {
      mVelocityTracker = VelocityTracker.obtain();
      }
      int action = event.getAction();
      switch (action) {
      case MotionEvent.ACTION_DOWN:
      mVelocityTracker.addMovement(event);
      startEventX = event.getX();
      startEventY = event.getY();
      isTouchMoving = false;
      if (mVerticalScrollView instanceof RecyclerView) {
      /**
      *RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
      *调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
      */
      ((RecyclerView) mVerticalScrollView).stopScroll();
      } else if (mVerticalScrollView instanceof NestedScrollingChild) {
      mVerticalScrollView.stopNestedScroll();
      }
      break;
      case MotionEvent.ACTION_MOVE:
      float currentX = event.getX();
      float currentY = event.getY();
      float dx = currentX - startEventX;
      float dy = currentY - startEventY;
      if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
      startEventX = currentX;
      startEventY = currentY;
      break;
      }
      View touchView = null;
      int offset = (int) -dy;
      if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
      touchView = findTouchView(currentX, currentY);
      //这里只关注头卡触摸事件即可
      isTouchMoving = touchView != null && touchView == getHeaderView();
      }
      if (isTouchMoving && !allowScroll(offset)) {
      isTouchMoving = false;
      }
      startEventX = currentX;
      startEventY = currentY;
      if (!isTouchMoving) {
      break;
      }
      mVelocityTracker.addMovement(event);
      int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
      int scrollOffset = computeVerticalScrollOffset();
      int targetOffset = scrollOffset + offset;
      if (targetOffset >= maxOffset) {
      offset = maxOffset - scrollOffset;
      }
      if (targetOffset <= 0) {
      offset = 0 - scrollOffset;
      }
      if (offset != 0) {
      scrollBy(0, offset);
      }
      Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
      super.dispatchTouchEvent(event);
      return true;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_OUTSIDE:
      mVelocityTracker.addMovement(event);
      if (isTouchMoving) {
      isTouchMoving = false;
      mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
      startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
      mVelocityTracker.recycle();
      mVelocityTracker = null;
      }
      break;
      }
      return super.dispatchTouchEvent(event);
      }
      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 {
      public final static int TYPE_HEAD = 0;
      public final static int TYPE_BODY = 1;
      private int childLayoutType = TYPE_HEAD;
      public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
      super(c, attrs);
      if (attrs == null) return;
      final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
      childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
      a.recycle();
      }
      public LayoutParams(int width, int height) {
      super(width, height);
      }
      public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
      super(source);
      }
      public LayoutParams(@NonNull MarginLayoutParams source) {
      super(source);
      }
      }
      }
      

      属性定义:

        <declare-styleable name="NestedPagerRecyclerViewLayout">
      <attr name="layoutScrollNestedType" format="flags">
      <flag name="Head" value="0"/>
      <flag name="Body" value="1"/>
      </attr>
      <attr name="headExpandedOffset" format="dimension|reference" />
      </declare-styleable>

      布局文件

      <?xml version="1.0" encoding="utf-8"?>
      <com.smartian.widget.NestedPagerRecyclerViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:id="@+id/NestedScrollChildLayout"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:focusable="true"
      android:focusableInTouchMode="true"
      app:headExpandedOffset="45dp">
      <LinearLayout
      android:id="@+id/head"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:orientation="vertical"
      app:layoutScrollNestedType="Head">
      <TextView
      android:layout_width="match_parent"
      android:layout_height="0dip"
      android:layout_weight="1"
      android:background="@color/colorAccent"
      android:gravity="center"
      android:text="top Head" />
      <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="45dp">
      <TextView
      android:id="@+id/tab1"
      android:layout_width="0dip"
      android:layout_height="45dp"
      android:layout_weight="1"
      android:background="@android:color/white"
      android:gravity="center"
      android:text="我是tab1" />
      <View
      android:layout_width="1dip"
      android:layout_height="match_parent"
      android:background="@color/colorAccent" />
      <TextView
      android:id="@+id/tab2"
      android:layout_width="0dip"
      android:layout_height="45dp"
      android:layout_weight="1"
      android:background="@android:color/white"
      android:gravity="center"
      android:text="我是tab2" />
      </LinearLayout>
      </LinearLayout>
      <android.support.v4.view.ViewPager
      android:id="@+id/body"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="@color/colorPrimary"
      app:layoutScrollNestedType="Body" />
      </com.smartian.widget.NestedPagerRecyclerViewLayout>

      至此,我们的方案基本实现了,使用方式如下

      public class MyNestedScrollViewActivity extends Activity implements View.OnClickListener {
      private ViewPager viewPager;
      private NestedPagerRecyclerViewLayout scrollChildLayout;
      @Override
      protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.layout_nested_scrolling_child_layout);
      scrollChildLayout = findViewById(R.id.NestedScrollChildLayout);
      scrollChildLayout.setHeadExpandOffset((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,45,getResources().getDisplayMetrics()));
      viewPager = findViewById(R.id.body);
      findViewById(R.id.tab1).setOnClickListener(this);
      findViewById(R.id.tab2).setOnClickListener(this);
      viewPager.setAdapter(new PagerAdapter() {
      @Override
      public int getCount() {
      return 2;
      }
      @Override
      public boolean isViewFromObject(@NonNull  View view, Object object) {
      return view==object;
      }
      @Override
      public void destroyItem(@NonNull  ViewGroup container, int position, @NonNull  Object object) {
      container.addView((View) object);
      }
      @NonNull
      @Override
      public Object instantiateItem(@NonNull ViewGroup container, int position) {
      View layoutView = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_recycler_view, container, false);
      RecyclerView recyclerView = layoutView.findViewById(R.id.recycler_view);
      recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext()));
      SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter(container.getContext(), position%2==0?getData():getData2());
      recyclerView.setAdapter(adapter);
      container.addView(layoutView);
      return layoutView;
      }
      });
      }
      private List<String> getData() {
      List<String> data = new ArrayList<>();
      data.add("#ff9999");
      data.add("#ffaa77");
      data.add("#ff9966");
      data.add("#ffcc55");
      data.add("#ff99bb");
      data.add("#ff77dd");
      data.add("#ff33bb");
      data.add("#ff9999");
      data.add("#ffaa77");
      data.add("#ff9966");
      data.add("#ffcc55");
      return data;
      }
      private List<String> getData2() {
      List<String> data = new ArrayList<>();
      data.add("#9999ff");
      data.add("#aa77ff");
      data.add("#9966ff");
      data.add("#cc55ff");
      data.add("#99bbff");
      data.add("#77ddff");
      data.add("#33bbff");
      data.add("#9999ff");
      data.add("#aa77ff");
      data.add("#9966ff");
      data.add("#cc55ff");
      return data;
      }
      @Override
      public void onClick(View v) {
      int id = v.getId();
      if(id==R.id.tab1){
      viewPager.setCurrentItem(0,true);
      }else if(id==R.id.tab2){
      viewPager.setCurrentItem(1,true);
      }
      }
      }
      

      请登录之后再进行评论

      登录

      手机阅读天地(APP)

      • 微信公众号
      • 微信小程序
      • 安卓APP
      手机浏览,惊喜多多
      匿名树洞,说我想说!
      问答悬赏,VIP可见!
      密码可见,回复可见!
      即时聊天、群聊互动!
      宠物孵化,赠送礼物!
      动态像框,专属头衔!
      挑战/抽奖,金币送不停!
      赶紧体会下,不会让你失望!
    • 实时动态
    • 签到
    • 做任务
    • 发表内容
    • 偏好设置
    • 到底部
    • 帖子间隔 侧栏位置:
    • 还没有账号?点这里立即注册