Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 開源項目: FlycoTabLayout

開源項目: FlycoTabLayout

編輯:關於Android編程

開源項目效果

\

調用實例

必練基本功

Android studio 項目導入依賴compile路徑

dependencies{
    compile 'com.android.support:support-v4:23.1.1'
    compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.2@aar'
}

FlycoTabLayout是一個Android TabLayout庫,目前有3個TabLayout

新增部分屬性 新增支持多種Indicator顯示器 新增支持未讀消息顯示 新增方法setViewPager
 /** 關聯ViewPager,用於不想在ViewPager適配器中設置titles數據的情況 */
    public void setViewPager(ViewPager vp, String[] titles)

    /** 關聯ViewPager,用於連適配器都不想自己實例化的情況 */
    public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments) 

CommonTabLayout:不同於SlidingTabLayout對ViewPager依賴,它是一個不依賴ViewPager可以與其他控件自由搭配使用的TabLayout.

支持多種Indicator顯示器,以及Indicator動畫 支持未讀消息顯示 支持Icon以及Icon位置 新增方法
/** 關聯數據支持同時切換fragments */
    public void setTabData(ArrayList tabEntitys, FragmentManager fm, int containerViewId, ArrayList fragments)
SegmentTabLayout:仿照QQ消息列表頭部tab切換的控件

自定義屬性表

tl_indicator_color          color       設置顯示器顏色
tl_indicator_height         dimension   設置顯示器高度
tl_indicator_width          dimension   設置顯示器固定寬度
tl_indicator_margin_left    dimension   設置顯示器margin,當indicator_width大於0,無效
tl_indicator_margin_top     dimension   設置顯示器margin,當indicator_width大於0,無效
tl_indicator_margin_right   dimension   設置顯示器margin,當indicator_width大於0,無效
tl_indicator_margin_bottom  dimension   設置顯示器margin,當indicator_width大於0,無效
tl_indicator_corner_radius  dimension   設置顯示器圓角弧度
tl_indicator_gravity        enum        設置顯示器上方(TOP)還是下方(BOTTOM),只對常規顯示器有用
tl_indicator_style          enum        設置顯示器為常規(NORMAL)或三角形(TRIANGLE)或背景色塊(BLOCK)
tl_underline_color          color       設置下劃線顏色
tl_underline_height         dimension   設置下劃線高度
tl_underline_gravity        enum        設置下劃線上方(TOP)還是下方(BOTTOM)
tl_divider_color            color       設置分割線顏色
tl_divider_width            dimension   設置分割線寬度
tl_divider_padding          dimension   設置分割線的paddingTop和paddingBottom
tl_tab_padding              dimension   設置tab的paddingLeft和paddingRight
tl_tab_space_equal          boolean     設置tab大小等分
tl_tab_width                dimension   設置tab固定大小
tl_textsize                 dimension   設置字體大小
tl_textSelectColor          color       設置字體選中顏色
tl_textUnselectColor        color       設置字體未選中顏色
tl_textBold                 boolean     設置字體加粗
tl_iconWidth                dimension   設置icon寬度(僅支持CommonTabLayout)
tl_iconHeight               dimension   設置icon高度(僅支持CommonTabLayout)
tl_iconVisible              boolean     設置icon是否可見(僅支持CommonTabLayout)
tl_iconGravity              enum        設置icon顯示位置,對應Gravity中常量值,左上右下(僅支持CommonTabLayout)
tl_iconMargin               dimension   設置icon與文字間距(僅支持CommonTabLayout)
tl_indicator_anim_enable    boolean     設置顯示器支持動畫(only for CommonTabLayout)
tl_indicator_anim_duration  integer     設置顯示器動畫時間(only for CommonTabLayout)
tl_indicator_bounce_enable  boolean     設置顯示器支持動畫回彈效果(only for CommonTabLayout)
tl_indicator_width_equal_title  boolean 設置顯示器與標題一樣長(only for SlidingTabLayout)

