TvTabLayout——封裝一個Android TV端易用的TabLayout

在這裏插入圖片描述

概述

Tab + ViewPager是我們常用的一種Android端UI架構,Android系統提供了TabLayout等控件用於實現與ViewPager配套使用問題,但是原生的TabLayout使用在Android TV端並不友好,需要添加複雜的邏輯解決按鍵與焦點問題,本文旨在封裝一個簡單的TvTabLayout控件,方便實現Android TV端的常用功能。

TvTabLayout

簡介

  • 參考TabLayout抽取了核心的切換邏輯
  • 添加了按鍵處理
  • 添加焦點效果處理邏輯
  • 修改了TabLayout TabView高度封裝的特性,開放TabView可定製UI效果
  • 支持三種Tab切換滾動模式

本控件只實現了控制ViewPager和切換Tab的核心邏輯,功能性、穩定性和在代碼封裝上遠不及TabLayout和其他開源框架,但是用於學習參考和小項目的使用也足夠。

使用

佈局

    <com.zhong.demo.fragment.widget.TVTabLayout
        android:id="@+id/tv_tab_layout"
        android:layout_width="300dp"
        android:layout_height="@dimen/tab_height"
        android:layout_centerHorizontal="true"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/man_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/tv_tab_layout"/>

代碼

    private void initTabLayout(){
        for(int i = 0; i < fragmentList.size(); i++){
            TextView tabView = (TextView) getTabView("TAG" + i);
            tabViewList.add(tabView);
            TVTabLayout.TabView tab = tabLayout.newTabView().setCustomView(tabView);
            tabLayout.addTab(tab);
        }
        //綁定ViewPager
        tabLayout.setupWithViewPager(viewPager);
        //設置Tab焦點框
        tabLayout.setTabFocusedBackground(R.drawable.focused_bg);
        //設置tab間距
        tabLayout.setTabsMargin(10);
        //設置模式:選中Tab居中模式
        tabLayout.setMode(TVTabLayout.MODE_SCROLLABLE_INDICATOR_CENTER);
        //添加監聽,處理TabView的UI效果
        tabLayout.addOnTabSelectedListener(new TVTabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TVTabLayout.TabView view, int index) {
                tabViewList.get(index).setTextColor(Color.RED);
            }

            @Override
            public void onTabUnSelected(TVTabLayout.TabView view, int index) {
                tabViewList.get(index).setTextColor(Color.BLACK);
            }
        });
    }

    /**
     * 獲取TabView
     * @param title
     * @return
     */
    private View getTabView(String title){
        TextView customView = new TextView(TVTabLayoutActivity.this);
        customView.setPadding(15,7,15,7);
        customView.setGravity(Gravity.CENTER);
        customView.setTextColor(Color.BLACK);
        customView.setText(title);
        return customView;
    }

實現思路

Tab相關邏輯封裝

封裝一個TabView,並提供了setCustomView(View)方法實現自定義樣式。

    public static final class TabView extends FrameLayout {
        private View mCustomView;

        //省略部分無關代碼

        @NonNull
        public TabView setCustomView(@Nullable View view) {
            mCustomView = view;
            updateView();
            return this;
        }

        void updateView() {
            if (mCustomView != null) {
                LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
                addView(mCustomView,layoutParams);
            }
        }
    }

使用時通過TvTabLayout.newTabView()來實例化一個TabView,通過addTab(TabView)添加TabView。

    public void addTab(@NonNull TabView view, int tabsMargin){
        view.setFocusable(false);
        view.setFocusableInTouchMode(false);
        mTabs.add(view);
        tabViewLinearLayout.addView(view, createLayoutParamsForTabs());

        if(mSelectedIndex == mTabs.indexOf(view)){
            setSelectTabView(mSelectedIndex);
        }
    }

    private LinearLayout.LayoutParams createLayoutParamsForTabs() {
        final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //設置Tab間距
        lp.leftMargin = tabsMargin;
        updateTabViewLayoutParams(lp);
        return lp;
    }

    private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
        //根據模式設置Tab尺寸
        if (mMode == MODE_FIXED_NON_SCROLLABLE) {
            lp.width = 0;
            lp.weight = 1;
        } else {
            lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
            lp.weight = 0;
        }
    }

