來源 http://blog.csdn.net/analyzesystem/article/details/51426473css
Android studio 項目導入依賴compile路徑java
dependencies{ compile 'com.android.support:support-v4:23.1.1' compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.2@aar' }
FlycoTabLayout是一個Android TabLayout庫,目前有3個TabLayoutandroid
/** 關聯ViewPager,用於不想在ViewPager適配器中設置titles數據的狀況 */ public void setViewPager(ViewPager vp, String[] titles) /** 關聯ViewPager,用於連適配器都不想本身實例化的狀況 */ public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList<Fragment> fragments)
CommonTabLayout:不一樣於SlidingTabLayout對ViewPager依賴,它是一個不依賴ViewPager能夠與其餘控件自由搭配使用的TabLayout.git
/** 關聯數據支持同時切換fragments */ public void setTabData(ArrayList<CustomTabEntity> tabEntitys, FragmentManager fm, int containerViewId, ArrayList<Fragment> fragments)
自定義屬性表github
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。api
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提供幾個對咱們比較有用的方法bash
/**顯示指定位置未讀紅點*/ 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引用就好,通常狀況下哪些方法都用不到了。編輯器
SlidingTabLayout對應的方法在這裏都適用再也不重複,CommonTabLayout最重要的就是setTabData(ArrayList tabEntitys)方法,使得CommonTabLayout再也不依賴於ViewPager完成初始化,實現底部導航或者頭部導航效果,讓咱們告別RadioButton+ViewPager的時代,CustomTabEntity的命名有點問題哈,命名是一個接口非要定義Entity結尾,TabEntity實現該接口,修改構造方法,初始化內部參數,下面是一個配合CommonTabLayout+ViewPager的導航實例ide
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實現效果就像qq消息列表頂部的切換效果,統一支持setTabData(mTitles)不過這裏傳入標題數組,tabLayout配合ViewPager切換調用tabLayout提供的方法setCurrentTab,SegmentTabLayout提供的 setTabData(String[] titles, FragmentActivity fa, int containerViewId, ArrayList fragments)方法在咱們frameLayout+fragment佈局切換Fragment比較實用的
粗略看過這個庫的自定義屬性文件,明悟了件事:自定義屬性的自動提示以下(之前我自定義屬性都放在declare-styleable直接定義的)
下面是這些自定義屬性的具體含義(attrs裏面有具體說明)
<!-- 如下是重用attr的正確姿式,一切爲了在佈局中能夠自動提示--> <!-- 圓角矩形背景色 --> <attr name="rv_backgroundColor" format="color"/> <!-- 圓角矩形背景色press --> <attr name="rv_backgroundPressColor" format="color"/> <!-- 圓角弧度,單位dp--> <attr name="rv_cornerRadius" format="dimension"/> <!-- 圓角弧度,單位dp--> <attr name="rv_strokeWidth" format="dimension"/> <!-- 圓角邊框顏色--> <attr name="rv_strokeColor" format="color"/> <!-- 圓角邊框顏色press --> <attr name="rv_strokePressColor" format="color"/> <!-- 文字顏色press--> <attr name="rv_textPressColor" format="color"/> <!-- 圓角弧度是高度一半--> <attr name="rv_isRadiusHalfHeight" format="boolean"/> <!-- 圓角矩形寬高相等,取較寬高中大值--> <attr name="rv_isWidthHeightEqual" format="boolean"/> <!-- 圓角弧度,單位dp,TopLeft--> <attr name="rv_cornerRadius_TL" format="dimension"/> <!-- 圓角弧度,單位dp,TopRight--> <attr name="rv_cornerRadius_TR" format="dimension"/> <!-- 圓角弧度,單位dp,BottomLeft--> <attr name="rv_cornerRadius_BL" format="dimension"/> <!-- 圓角弧度,單位dp,BottomRight--> <attr name="rv_cornerRadius_BR" format="dimension"/> <!-- 是否有Ripple效果,api21+有效--> <attr name="rv_isRippleEnable" format="boolean"/> <declare-styleable name="RoundTextView"> <attr name="rv_backgroundColor"/> <attr name="rv_backgroundPressColor"/> <attr name="rv_cornerRadius"/> <attr name="rv_strokeWidth"/> <attr name="rv_strokeColor"/> <attr name="rv_strokePressColor"/> <attr name="rv_textPressColor"/> <attr name="rv_isRadiusHalfHeight"/> <attr name="rv_isWidthHeightEqual"/> <attr name="rv_cornerRadius_TL"/> <attr name="rv_cornerRadius_TR"/> <attr name="rv_cornerRadius_BL"/> <attr name="rv_cornerRadius_BR"/> <attr name="rv_isRippleEnable"/> </declare-styleable>
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獲取構造函數初始化的實例進行設置和獲取對應屬性。
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<Fragment> fragments = new ArrayList<>(); private String[] titles; public InnerPagerAdapter(FragmentManager fm, ArrayList<Fragment> 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適配器,最重要的是自定義屬性解析和屬性值代碼設置經過一個類來代理完成。