該庫依賴於動畫兼容庫NineOldAndroids和FlycoRoundView,稍後在源碼分析裡簡單了解一下FlycoRoundView。

SlidingTabLayout調用

SlidingTabLayout自定義屬性支持下劃線設置,控制下劃線顯示方向寬高,可以讓線寬=文字寬度,也可以固定比例寬度,可以設置未讀消息的小紅點,也可以設置未讀消息數量,當前這一切的前提都是基於ViewPager來實現,都需要綁定ViewPager,通過多種綁定方法

 /**關聯ViewPager,Adapter重寫了getPageTitle方法*/
 tabLayout.setViewPager(vp);

 /**關聯ViewPager,用於不想在ViewPager適配器中設置titles數據的情況*/
 tabLayout.setViewPager(vp, mTitles);

 /**關聯ViewPager,用於連適配器都不想自己實例化的情況,內部幫助實例化了一個InnerPagerAdapter*/
 tabLayout.setViewPager(vp, mTitles, this, mFragments);

下面我們來看看tabLayout提供幾個對我們比較有用的方法

/**顯示指定位置未讀紅點*/
tabLayout.showDot(4);
/**隱藏指定位置未讀紅點或消息*/
tabLayout.hideMsg(5);
/**showMsg(int position, int num):position位置,num小於等於0顯示紅點,num大於0顯示數字,作用:顯示未讀消息,如果消息數量>99,顯示效果99+*/
tabLayout.showMsg(3, 5);
/**  setMsgMargin(int position, float leftPadding, float bottomPadding)設置未讀消息偏移,原點為文字的右上角.當控件高度固定,消息提示位置易控制,顯示效果佳 */
tabLayout.setMsgMargin(3, 0, 10);

/**設置未讀消息消息的背景*/
 MsgView msgView = tabLayout.getMsgView(3);
 if (msgView != null) {
     msgView.setBackgroundColor(Color.parseColor("#6D8FB0"));
  }
//...................略...........

自定義的屬性那麼多,對應的set方法自然也不少,不過對照上面自定義屬性xml引用就好,一般情況下哪些方法都用不到了。

CommonTabLayout調用

SlidingTabLayout對應的方法在這裡都適用不再重復,CommonTabLayout最重要的就是setTabData(ArrayList tabEntitys)方法,使得CommonTabLayout不再依賴於ViewPager完成初始化,實現底部導航或者頭部導航效果,讓我們告別RadioButton+ViewPager的時代,CustomTabEntity的命名有點問題哈,命名是一個接口非要定義Entity結尾,TabEntity實現該接口,修改構造方法,初始化內部參數,下面是一個配合CommonTabLayout+ViewPager的導航實例


        mFragmentList = addFragmentList(R.id.home_frameLayout, fragmentClasses);

        for (int i = 0; i < titles.length; i++) {
            mTabEntities.add(new TabEntity(titles[i], checkeds[i], normals[i]));
        }

        commonTabLayout.setTabData(mTabEntities);
        commonTabLayout.setOnTabSelectListener(new OnTabSelectListener() {
            @Override
            public void onTabSelect(int position) {

                if (position == 1) {
                    topBarBuilder.configSearchStyle(titles[position], R.drawable.ic_action_search);
                } else {
                    topBarBuilder.configTitle(titles[position]);
                }

                showFragment(R.id.home_frameLayout, position, mFragmentList);
                onLoggerD("initial callback ,show fragment with position " + position + ";FragmentName:" + mFragmentList.get(position).toString());
            }

            @Override
            public void onTabReselect(int position) {
                //TODO 重選
            }
        });
SegmentTabLayout調用

SegmentTabLayout實現效果就像qq消息列表頂部的切換效果,統一支持setTabData(mTitles)不過這裡傳入標題數組,tabLayout配合ViewPager切換調用tabLayout提供的方法setCurrentTab,SegmentTabLayout提供的 setTabData(String[] titles, FragmentActivity fa, int containerViewId, ArrayList fragments)方法在我們frameLayout+fragment布局切換Fragment比較實用的

