自定義ScrollView和TabLayout聯(lián)動(二)

前言:在上一篇文章中我們通過自定義ScrollView實現(xiàn)和TabLayout的聯(lián)動實現(xiàn)了頁面滾動切換Tab的功能,但是遺留了很多bug。本章將會將這些bug統(tǒng)統(tǒng)解決,讓大家更方便使用。如果想要了解實現(xiàn)過程的建議閱讀 自定義ScrollView和TabLayout聯(lián)動(一)

這里先放置上個版本的代碼(簡化版),方便我們理解,如果想要最新版的代碼,可直接滑至底部查看。

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監(jiān)聽
          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: 未設(shè)置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:有讀者反應(yīng)說在快速滑動的時候會出現(xiàn)tab未能切換的問題

經(jīng)過一系列的排查和debug發(fā)現(xiàn),是因為isManualScroll值為false導(dǎo)致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事件分發(fā)的知識我們知道當(dāng)子View設(shè)置setOnClickListener()后,會將事件消費,所以ScrollView的OnTouchListener無法收到ACTION_DOWN事件,那么有沒有辦法可以拿到ACTION_DOWN事件,又不影響子View消費事件呢?那就是重寫
dispatchTouchEvent()方法,這個方法是View用來分發(fā)事件的,也可以重寫onTouchEvent()來實現(xiàn),我們可以將之前的代碼塊移到這里:

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

因為我們只是需要在里面執(zhí)行一些邏輯,不需要消費事件,所以直接返回super.dispatchTouchEvent(ev),這樣問題就解決了。

問題2:在滑動界面的時候去點擊tab會出現(xiàn)tab切換失敗的問題。

這個問題其實在自己使用的時候也發(fā)現(xiàn)了,但是因為換工作的原因沒有去及時解決。今天就來做個了斷吧。
首先在OnTabSelectedListener中的三個方法中打印一下日志,方便我們排查問題。當(dāng)出現(xiàn)這種問題的時候去點擊tabLayout發(fā)現(xiàn)并沒有打印onTabSelected方法,而是打印了onTabReselected方法。這就奇怪了,在點擊之前tabLayout選中的明明是第三個,而點擊第二個tab的時候卻走了onTabReselected方法呢。
然后我又通過debug發(fā)現(xiàn)雖然界面顯示第三個是選中狀態(tài),但代碼中selectedTab的position卻是第二個,所以當(dāng)點擊第二個tab走的是onTabReselected。原來如此,沒想到它竟然是個表里不一的家伙。怎么搞定他呢,此時我突然想到viewpager可以和TabLayout聯(lián)動,那她在切換page的時候是怎么處理的呢,我們?nèi)ヒ惶骄烤梗罱K我發(fā)現(xiàn)這塊代碼在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);
            }

        }

通過這個代碼我們知道,當(dāng)viewPager滑動完成時會調(diào)用tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator),而滑動結(jié)束后,頁面會被選中,然后會執(zhí)行onPageSelected()方法,方法中主要執(zhí)行了tabLayout.selectTab()方法。這么看來主要方法也就是setScrollPosition和selectTap方法了。
通過查閱資料了解了這兩個方法的作用
setScrollPosition:調(diào)用此方法不會更新所選的選項卡,它僅用于繪圖目的。
selectTap:選擇給定的標簽。
真相大白了,原來setScrollPosition只是徒有其表啊,要想真正改變selectab,還是要靠selectTab。簡單,不就是改個方法么。
mTabLayout.se(提示呢?)
mTabLayout.select(我都快打完了還不提示)
mTabLayout.selectTab(紅色的?有點不對勁呀)
回到tabLayout中一看,他竟然不是public方法,好吧,看來我之前這樣寫是有原因的,不過也難不倒我,平時在使用viewPager+TabLayout的時候會有時候會通過mTabLayout.getTabAt(0).select()設(shè)置默認頁面,所以我們可以使用這個方法來更新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。
看了下日志輸入,發(fā)現(xiàn)onTabSelected被調(diào)用了,怪不得,因為這樣就和點擊tab的邏輯錯亂了。那我們就需要區(qū)分一下onTabSelected的觸發(fā)事件。我們增加一個boolean變量mSelectTabFlag用于區(qū)分觸發(fā)事件,代碼如下:

    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觸發(fā)
                // smoothScrollTo可以平滑的滑動到指定位置,并打斷慣性滑動
                smoothScrollTo(0, getViewTop(oldPosition));
            } else { //通過滑動時切換Tab觸發(fā)
                isManualScroll = true;
            }
            mSelectTabFlag = false;
        }

如果是滑動觸發(fā)的切換tab,則將mSelectTabFlag設(shè)置為true,然后在onTabSelected中取反重新賦值,這樣由滑動觸發(fā)切換tab后mSelectTabFlag值為false,由點擊tabLayout觸發(fā)則為true,然后在邏輯執(zhí)行完畢后將mSelectTabFlag重置為false。這樣就可以正常運行了

圖片過大,稍后整理上傳

TabWithScrollView新版完整代碼:

/**
 * Created by Hao on 2019/7/21.
 * Describe ScrollView和TabLayout的聯(lián)動
 */
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;

    /**
     * 需要聯(lián)動的tabLayout
     */
    private TabLayout mTabLayout;

    /**
     * ScrollView的滑動回調(diào)
     */
    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數(shù)量和視圖View的數(shù)量不一致");
        }
        return mViewList.get(position).getTop() - mTranslationY;
    }

    /**
     * 設(shè)置選中的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();
            }
        }
    }

    /**
     * 設(shè)置綁定的tabLayout,并給tabLayout添加OnTabSelectedListener監(jiān)聽
     *
     * @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觸發(fā)
                // smoothScrollTo可以平滑的滑動到指定位置,并打斷慣性滑動
                smoothScrollTo(0, getViewTop(oldPosition));
            } else { //通過滑動時切換Tab觸發(fā)
                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的滾動回調(diào)
     */
    public interface OnScrollCallback {
        void onScrollCallback(int l, int t, int oldl, int oldt);
    }

}

源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容