處理按鍵事件重寫dispatchKeyEvent(KeyEvent)方法添加左右按鍵處理邏輯,並添加了選中邊界Tab可循環滾動邏輯

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if(event.getAction() != KeyEvent.ACTION_DOWN){
            return super.dispatchKeyEvent(event);
        }

        switch (event.getKeyCode()){
            case KeyEvent.KEYCODE_DPAD_LEFT:
                int currentTabIndexLeft = mSelectedIndex;
                if(isLoop){
                    int nextLeftPosition = currentTabIndexLeft > 0 ? --currentTabIndexLeft : getTabCount() -1;
                    setSelectTabView(nextLeftPosition);
                } else{
                    if(currentTabIndexLeft > 0){
                        int nextLeftPosition =  --currentTabIndexLeft;
                        setSelectTabView(nextLeftPosition);
                    } else{
                        Log.d(TAG, "index == 0, 不循環");
                    }
                }
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                int currentTabIndexRight = mSelectedIndex;
                if(isLoop){
                    int nextRightPosition = currentTabIndexRight < getTabCount() - 1 ? ++currentTabIndexRight : 0;
                    setSelectTabView(nextRightPosition);
                } else{
                    if(currentTabIndexRight < getTabCount() - 1){
                        int nextRightPosition = ++currentTabIndexRight;
                        setSelectTabView(nextRightPosition);
                    } else{
                        Log.d(TAG, "index == count, 不循環");
                    }
                }
                return true;
        }

        return super.dispatchKeyEvent(event);
    }

TvTabLayout與ViewPager的雙向綁定