源碼分析

FlycoRoundView源碼分析

粗略看過這個庫的自定義屬性文件,明悟了件事:自定義屬性的自動提示如下(以前我自定義屬性都放在declare-styleable直接定義的)

\

下面是這些自定義屬性的具體含義(attrs裡面有具體說明)<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">

Round系列的View控件自定義 RoundTextView 、RoundFrameLayout 、RoundLinearLayout RoundRelativeLayout,這幾個控件內部源碼實現並不復雜,可以說簡單之極,構造函數通過RoundViewDelegate代理解析自定義屬性,其次就是onMeasure和onLayout的測量相關的,ToggleButton源碼分析一篇有提到這裡使用EXACTLY

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (delegate.isWidthHeightEqual() && getWidth() > 0 && getHeight() > 0) {
            int max = Math.max(getWidth(), getHeight());
            int measureSpec = MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY);
            super.onMeasure(measureSpec, measureSpec);
            return;
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (delegate.isRadiusHalfHeight()) { //如果弧度是高度的一半,直接設置radio為高度一半,否則調用
            delegate.setCornerRadius(getHeight() / 2);
        }else {
            delegate.setBgSelector();// Ripple效果兼容21+,Ripple效果的實現有多重,這裡使用的RippleDrawable,具體使用方法請參考api,這裡不是本篇重點
        }
    }

setBgSelector 方法使用到了GradientDrawable、StateListDrawable,沒了解過的可以參考Drawable系列這篇博客有相應的簡單介紹,RoundViewDelegate提供了這些自定義屬性的set get方法,我們代碼調用通過自定義控件getDelegate獲取構造函數初始化的實例進行設置和獲取對應屬性。

FlycoTabLayout Lib庫源碼分析

MsgView仿照FlycoRoundView庫編寫的一個自定義控件,主要用於未讀消息的展示,大同小異的代碼就此略過。

SlidingTabLayout自定義控件千篇一律的自定義屬性飄過,來到必經之路setViewPager,發現新大陸notifyDataSetChanged()調用,通過dapter的getPageTitle方法獲取標題,並inflate添加一個布局到HorizontalScrollView的子View LinearLayout,並綁定Tab想的監聽回調。

 /** 更新數據 */
 public void notifyDataSetChanged() {
        mTabsContainer.removeAllViews();
        this.mTabCount = mTitles == null ? mViewPager.getAdapter().getCount() : mTitles.length;
        View tabView;
        for (int i = 0; i < mTabCount; i++) {
            if (mViewPager.getAdapter() instanceof CustomTabProvider) {
                tabView = ((CustomTabProvider) mViewPager.getAdapter()).getCustomTabView(this, i);
            } else {
                tabView = View.inflate(mContext, R.layout.layout_tab, null);
            }

            CharSequence pageTitle = mTitles == null ? mViewPager.getAdapter().getPageTitle(i) : mTitles[i];
            addTab(i, pageTitle.toString(), tabView);
        }

        updateTabStyles();
    }

/** 創建並添加tab */
private void addTab(final int position, String title, View tabView) {
        TextView tv_tab_title = (TextView) tabView.findViewById(R.id.tv_tab_title);
        if (tv_tab_title != null) {
            if (title != null) tv_tab_title.setText(title);
        }

        tabView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mViewPager.getCurrentItem() != position) {
                    mViewPager.setCurrentItem(position);
                    if (mListener != null) {
                        mListener.onTabSelect(position);
                    }
                } else {
                    if (mListener != null) {
                        mListener.onTabReselect(position);
                    }
                }
            }
        });

        /** 每一個Tab的布局參數 */
        LinearLayout.LayoutParams lp_tab = mTabSpaceEqual ?
                new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f) :
                new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        if (mTabWidth > 0) {
            lp_tab = new LinearLayout.LayoutParams((int) mTabWidth, LayoutParams.MATCH_PARENT);
        }

        mTabsContainer.addView(tabView, position, lp_tab);
    }

