自定义ScrollView和TabLayout联动(二)

前言:在上一篇文章中我们通过自定义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);
    }

}

源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351