实现ScrollView和tabLayout的联动

最近在做新项目,需要实现一个淘宝商品详情页的效果,根据滚动的距离切换顶部的tab标签,点击tab标签可以滚动到指定位置。第一次看到这个效果想到的实现方案就是ScrollView和tabLayou的联动,但是因为对view的滑动事件不是很了解,很多方法也不知道,所以只能去百度、谷歌了。在查找的过程中发现RecyclerView也可以实现这个效果,想着RecyclerView实现的话,由于每个内容板块都是一个item,滑动定位应该会很准,不过由于多个请求,需要重新组装数据,有感觉很麻烦,放弃了,现在想起来是有点懒。回到ScrollView的实现方案后查到了一个自定义的ScrollView,只需要将每个内容板块的高度计算好后传到ScrollView,由他内部去处理就可以了,感觉使用起来很简单,就选择了他。一顿操作下载终于实现了这个效果,不过当去使用的时候发现好多问题,比如滑动切换标签的时机都是错的,(这个是因为请求数据后,对一些view隐藏显示导致的,后来选择在每次请求数据后都去重新计算高度,不过效果还是有点差),还有当ScrollView处于惯性滑动时,切换标签无法停止惯性滑动,还有tablayout的指示器和选中状态不统一各种问题,当时有点菜一个也没解决。这次新项目再次用到了这个效果,我觉得是时候去研究一下了原理了,就开始看那个自定义的ScrollView尝试去理解它,一个一个的去解决问题。首先看一下实现后的效果:


详情界面.gif

下面是遇到的问题,我们一个一个的去解决:

1.切换标签的滑动距离计算的不对:

因为项目中这个界面有3个请求,如果按照之前的写法,我需要在3个请求的成功回调中去重新计算高度,还有一些大图,可能无法立即显示,这个高度也无法去准确的计算出来,所以我干脆不再提前计算好,而是直接将内容板块的view传递过去,实时计算view距离顶部的距离,再根据距离顶部的距离去切换tab。

        List views = new LinkedList<>();
        views.add(view1);
        views.add(view2);
        views.add(view3);
        views.add(view4);
        scrollView.setAnchorList(views); // 设置视图集合

重写ScrollView的onScrollChanged方法,去循环views,然后使用getViewTop()获取view距离顶部的高度,如果滑动距离t大于该view距顶部的高度,就使用setSelectedTab()切换到该tab标签。

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mViewList == null) {
            return;
        }
        for (int i = mViewList.size() - 1; i >= 0; i--) {
            if (t > getScrollDistance(i)) {
                // 当达到该View的高度时去切换
                setSelectedTab(i);
                break;
            }
        }
    }

    /**
     * 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
     *
     * @param position
     * @return
     */
    private int getViewTop(int position) {
        if (position >= mViewList.size() + 1) {
            throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
        }
        return mViewList.get(position).getTop() - mTranslationY;
    }

    /**
     * 设置选中的tab标签
     *
     * @param position
     */
    private void setSelectedTab(int position) {
        if (mTabLayout != null && position != oldPosition) {
            // 该方法不会走tabLayout的onTabSelected监听
            mTabLayout.setScrollPosition(position, 0, true);
        }
        oldPosition = position;
    }

这样切换标签时机就非常准确了。

问题2:惯性滑动时去切换tab无法终止惯性滑动,导致会自己滑动到下个标签。

造成这个问题的原因其实是因为调用方法错误的原因,之前调用的是scrollTo(x, y)方法,改成smoothScrollTo(x, y)就可以了。下面是两个方法的区别

scrollTo(x, y); // 该方法会立即滚动到指定的坐标,不会终止正在滑动的事件。

smoothScrollTo(x, y);// 该方法会平缓的滚动到指定的坐标,并且会终止正在滚动的事件。

问题3:tab标签的指示器和选择tab的字体颜色错乱

其实造成这个的原因和问题2有关系,在我点击tab切换标签的时候,scrollview又惯性滚动到了下个标签,导致tabLayou没有及时的切换所有的状态造成的,解决了问题2该问题也不再出现。这下问题都解决了,回过头来发现其实问题都很简单,只是当时有点摸不着头脑,而且从来没有去看里面的实现逻辑,就打了退堂鼓觉得自己不能理解,以后要拒绝当伸手党,应该多看看源码,理解里面的原理。

为了更方便的使用,我将tabLayout也传递进去了,这样只需要几行代码就可以实现这个效果了,实现起来就两步
1.第一步就是将内容板块的view集合传递进来

        List views = new LinkedList<>();
        views.add(llCommodity);
        views.add(cardEvaluate);
        views.add(llDetails);
        views.add(llRecommend);
        scrollView.setAnchorList(views); // 设置视图集合

2.第二部绑定tabLayout

