美文网首页
自定义ScrollView和TabLayout联动(二)

自定义ScrollView和TabLayout联动(二)

作者: 岁月静好丶丶丶 | 来源:发表于2020-07-21 17:22 被阅读0次

    前言:在上一篇文章中我们通过自定义ScrollView实现和TabLayout的联动实现了页面滚动切换Tab的功能,但是遗留了很多bug。本章将会将这些bug统统解决,让大家更方便使用。如果想要了解实现过程的建议阅读 自定义ScrollView和TabLayout联动(一)

    这里先放置上个版本的代码(简化版),方便我们理解,如果想要最新版的代码,可直接滑至底部查看。

    public class TabWithScrollView extends ScrollView {
    
      private static final String TAG = "TabWithScrollView";
      private List<View> mViewList;
      private boolean isManualScroll;
      private int oldPosition = 0;
      private TabLayout mTabLayout;
      private OnScrollCallback onScrollCallback;
      private int mTranslationY = 10;
    
      @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;
                  }
              }
          }
      }
      private int getViewTop(int position) {
          ...
      }
      private void setSelectedTab(int position) {
          if (mTabLayout != null && position != oldPosition) {
              // 该方法不会走tabLayout的onTabSelected监听
              mTabLayout.setScrollPosition(position, 0, true);
          }
          oldPosition = position;
      }
      public void setupWithTabLayout(TabLayout tabLayout) {
          ...
      }
      public void setAnchorList(List<View> anchorList) {
          ...
      }
      public void setOnScrollCallback(OnScrollCallback onScrollCallback) {
          ...
      }
      public void setTranslationY(int 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) {
          }
      };
      public interface OnScrollCallback {
          ...
      }
    }
    

    问题1:有读者反应说在快速滑动的时候会出现tab未能切换的问题

    经过一系列的排查和debug发现,是因为isManualScroll值为false导致onScrollChanged中的切换tab的逻辑没有走,而isManualScroll没有被正确的赋值的原因是setOnTouchListener没有收到ACTION_DOWN的事件。之前的代码:

      @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;
              }
          });
      }
    

    通过Android事件分发的知识我们知道当子View设置setOnClickListener()后,会将事件消费,所以ScrollView的OnTouchListener无法收到ACTION_DOWN事件,那么有没有办法可以拿到ACTION_DOWN事件,又不影响子View消费事件呢?那就是重写
    dispatchTouchEvent()方法,这个方法是View用来分发事件的,也可以重写onTouchEvent()来实现,我们可以将之前的代码块移到这里:

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                Log.i(TAG, "onTouch: ACTION_DOWN");
                isManualScroll = true;
            }
            return super.dispatchTouchEvent(ev);
        }
    

    因为我们只是需要在里面执行一些逻辑,不需要消费事件,所以直接返回super.dispatchTouchEvent(ev),这样问题就解决了。

    问题2:在滑动界面的时候去点击tab会出现tab切换失败的问题。

    这个问题其实在自己使用的时候也发现了,但是因为换工作的原因没有去及时解决。今天就来做个了断吧。
    首先在OnTabSelectedListener中的三个方法中打印一下日志,方便我们排查问题。当出现这种问题的时候去点击tabLayout发现并没有打印onTabSelected方法,而是打印了onTabReselected方法。这就奇怪了,在点击之前tabLayout选中的明明是第三个,而点击第二个tab的时候却走了onTabReselected方法呢。
    然后我又通过debug发现虽然界面显示第三个是选中状态,但代码中selectedTab的position却是第二个,所以当点击第二个tab走的是onTabReselected。原来如此,没想到它竟然是个表里不一的家伙。怎么搞定他呢,此时我突然想到viewpager可以和TabLayout联动,那她在切换page的时候是怎么处理的呢,我们去一探究竟,最终我发现这块代码在TabLayout的TabLayoutOnPageChangeListener类中:

            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                TabLayout tabLayout = (TabLayout)this.tabLayoutRef.get();
                if (tabLayout != null) {
                    boolean updateText = this.scrollState != 2 || this.previousScrollState == 1;
                    boolean updateIndicator = this.scrollState != 2 || this.previousScrollState != 0;
                    tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
                }
    
            }
    
            public void onPageSelected(int position) {
                TabLayout tabLayout = (TabLayout)this.tabLayoutRef.get();
                if (tabLayout != null && tabLayout.getSelectedTabPosition() != position && position < tabLayout.getTabCount()) {
                    boolean updateIndicator = this.scrollState == 0 || this.scrollState == 2 && this.previousScrollState == 0;
                    tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
                }
    
            }
    

    通过这个代码我们知道,当viewPager滑动完成时会调用tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator),而滑动结束后,页面会被选中,然后会执行onPageSelected()方法,方法中主要执行了tabLayout.selectTab()方法。这么看来主要方法也就是setScrollPosition和selectTap方法了。
    通过查阅资料了解了这两个方法的作用
    setScrollPosition:调用此方法不会更新所选的选项卡,它仅用于绘图目的。
    selectTap:选择给定的标签。
    真相大白了,原来setScrollPosition只是徒有其表啊,要想真正改变selectab,还是要靠selectTab。简单,不就是改个方法么。
    mTabLayout.se(提示呢?)
    mTabLayout.select(我都快打完了还不提示)
    mTabLayout.selectTab(红色的?有点不对劲呀)
    回到tabLayout中一看,他竟然不是public方法,好吧,看来我之前这样写是有原因的,不过也难不倒我,平时在使用viewPager+TabLayout的时候会有时候会通过mTabLayout.getTabAt(0).select()设置默认页面,所以我们可以使用这个方法来更新tab。

        private void setSelectedTab(int position) {
            if (mTabLayout != null && position != oldPosition) {
                Log.i(TAG, "setSelectedTab: " + position);
                oldPosition = position;
                TabLayout.Tab newTab = mTabLayout.getTabAt(position);
                if (newTab != null) {
                    newTab.select();
                }
            }
        }
    

    走起

    图片过大,稍后整理上传

    卡顿,tab切换错误,我怎么感觉我是在写bug。
    看了下日志输入,发现onTabSelected被调用了,怪不得,因为这样就和点击tab的逻辑错乱了。那我们就需要区分一下onTabSelected的触发事件。我们增加一个boolean变量mSelectTabFlag用于区分触发事件,代码如下:

        private void setSelectedTab(int position) {
            if (mTabLayout != null && position != oldPosition) {
                Log.i(TAG, "setSelectedTab: " + position);
                oldPosition = position;
                TabLayout.Tab newTab = mTabLayout.getTabAt(position);
                if (newTab != null) {
                    mSelectTabFlag = true;
                    newTab.select();
                }
            }
        }
    
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                oldPosition = tab.getPosition();
                isManualScroll = false;
                mSelectTabFlag = !mSelectTabFlag;
                if (mViewList == null) {
                    return;
                }
                if (mSelectTabFlag) { // 通过点击Tab触发
                    // smoothScrollTo可以平滑的滑动到指定位置,并打断惯性滑动
                    smoothScrollTo(0, getViewTop(oldPosition));
                } else { //通过滑动时切换Tab触发
                    isManualScroll = true;
                }
                mSelectTabFlag = false;
            }
    

    如果是滑动触发的切换tab,则将mSelectTabFlag设置为true,然后在onTabSelected中取反重新赋值,这样由滑动触发切换tab后mSelectTabFlag值为false,由点击tabLayout触发则为true,然后在逻辑执行完毕后将mSelectTabFlag重置为false。这样就可以正常运行了

    图片过大,稍后整理上传

    TabWithScrollView新版完整代码:

    /**
     * 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;
    
        private boolean mSelectTabFlag = false;
    
    
        public TabWithScrollView(Context context) {
            super(context);
        }
    
        public TabWithScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public TabWithScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                Log.i(TAG, "onTouch: ACTION_DOWN");
                isManualScroll = true;
            }
            return super.dispatchTouchEvent(ev);
        }
    
        @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) {
                Log.i(TAG, "setSelectedTab: " + position);
                oldPosition = position;
                TabLayout.Tab newTab = mTabLayout.getTabAt(position);
                if (newTab != null) {
                    mSelectTabFlag = true;
                    newTab.select();
                }
            }
        }
    
        /**
         * 设置绑定的tabLayout,并给tabLayout添加OnTabSelectedListener监听
         *
         * @param tabLayout
         */
        public void setupWithTabLayout(TabLayout tabLayout) {
            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) {
                oldPosition = tab.getPosition();
                isManualScroll = false;
                mSelectTabFlag = !mSelectTabFlag;
                if (mViewList == null) {
                    return;
                }
                if (mSelectTabFlag) { // 通过点击Tab触发
                    // smoothScrollTo可以平滑的滑动到指定位置,并打断惯性滑动
                    smoothScrollTo(0, getViewTop(oldPosition));
                } else { //通过滑动时切换Tab触发
                    isManualScroll = true;
                }
                mSelectTabFlag = false;
            }
    
            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                Log.i(TAG, "onTabUnselected: " + tab.getPosition());
            }
    
            @Override
            public void onTabReselected(TabLayout.Tab tab) {
                Log.i(TAG, "onTabReselected: " + tab.getPosition());
            }
        };
    
        /**
         * ScrollView的滚动回调
         */
        public interface OnScrollCallback {
            void onScrollCallback(int l, int t, int oldl, int oldt);
        }
    
    }
    

    源码地址

    相关文章

      网友评论

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

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