添加TabLayoutOnPageChangeListenerViewPagerOnTabSelectedListener兩個回調監聽,實現TvTabLayout和ViewPager的雙向綁定。

    public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
        private final WeakReference<TVTabLayout> mTabLayoutRef;
        private int mPreviousScrollState;
        private int mScrollState;

        //省略部分代碼

        @Override
        public void onPageSelected(final int position) {
            //監聽ViewPager的Item選中事件,切換Tab
            final TVTabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
                    && position < tabLayout.getTabCount()) {
                // Select the tab, only updating the indicator if we're not being dragged/settled
                // (since onPageScrolled will handle that).
                tabLayout.setSelectTabView(position);
            }
        }
    }

    public interface OnTabSelectedListener{
        void onTabSelected(TabView view, int index);

        void onTabUnSelected(TabView view, int index);
    }

    public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener {
        private final ViewPager mViewPager;

        //省略部分代碼

        @Override
        public void onTabSelected(TabView view, int index) {
            //監聽Tab選中事件,切換ViewPager
            if(mViewPager != null && mViewPager.getCurrentItem() != index){
                try{
                    //部分型號上拋出異常:
                    //IllegalStateException: FragmentManager is already executing transactions
                    //簡單捕獲處理下,後續完善
                    mViewPager.setCurrentItem(index);
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

提供setupWithViewPager(ViewPager)方法用於綁定ViewPager。

    public void setupWithViewPager(ViewPager viewPager){
        if(this.viewPager != null){
            //移除所有監聽
            if(onPageChangeListener != null){
                this.viewPager.removeOnPageChangeListener(onPageChangeListener);
            }

            if(onTabSelectedListener != null){
                this.removeOnTabSelectedListener(onTabSelectedListener);
            }
        }

        if(viewPager != null){
            this.viewPager = viewPager;
            //綁定,設置監聽
            onPageChangeListener = new TabLayoutOnPageChangeListener(this);
            viewPager.addOnPageChangeListener(onPageChangeListener);

            onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
            this.addOnTabSelectedListener(onTabSelectedListener);
        } else{
            this.viewPager = null;
            this.onPageChangeListener = null;
            this.onTabSelectedListener = null;
        }
    }

OnTabSelectedListener事件使用了觀察者模式,除了綁定ViewPagerOnTabSelectedListener監聽外,還可以通過實現OnTabSelectedListener接口實現自定義事件監聽,爲TvTabLayout單獨使用(不搭配ViewPager)提供了條件。用戶可以通過監聽OnTabSelectedListener的onTabSelected(TabView,int)事件,來實現自定義的TabView的選中效果(UI效果、動畫等)或者TvTabLayout單獨使用的需求。

Tab滾動效果

定義了三種Tab滾動模式。

    /**
     * Tab填充滿顯示區域,不可滾動
     */
    public static final int MODE_FIXED_NON_SCROLLABLE = 1;
    /**
     * Tab可滾動且選中欄目一直居中
     */
    public static final int MODE_SCROLLABLE_INDICATOR_CENTER = 2;
    /**
     * Tab可滾動
     */
    public static final int MODE_SCROLLABLE = 3;

    @IntDef(value = {MODE_FIXED_NON_SCROLLABLE, MODE_SCROLLABLE, MODE_SCROLLABLE_INDICATOR_CENTER})
    public @interface Mode{}
    //TabView Mode
    private int mMode = MODE_FIXED_NON_SCROLLABLE;

重寫onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法根據Mode修改TabView尺寸。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(getChildCount() == 1){
            View view = getChildAt(0);
            int childViewWidth = view.getMeasuredWidth();

            switch (mMode){
                case MODE_FIXED_NON_SCROLLABLE:
                    if(childViewWidth >= getMeasuredWidth()){
                        //更新Tab的尺寸
                        int tabViewCount = tabViewLinearLayout.getChildCount();
                        for(int i = 0; i < tabViewCount; i++){
                            ViewGroup.LayoutParams lp = tabViewLinearLayout.getChildAt(i).getLayoutParams();
                            lp.width = getMeasuredWidth() / tabViewCount;
                        }
                    }
                    break;
                //其他模式的修改邏輯
            }
        }
    }

當TabView被選中時,根據Mode設置滾動效果。

   //TabView選中處理
   void setSelectTabView(TabView tabView, int position){
        final TabView currentTab = mSelectedTabView;
        final int currentTabIndex = mSelectedIndex;

        if (currentTab == tabView) {
            if (currentTab != null) {
                dispatchTabSelected(currentTab, currentTabIndex);

                if(hasFocus()){
                    setFocusedTabView(currentTabIndex);
                }
            }
        } else {
            if (currentTab != null) {
                dispatchTabUnselected(currentTab, currentTabIndex);
                setUnFocusedTabView(currentTabIndex);
            }
            mSelectedTabView = tabView;
            mSelectedIndex = position;
            if (tabView != null) {
                dispatchTabSelected(tabView, position);

                if(hasFocus()){
                    setFocusedTabView(position);
                }
            }

            setScrollPosition();
        }
    }


    //設置滾動效果
    private void setScrollPosition(){
        View tabView = mSelectedTabView;
        int index = mSelectedIndex;
        if(mMode == MODE_SCROLLABLE || mMode == MODE_SCROLLABLE_INDICATOR_CENTER){
            int scrollViewWidth = getMeasuredWidth();
            int scrolled = getScrollX();
            int tabViewX = (int) tabView.getX();
            int tabViewWidth = tabView.getMeasuredWidth();
            int tabViewLinearLayoutWidth = tabViewLinearLayout.getMeasuredWidth();
            Log.d(TAG, "index = " + index + " --- scrollViewWidth = " + scrollViewWidth + " --- scrolled = " + scrolled + " --- tabViewX = " + tabViewX + " --- tabViewWidth" + tabViewWidth);

            switch (mMode){
                case MODE_SCROLLABLE:
                    if(tabViewX + tabViewWidth > scrollViewWidth + scrolled){
                        int scrollTo = tabViewX + tabViewWidth - scrollViewWidth;
                        smoothScrollTo(scrollTo, 0);
                    } else if(scrolled > tabViewX){
                        int scrollTo = tabViewX;
                        smoothScrollTo(scrollTo, 0);
                    }
                    break;
                case MODE_SCROLLABLE_INDICATOR_CENTER:
                    if(tabViewX + tabViewWidth / 2 < scrollViewWidth / 2 ||
                            tabViewX + tabViewWidth / 2 > tabViewLinearLayoutWidth - scrollViewWidth / 2){
                        break;
                    }

                    if((tabViewX + tabViewWidth / 2) != scrolled + scrollViewWidth / 2){
                        int scrollTo = tabViewX + tabViewWidth / 2 - scrollViewWidth / 2;
                        smoothScrollTo(scrollTo, 0);
                    }
                    break;
            }
        }
    }

實現思路大量借鑑了系統的TabLayout,整體實現邏輯並不複雜,實現細節可以閱讀下面的源碼。

源碼

/**
 * <pre>
 *     author: Zhong
 *     time  : 2019/9/24
 *     desc  : 封裝了一個電視端易用的TabLayout
 * </pre>
 */
public class TVTabLayout extends HorizontalScrollView {
    private static final String TAG = TVTabLayout.class.getSimpleName();

    //所有的TabView
    private final ArrayList<TabView> mTabs = new ArrayList<>();
    //當前選中的TabView
    private TabView mSelectedTabView = null;
    //當前選中的Tab index
    private int mSelectedIndex = -1;

    //tab之間的間距
    private int tabsMargin = 0;
    //是否循環,焦點移動邊界時,是否循環
    private boolean isLoop = false;

    private LinearLayout tabViewLinearLayout;
    /**
     * Tab填充滿顯示區域,不可滾動
     */
    public static final int MODE_FIXED_NON_SCROLLABLE = 1;
    /**
     * Tab可滾動且選中欄目一直居中
     */
    public static final int MODE_SCROLLABLE_INDICATOR_CENTER = 2;
    /**
     * Tab可滾動
     */
    public static final int MODE_SCROLLABLE = 3;

    @IntDef(value = {MODE_FIXED_NON_SCROLLABLE, MODE_SCROLLABLE, MODE_SCROLLABLE_INDICATOR_CENTER})
    public @interface Mode{}
    //TabView Mode
    private int mMode = MODE_FIXED_NON_SCROLLABLE;

    //Tab焦點背景
    private Drawable mTabFocusedBackground = null;
    //TabView的選中監聽,觀察者模式
    private ArrayList<OnTabSelectedListener> mSelectedListeners = new ArrayList<>();
    //綁定的ViewPager
    private ViewPager viewPager;
    //ViewPager頁面切換監聽
    private ViewPager.OnPageChangeListener onPageChangeListener;
    //TabLayout切換監聽
    private OnTabSelectedListener onTabSelectedListener;

    public TVTabLayout(Context context) {
        this(context, null);
    }

    public TVTabLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TVTabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

//        setOrientation(HORIZONTAL);
        setFocusable(true);
        setFocusableInTouchMode(true);
        // Disable the Scroll Bar
        setHorizontalScrollBarEnabled(false);
        setFillViewport(true);

        // Add the TabStrip
        tabViewLinearLayout = new LinearLayout(context);
        tabViewLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
        tabViewLinearLayout.setFocusable(false);
        tabViewLinearLayout.setFocusableInTouchMode(false);

        applyMode();
        addView(tabViewLinearLayout, new HorizontalScrollView.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
    }


    private void applyMode(){
        switch (mMode) {
            case MODE_FIXED_NON_SCROLLABLE:
                tabViewLinearLayout.setGravity(Gravity.CENTER);
                break;
            case MODE_SCROLLABLE:
            case MODE_SCROLLABLE_INDICATOR_CENTER:
                tabViewLinearLayout.setGravity(Gravity.CENTER_VERTICAL| START);
                break;
        }
    }

    public void setMode(@Mode int mode){
        if(mode != mMode){
            this.mMode = mode;

            applyMode();
        }
    }

    /**
     * Return the current mode
     * @see #setMode(int)
     */
    @Mode
    public int getMode(){
        return mMode;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(getChildCount() == 1){
            View view = getChildAt(0);
            int childViewWidth = view.getMeasuredWidth();

            switch (mMode){
                case MODE_FIXED_NON_SCROLLABLE:
                    if(childViewWidth >= getMeasuredWidth()){
                        //更新Tab的尺寸
                        int tabViewCount = tabViewLinearLayout.getChildCount();
                        for(int i = 0; i < tabViewCount; i++){
                            ViewGroup.LayoutParams lp = tabViewLinearLayout.getChildAt(i).getLayoutParams();
                            lp.width = getMeasuredWidth() / tabViewCount;
                        }
                    }
                    break;
                //其他模式的修改邏輯
            }
        }
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        if(gainFocus){
            if(mSelectedIndex >= 0){
                setSelectTabView(mSelectedIndex);
            } else{
                setSelectTabView(0);
            }
        } else{
            setUnFocusedTabView(mSelectedIndex);
        }
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if(event.getAction() != KeyEvent.ACTION_DOWN){
            return super.dispatchKeyEvent(event);
        }

        switch (event.getKeyCode()){
            case KeyEvent.KEYCODE_DPAD_LEFT:
                int currentTabIndexLeft = mSelectedIndex;
                if(isLoop){
                    int nextLeftPosition = currentTabIndexLeft > 0 ? --currentTabIndexLeft : getTabCount() -1;
                    setSelectTabView(nextLeftPosition);
                } else{
                    if(currentTabIndexLeft > 0){
                        int nextLeftPosition =  --currentTabIndexLeft;
                        setSelectTabView(nextLeftPosition);
                    } else{
                        Log.d(TAG, "index == 0, 不循環");
                    }
                }
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                int currentTabIndexRight = mSelectedIndex;
                if(isLoop){
                    int nextRightPosition = currentTabIndexRight < getTabCount() - 1 ? ++currentTabIndexRight : 0;
                    setSelectTabView(nextRightPosition);
                } else{
                    if(currentTabIndexRight < getTabCount() - 1){
                        int nextRightPosition = ++currentTabIndexRight;
                        setSelectTabView(nextRightPosition);
                    } else{
                        Log.d(TAG, "index == count, 不循環");
                    }
                }
                return true;
        }

        return super.dispatchKeyEvent(event);
    }

    @Override
    public void setOnKeyListener(OnKeyListener l) {
        super.setOnKeyListener(l);
    }

    /**
     * 設置Tab間距
     * @param margin 間距
     */
    public void setTabsMargin(int margin){
        this.tabsMargin = margin;
    }

    /**
     * 添加tab
     * @param view tabView
     */
    public void addTab(@NonNull TabView view){
        addTab(view, tabsMargin);
    }

    /**
     * 添加tab
     * @param view tabView
     */
    public void addTab(@NonNull TabView view, int tabsMargin){
        view.setFocusable(false);
        view.setFocusableInTouchMode(false);
        mTabs.add(view);
        tabViewLinearLayout.addView(view, createLayoutParamsForTabs());

        if(mSelectedIndex == mTabs.indexOf(view)){
            setSelectTabView(mSelectedIndex);
        }
    }

    private LinearLayout.LayoutParams createLayoutParamsForTabs() {
        final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //設置Tab間距
        lp.leftMargin = tabsMargin;
        updateTabViewLayoutParams(lp);
        return lp;
    }

    private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
        //根據模式設置Tab尺寸
        if (mMode == MODE_FIXED_NON_SCROLLABLE) {
            lp.width = 0;
            lp.weight = 1;
        } else {
            lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
            lp.weight = 0;
        }
    }

    /**
     * 獲取當前選中的Tab index
     * @return select index
     */
    public int getSelectedTabPosition() {
        return mSelectedIndex;
    }

    /**
     * 根據Index 獲取TabView
     * @param index index
     * @return tabView
     */
    public TabView getTab(int index){
        return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
    }

    /**
     * 獲取TabView個數
     * @return TabView個數
     */
    public int getTabCount() {
        return mTabs.size();
    }

    /**
     * 設置獲取焦點的TavView
     * @param position
     */
    protected void setFocusedTabView(int position){
        final int tabCount = getTabCount();
        if (position < tabCount) {
            if(mTabFocusedBackground != null && getTab(position) != null){
                getTab(position).setBackground(mTabFocusedBackground);
            }
        }
    }

    /**
     * 設置失去焦點的TabView
     * @param position
     */
    protected void setUnFocusedTabView(int position){
        final int tabCount = getTabCount();
        if (position < tabCount) {
            if(mTabFocusedBackground != null && getTab(position) != null){
                getTab(position).setBackground(null);
            }
        }
    }

    /**
     * 設置Tab選中狀態
     * @param position
     */
    public void setSelectTabView(int position) throws IndexOutOfBoundsException{
        final int tabCount = getTabCount();
        if (position < tabCount) {
            setSelectTabView(getTab(position), position);
        } else{
            throw new IndexOutOfBoundsException("index out of TabView count");
        }
    }

    /**
     * 設置Tab選中狀態
     * @param tabView
     */
    void setSelectTabView(TabView tabView, int position){
        final TabView currentTab = mSelectedTabView;
        final int currentTabIndex = mSelectedIndex;

        if (currentTab == tabView) {
            if (currentTab != null) {
                dispatchTabSelected(currentTab, currentTabIndex);

                if(hasFocus()){
                    setFocusedTabView(currentTabIndex);
                }
            }
        } else {
            if (currentTab != null) {
                dispatchTabUnselected(currentTab, currentTabIndex);
                setUnFocusedTabView(currentTabIndex);
            }
            mSelectedTabView = tabView;
            mSelectedIndex = position;
            if (tabView != null) {
                dispatchTabSelected(tabView, position);

                if(hasFocus()){
                    setFocusedTabView(position);
                }
            }

            setScrollPosition();
        }
    }

    /**
     * 分發Tab選中狀態
     * @param tab selected TabView
     * @param position selected TabView`s index
     */
    private void dispatchTabSelected(@NonNull TabView tab, int position) {
        for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
            mSelectedListeners.get(i).onTabSelected(tab,position);
        }
    }

    private void dispatchTabUnselected(@NonNull TabView tab, int position) {
        for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
            mSelectedListeners.get(i).onTabUnSelected(tab, position);
        }
    }

    public void setTabSelectedListener(OnTabSelectedListener onTabSelectedListener){
        if (onTabSelectedListener != null) {
            addOnTabSelectedListener(onTabSelectedListener);
        }
    }

    public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
        if (!mSelectedListeners.contains(listener)) {
            mSelectedListeners.add(listener);
        }
    }

    public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener){
        if(mSelectedListeners != null){
            mSelectedListeners.remove(listener);
        }
    }

    public void setTabFocusedBackground(Drawable drawable){
        mTabFocusedBackground = drawable;
    }

    public void setTabFocusedBackground(int resource){
        Drawable drawable = getResources().getDrawable(resource);
        mTabFocusedBackground = drawable;
    }

    /**
     * 滾動處理
     */
    private void setScrollPosition(){
        View tabView = mSelectedTabView;
        int index = mSelectedIndex;
        if(mMode == MODE_SCROLLABLE || mMode == MODE_SCROLLABLE_INDICATOR_CENTER){
            int scrollViewWidth = getMeasuredWidth();
            int scrolled = getScrollX();
            int tabViewX = (int) tabView.getX();
            int tabViewWidth = tabView.getMeasuredWidth();
            int tabViewLinearLayoutWidth = tabViewLinearLayout.getMeasuredWidth();
            Log.d(TAG, "index = " + index + " --- scrollViewWidth = " + scrollViewWidth + " --- scrolled = " + scrolled + " --- tabViewX = " + tabViewX + " --- tabViewWidth" + tabViewWidth);

            switch (mMode){
                case MODE_SCROLLABLE:
                    if(tabViewX + tabViewWidth > scrollViewWidth + scrolled){
                        int scrollTo = tabViewX + tabViewWidth - scrollViewWidth;
                        smoothScrollTo(scrollTo, 0);
                    } else if(scrolled > tabViewX){
                        int scrollTo = tabViewX;
                        smoothScrollTo(scrollTo, 0);
                    }
                    break;
                case MODE_SCROLLABLE_INDICATOR_CENTER:
                    if(tabViewX + tabViewWidth / 2 < scrollViewWidth / 2 ||
                            tabViewX + tabViewWidth / 2 > tabViewLinearLayoutWidth - scrollViewWidth / 2){
                        break;
                    }

                    if((tabViewX + tabViewWidth / 2) != scrolled + scrollViewWidth / 2){
                        int scrollTo = tabViewX + tabViewWidth / 2 - scrollViewWidth / 2;
                        smoothScrollTo(scrollTo, 0);
                    }
                    break;
            }
        }
    }

    @NonNull
    public TabView newTabView() {
        TabView tabView = new TabView(getContext());
        tabView.setFocusable(false);
        return tabView;
    }

    /**
     * 綁定ViewPager
     * @param viewPager ViewPager
     */
    public void setupWithViewPager(ViewPager viewPager){
        if(this.viewPager != null){
            //移除所有監聽
            if(onPageChangeListener != null){
                this.viewPager.removeOnPageChangeListener(onPageChangeListener);
            }

            if(onTabSelectedListener != null){
                this.removeOnTabSelectedListener(onTabSelectedListener);
            }
        }

        if(viewPager != null){
            this.viewPager = viewPager;
            //綁定,設置監聽
            onPageChangeListener = new TabLayoutOnPageChangeListener(this);
            viewPager.addOnPageChangeListener(onPageChangeListener);

            onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
            this.addOnTabSelectedListener(onTabSelectedListener);
        } else{
            this.viewPager = null;
            this.onPageChangeListener = null;
            this.onTabSelectedListener = null;
        }
    }

    /**
     public interface OnTabFocusListener{
     public void onTabFocused(View view, int index);

     public void onTabUnFocus(View view, int index);
     }
     **/


    public static final class TabView extends FrameLayout {
        private View mCustomView;

        public TabView(@NonNull Context context) {
            super(context);
        }

        public TabView(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }

        public TabView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }

        @Nullable
        public View getCustomView() {
            return mCustomView;
        }

        @NonNull
        public TabView setCustomView(@Nullable View view) {
            mCustomView = view;
            updateView();
            return this;
        }

        void updateView() {
            if (mCustomView != null) {
                LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
                setPadding(4,4,4,4);
                addView(mCustomView,layoutParams);
            }
        }
    }

    public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
        private final WeakReference<TVTabLayout> mTabLayoutRef;
        private int mPreviousScrollState;
        private int mScrollState;

        public TabLayoutOnPageChangeListener(TVTabLayout tabLayout) {
            mTabLayoutRef = new WeakReference<>(tabLayout);
        }

        @Override
        public void onPageScrollStateChanged(final int state) {
            mPreviousScrollState = mScrollState;
            mScrollState = state;
        }

        @Override
        public void onPageScrolled(final int position, final float positionOffset,
                                   final int positionOffsetPixels) {
        }

        @Override
        public void onPageSelected(final int position) {
            //監聽ViewPager的Item選中事件,切換Tab
            final TVTabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
                    && position < tabLayout.getTabCount()) {
                // Select the tab, only updating the indicator if we're not being dragged/settled
                // (since onPageScrolled will handle that).
                tabLayout.setSelectTabView(position);
            }
        }
    }

    public interface OnTabSelectedListener{
        void onTabSelected(TabView view, int index);

        void onTabUnSelected(TabView view, int index);
    }

    public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener {
        private final ViewPager mViewPager;

        public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
            mViewPager = viewPager;
        }

        @Override
        public void onTabSelected(TabView view, int index) {
            //監聽Tab選中事件,切換ViewPager
            if(mViewPager != null && mViewPager.getCurrentItem() != index){
                try{
                    //部分型號上拋出異常:
                    //IllegalStateException: FragmentManager is already executing transactions
                    //簡單捕獲處理下,後續完善
                    mViewPager.setCurrentItem(index);
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onTabUnSelected(TabView view, int index) {

        }
    }
}

總結

TvTabLayout控件是從現有項目中剝離出來的,未做過多的功能性封裝。相較於其他類似開源項目略顯簡陋,但是也希望能爲你解決類似問題提供思路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章