calcIndicatorRect方法根據不同的屬性計算出Rect范圍以便以繪制,具體繪制方法參考Canvas API,這裡提一點下面這個方法非常有必要,如果在自定義控件的構造函數或者其他繪制相關地方使用系統依賴的代碼,會導致可視化編輯器無法報錯並提示:Use View.isInEditMode() in your custom views to skip code when shown in Eclipseis,加上了isInEditMode的判斷就不會再報錯了。

 if (isInEditMode() || mTabCount <= 0) {
            return;
        }

對於代碼設置自定義的屬性值,會調用下面這兩個方法 invalidate()和 updateTabStyles();涉及到了繪制的則調用invalidate,可以直接修改的則調用updateTabStyles(個人感覺不太友好,比如代碼設置一個屬性值,就需要遍歷所有view,同時重新調用屬性賦值,關鍵只修改了其中一個屬性!!)

  private void updateTabStyles() {
        for (int i = 0; i < mTabCount; i++) {
            View v = mTabsContainer.getChildAt(i);
//            v.setPadding((int) mTabPadding, v.getPaddingTop(), (int) mTabPadding, v.getPaddingBottom());
            TextView tv_tab_title = (TextView) v.findViewById(R.id.tv_tab_title);
            if (tv_tab_title != null) {
                tv_tab_title.setTextColor(i == mCurrentTab ? mTextSelectColor : mTextUnselectColor);
                tv_tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextsize);
                tv_tab_title.setPadding((int) mTabPadding, 0, (int) mTabPadding, 0);
                if (mTextAllCaps) {
                    tv_tab_title.setText(tv_tab_title.getText().toString().toUpperCase());
                }

                if (mTextBold) {
                    tv_tab_title.getPaint().setFakeBoldText(mTextBold);
                }
            }
        }
    }

setMsg 、setDot方法在調用示例有提到用法,這裡的show hide狀態如何保存的呢?這裡有用到和AbsListView內部狀態保持一樣的方法:SparseArray來保存對應位置的狀態

    /**
     * 顯示未讀消息
     *
     * @param position 顯示tab位置
     * @param num      num小於等於0顯示紅點,num大於0顯示數字
     */
    public void showMsg(int position, int num) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }

        View tabView = mTabsContainer.getChildAt(position);
        MsgView tipView = (MsgView) tabView.findViewById(R.id.rtv_msg_tip);
        if (tipView != null) {
            UnreadMsgUtils.show(tipView, num);

            if (mInitSetMap.get(position) != null && mInitSetMap.get(position)) {
                return;
            }

            setMsgMargin(position, 4, 2);
            mInitSetMap.put(position, true);
        }
    }

    /**
     * 顯示未讀紅點
     *
     * @param position 顯示tab位置
     */
    public void showDot(int position) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }
        showMsg(position, 0);
    }

    /** 隱藏未讀消息 */
    public void hideMsg(int position) {
        if (position >= mTabCount) {
            position = mTabCount - 1;
        }

        View tabView = mTabsContainer.getChildAt(position);
        MsgView tipView = (MsgView) tabView.findViewById(R.id.rtv_msg_tip);
        if (tipView != null) {
            tipView.setVisibility(View.GONE);
        }
    }

細心的你一定會發現show並沒有直接控制View的Visibility顯示隱藏,而是用了UnreadMsgUtils,這個類提供了兩個方法setSize和show方法,show方法對show的countSize進行了一次判斷轉換0為點,1-99圓+數字,>99則顯示99+,背景的弧度為寬度的一半