scrollView.setupWithTabLayout(tabLayout); // 设置绑定的tabLayout

3.如果还想实现一个标题栏的渐变,可以设置滑动监听和距离顶部的偏差值

// 因为标题栏在ScrollView上面,所以需要设置一个距离顶部的偏差值(标题栏的高度+状态栏的高度),防止标题栏遮盖布局
scrollView.setTranslationY(SizeUtils.getMeasuredHeight(toolbar) + BarUtils.getStatusBarHeight());
scrollView.setOnScrollCallback(new TabWithScrollView.OnScrollCallback() {
            @Override
            public void onScrollCallback(int l, int t, int oldl, int oldt) {
                setBgAlphaChange(t, height);
            }
        });

下面是整个类的代码以及源码:

/**
* Created by Hao on 2019/7/21.
* Describe ScrollView和TabLayout的联动
*/
public class TabWithScrollView extends ScrollView {

  private static final String TAG = "TabWithScrollView";

  /**
   * 模块View的集合
   */
  private List mViewList;

  /**
   * 是否是ScrollView引起的滑动,true-是,false-TabLayout引起的滑动
   */
  private boolean isManualScroll;

  /**
   * 记录上一次点击的position,防止多次点击
   */
  private int oldPosition = 0;

  /**
   * 需要联动的tabLayout
   */
  private TabLayout mTabLayout;

  /**
   * ScrollView的滑动回调
   */
  private OnScrollCallback onScrollCallback;

  /**
   * 距离顶部的偏移量,默认为10px;
   */
  private int mTranslationY = 10;


  public TabWithScrollView(Context context) {
      super(context);
      setOnTouchListener();
  }

  public TabWithScrollView(Context context, AttributeSet attrs) {
      super(context, attrs);
      setOnTouchListener();
  }

  public TabWithScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
      super(context, attrs, defStyleAttr);
      setOnTouchListener();
  }

  @SuppressLint("ClickableViewAccessibility")
  public void setOnTouchListener() {
      super.setOnTouchListener(new OnTouchListener() {
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              if (event.getAction() == MotionEvent.ACTION_DOWN) {
                  isManualScroll = true;
              }
              return false;
          }
      });
  }

  @Override
  protected void onScrollChanged(int l, int t, int oldl, int oldt) {
      super.onScrollChanged(l, t, oldl, oldt);
      if (onScrollCallback != null) {
          onScrollCallback.onScrollCallback(l, t, oldl, oldt);
      }
      if (isManualScroll) {
          if (mViewList == null) {
              return;
          }
          for (int i = mViewList.size() - 1; i >= 0; i--) {
              if (t > getViewTop(i)) {
                  setSelectedTab(i);
                  break;
              }
          }
      }
  }

  /**
   * 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
   *
   * @param position
   * @return
   */
  private int getViewTop(int position) {
      if (position >= mViewList.size() + 1) {
          throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
      }
      return mViewList.get(position).getTop() - mTranslationY;
  }

  /**
   * 设置选中的tab标签
   *
   * @param position
   */
  private void setSelectedTab(int position) {
      if (mTabLayout != null && position != oldPosition) {
          // 该方法不会走tabLayout的onTabSelected监听
          mTabLayout.setScrollPosition(position, 0, true);
      }
      oldPosition = position;
  }

  /**
   * 设置绑定的tabLayout,并给tabLayout添加OnTabSelectedListener监听
   *
   * @param tabLayout
   */
  public void setupWithTabLayout(TabLayout tabLayout) {
      if (mTabLayout != null) {
          mTabLayout.removeOnTabSelectedListener(mTabSelectedListener);
      }
      if (tabLayout != null) {
          mTabLayout = tabLayout;
          mTabLayout.addOnTabSelectedListener(mTabSelectedListener);
      }
  }

  public void setAnchorList(List anchorList) {
      this.mViewList = anchorList;
  }

  public void setOnScrollCallback(OnScrollCallback onScrollCallback) {
      this.onScrollCallback = onScrollCallback;
  }

  public void setTranslationY(int translationY) {
      this.mTranslationY = translationY;
  }

  TabLayout.OnTabSelectedListener mTabSelectedListener = new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
          isManualScroll = false;
          if (mViewList == null) {
              Log.i(TAG, "onTabSelected: 未设置View集合");
              return;
          }
          // smoothScrollTo可以平滑的滑动到指定位置,并打断惯性滑动
          smoothScrollTo(0, getViewTop(tab.getPosition()));
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
  };

  /**
   * ScrollView的滚动回调
   */
  public interface OnScrollCallback {
      void onScrollCallback(int l, int t, int oldl, int oldt);
  }

}

源码地址:https://github.com/returntoorigin/CustomView

你可能感兴趣的:(实现ScrollView和tabLayout的联动)