美文网首页高级UI
自定义ScrollView和TabLayout联动(一)

自定义ScrollView和TabLayout联动(一)

作者: 岁月静好丶丶丶 | 来源:发表于2019-07-26 15:01 被阅读0次

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


    详情界面.gif

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

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

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

            List<View> 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<View> 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<View> 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<View> 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/ilatent/CustomView

    相关文章

      网友评论

        本文标题:自定义ScrollView和TabLayout联动(一)

        本文链接:https://www.haomeiwen.com/subject/eeqilctx.html