再回到setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)方法,在我們調用該方法時內部幫我們創建了內部定義的InnerPagerAdapter適配器,如果你想偷懶不想寫適配器就可以調用這個方法。InnerPagerAdapter重寫了getPageTitle,以便於notifyDataSetChanged方法調用動態添加tab項。

 class InnerPagerAdapter extends FragmentPagerAdapter {
        private ArrayList fragments = new ArrayList<>();
        private String[] titles;

        public InnerPagerAdapter(FragmentManager fm, ArrayList fragments, String[] titles) {
            super(fm);
            this.fragments = fragments;
            this.titles = titles;
        }

        @Override
        public int getCount() {
            return fragments.size();
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return titles[position];
        }

        @Override
        public Fragment getItem(int position) {
            return fragments.get(position);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            // 覆寫destroyItem並且空實現,這樣每個Fragment中的視圖就不會被銷毀
            // super.destroyItem(container, position, object);
        }

        @Override
        public int getItemPosition(Object object) {
            return PagerAdapter.POSITION_NONE;
        }
    }

CommonTabLayout與上面SlidingTabLayout百分之99的相似度,重復的不在敘述,區別點在於setTabData和notifyDataSetChanged方法,notifyDataSetChanged根據Icon的Gravity屬性進入不同布局的View做Tab,雖然TextView有drawableLeftRightTopBottom的相關屬性,但是並不能讓我們那麼自由的控制Ui。

    /** 更新數據 */
    public void notifyDataSetChanged() {
        mTabsContainer.removeAllViews();
        this.mTabCount = mTabEntitys.size();
        View tabView;
        for (int i = 0; i < mTabCount; i++) {
            if (mIconGravity == Gravity.LEFT) {
                tabView = View.inflate(mContext, R.layout.layout_tab_left, null);
            } else if (mIconGravity == Gravity.RIGHT) {
                tabView = View.inflate(mContext, R.layout.layout_tab_right, null);
            } else if (mIconGravity == Gravity.BOTTOM) {
                tabView = View.inflate(mContext, R.layout.layout_tab_bottom, null);
            } else {
                tabView = View.inflate(mContext, R.layout.layout_tab_top, null);
            }

            tabView.setTag(i);
            addTab(i, tabView);
        }

        updateTabStyles();
    }

setTabData(ArrayList tabEntitys, FragmentActivity fa, int containerViewId, ArrayList fragments)方法內部初始化了一個Fragment的管理輔助類FragmentChangeManager,該類在構造函數動態添加隱藏了fragment,對外提供setFragments(int index)顯示指定位置的Fragment,這個在frameLayout+Fragment+commonTabLayout布局裡面免去了我們管理fagment的煩惱

SegmentTabLayout相比較於CommonTabLayout多了動畫這塊的處理,點擊了某一項Tab,調用setCurrentTab,間接調用calcOffset開啟了動畫,動畫的執行過程中onAnimationUpdate重新重繪,調整位置。

  tabView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = (Integer) v.getTag();
                if (mCurrentTab != position) {
                    setCurrentTab(position);
                    if (mListener != null) {
                        mListener.onTabSelect(position);
                    }
                } else {
                    if (mListener != null) {
                        mListener.onTabReselect(position);
                    }
                }
            }
        });

    //setter and getter
    public void setCurrentTab(int currentTab) {
        mLastTab = this.mCurrentTab;
        this.mCurrentTab = currentTab;
        updateTabSelection(currentTab);
        if (mFragmentChangeManager != null) {
            mFragmentChangeManager.setFragments(currentTab);
        }
        if (mIndicatorAnimEnable) {
            calcOffset();
        } else {
            invalidate();
        }
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        IndicatorPoint p = (IndicatorPoint) animation.getAnimatedValue();
        mIndicatorRect.left = (int) p.left;
        mIndicatorRect.right = (int) p.right;
        invalidate();
    }

小結

首先說一點這裡不提供demo,需要去官方https://github.com/H07000223/FlycoTabLayout down ,其次呢這個庫看了之後還是有很大收獲的,比如自定義屬性的運用, setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)內部實例一個adapter適配器,最重要的是自定義屬性解析和屬性值代碼設置通過一個類來代理完成。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved