開源項目效果
Android studio 項目導入依賴compile路徑
dependencies{? ? compile'com.android.support:support-v4:23.1.1'compile'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.2@aar'}
1
2
3
4
FlycoTabLayout是一個Android TabLayout庫,目前有3個TabLayout
SlidingTabLayout:參照PagerSlidingTabStrip進行大量修改.關于PagerSlidingTabStrip源碼分析可以參照我以前的一篇博文http://blog.csdn.net/analyzesystem/article/details/50316745?
新增部分屬性
新增支持多種Indicator顯示器
新增支持未讀消息顯示
新增方法setViewPager
/** 關聯ViewPager,用于不想在ViewPager適配器中設置titles數據的情況 */public void setViewPager(ViewPager vp, String[] titles)/** 關聯ViewPager,用于連適配器都不想自己實例化的情況 */public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)
1
2
3
4
5
CommonTabLayout:不同于SlidingTabLayout對ViewPager依賴,它是一個不依賴ViewPager可以與其他控件自由搭配使用的TabLayout.
支持多種Indicator顯示器,以及Indicator動畫
支持未讀消息顯示
支持Icon以及Icon位置
新增方法
/** 關聯數據支持同時切換fragments */publicvoidsetTabData(ArrayList tabEntitys, FragmentManager fm,intcontainerViewId, ArrayList fragments)
1
2
SegmentTabLayout:仿照QQ消息列表頭部tab切換的控件
自定義屬性表
tl_indicator_colorcolor設置顯示器顏色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_colorcolor設置下劃線顏色tl_underline_height? ? ? ? dimension? 設置下劃線高度tl_underline_gravity? ? ? ? enum? ? ? ? 設置下劃線上方(TOP)還是下方(BOTTOM)tl_divider_colorcolor設置分割線顏色tl_divider_width? ? ? ? ? ? dimension? 設置分割線寬度tl_divider_padding? ? ? ? ? dimension? 設置分割線的paddingTop和paddingBottomtl_tab_padding? ? ? ? ? ? ? dimension? 設置tab的paddingLeft和paddingRighttl_tab_space_equal? ? ? ? ? boolean? ? 設置tab大小等分tl_tab_width? ? ? ? ? ? ? ? dimension? 設置tab固定大小tl_textsize? ? ? ? ? ? ? ? dimension? 設置字體大小tl_textSelectColorcolor設置字體選中顏色tl_textUnselectColorcolor設置字體未選中顏色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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
該庫依賴于動畫兼容庫NineOldAndroids和FlycoRoundView,稍后在源碼分析里簡單了解一下FlycoRoundView。
SlidingTabLayout自定義屬性支持下劃線設置,控制下劃線顯示方向寬高,可以讓線寬=文字寬度,也可以固定比例寬度,可以設置未讀消息的小紅點,也可以設置未讀消息數量,當前這一切的前提都是基于ViewPager來實現,都需要綁定ViewPager,通過多種綁定方法
/**關聯ViewPager,Adapter重寫了getPageTitle方法*/tabLayout.setViewPager(vp);/**關聯ViewPager,用于不想在ViewPager適配器中設置titles數據的情況*/tabLayout.setViewPager(vp, mTitles);/**關聯ViewPager,用于連適配器都不想自己實例化的情況,內部幫助實例化了一個InnerPagerAdapter*/tabLayout.setViewPager(vp, mTitles,this, mFragments);
1
2
3
4
5
6
7
8
下面我們來看看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"));? }//...................略...........
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
自定義的屬性那么多,對應的set方法自然也不少,不過對照上面自定義屬性xml引用就好,一般情況下哪些方法都用不到了。
SlidingTabLayout對應的方法在這里都適用不再重復,CommonTabLayout最重要的就是setTabData(ArrayList tabEntitys)方法,使得CommonTabLayout不再依賴于ViewPager完成初始化,實現底部導航或者頭部導航效果,讓我們告別RadioButton+ViewPager的時代,CustomTabEntity的命名有點問題哈,命名是一個接口非要定義Entity結尾,TabEntity實現該接口,修改構造方法,初始化內部參數,下面是一個配合CommonTabLayout+ViewPager的導航實例
mFragmentList = addFragmentList(R.id.home_frameLayout, fragmentClasses);for(inti =0; i < titles.length; i++) {? ? ? ? ? ? mTabEntities.add(newTabEntity(titles[i], checkeds[i], normals[i]));? ? ? ? }? ? ? ? commonTabLayout.setTabData(mTabEntities);? ? ? ? commonTabLayout.setOnTabSelectListener(newOnTabSelectListener() {@OverridepublicvoidonTabSelect(intposition) {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());? ? ? ? ? ? }@OverridepublicvoidonTabReselect(intposition) {//TODO 重選}? ? ? ? });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SegmentTabLayout實現效果就像qq消息列表頂部的切換效果,統一支持setTabData(mTitles)不過這里傳入標題數組,tabLayout配合ViewPager切換調用tabLayout提供的方法setCurrentTab,SegmentTabLayout提供的 setTabData(String[] titles, FragmentActivity fa, int containerViewId, ArrayList fragments)方法在我們frameLayout+fragment布局切換Fragment比較實用的
粗略看過這個庫的自定義屬性文件,明悟了件事:自定義屬性的自動提示如下(以前我自定義屬性都放在declare-styleable直接定義的)
下面是這些自定義屬性的具體含義(attrs里面有具體說明)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Round系列的View控件自定義 RoundTextView 、RoundFrameLayout 、RoundLinearLayout RoundRelativeLayout,這幾個控件內部源碼實現并不復雜,可以說簡單之極,構造函數通過RoundViewDelegate代理解析自定義屬性,其次就是onMeasure和onLayout的測量相關的,ToggleButton源碼分析一篇有提到這里使用EXACTLY
@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);if(delegate.isWidthHeightEqual() && getWidth() >0&& getHeight() >0) {intmax = Math.max(getWidth(), getHeight());intmeasureSpec = MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY);super.onMeasure(measureSpec, measureSpec);return;? ? ? ? }super.onMeasure(widthMeasureSpec, heightMeasureSpec);? ? }@OverrideprotectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom) {super.onLayout(changed, left, top, right, bottom);if(delegate.isRadiusHalfHeight()) {//如果弧度是高度的一半,直接設置radio為高度一半,否則調用delegate.setCornerRadius(getHeight() /2);? ? ? ? }else{? ? ? ? ? ? delegate.setBgSelector();// Ripple效果兼容21+,Ripple效果的實現有多重,這里使用的RippleDrawable,具體使用方法請參考api,這里不是本篇重點}? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setBgSelector 方法使用到了GradientDrawable、StateListDrawable,沒了解過的可以參考Drawable系列這篇博客有相應的簡單介紹,RoundViewDelegate提供了這些自定義屬性的set get方法,我們代碼調用通過自定義控件getDelegate獲取構造函數初始化的實例進行設置和獲取對應屬性。
MsgView仿照FlycoRoundView庫編寫的一個自定義控件,主要用于未讀消息的展示,大同小異的代碼就此略過。
SlidingTabLayout自定義控件千篇一律的自定義屬性飄過,來到必經之路setViewPager,發現新大陸notifyDataSetChanged()調用,通過dapter的getPageTitle方法獲取標題,并inflate添加一個布局到HorizontalScrollView的子View LinearLayout,并綁定Tab想的監聽回調。
/** 更新數據 */publicvoidnotifyDataSetChanged() {? ? ? ? mTabsContainer.removeAllViews();this.mTabCount = mTitles ==null? mViewPager.getAdapter().getCount() : mTitles.length;? ? ? ? View tabView;for(inti =0; i < mTabCount; i++) {if(mViewPager.getAdapter()instanceofCustomTabProvider) {? ? ? ? ? ? ? ? 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 */privatevoidaddTab(finalintposition, 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(newOnClickListener() {@OverridepublicvoidonClick(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 ?newLinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT,1.0f) :newLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);if(mTabWidth >0) {? ? ? ? ? ? lp_tab =newLinearLayout.LayoutParams((int) mTabWidth, LayoutParams.MATCH_PARENT);? ? ? ? }? ? ? ? mTabsContainer.addView(tabView, position, lp_tab);? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
calcIndicatorRect方法根據不同的屬性計算出Rect范圍以便以繪制,具體繪制方法參考Canvas API,這里提一點下面這個方法非常有必要,如果在自定義控件的構造函數或者其他繪制相關地方使用系統依賴的代碼,會導致可視化編輯器無法報錯并提示:Use View.isInEditMode() in your custom views to skip code when shown in Eclipseis,加上了isInEditMode的判斷就不會再報錯了。
if(isInEditMode() || mTabCount <=0) {return;? ? ? ? }
1
2
3
對于代碼設置自定義的屬性值,會調用下面這兩個方法 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);}? ? ? ? ? ? }? ? ? ? }? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setMsg 、setDot方法在調用示例有提到用法,這里的show hide狀態如何保存的呢?這里有用到和AbsListView內部狀態保持一樣的方法:SparseArray來保存對應位置的狀態
/**? ? * 顯示未讀消息? ? *? ? * @paramposition 顯示tab位置? ? * @paramnum? ? ? num小于等于0顯示紅點,num大于0顯示數字? ? */publicvoidshowMsg(intposition,intnum) {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);? ? ? ? }? ? }/**? ? * 顯示未讀紅點? ? *? ? * @paramposition 顯示tab位置? ? */publicvoidshowDot(intposition) {if(position >= mTabCount) {? ? ? ? ? ? position = mTabCount -1;? ? ? ? }? ? ? ? showMsg(position,0);? ? }/** 隱藏未讀消息 */publicvoidhideMsg(intposition) {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);? ? ? ? }? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
細心的你一定會發現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 {privateArrayList fragments =newArrayList<>();privateString[] titles;publicInnerPagerAdapter(FragmentManager fm, ArrayList fragments, String[] titles) {super(fm);this.fragments = fragments;this.titles = titles;? ? ? ? }@OverridepublicintgetCount() {returnfragments.size();? ? ? ? }@OverridepublicCharSequencegetPageTitle(intposition) {returntitles[position];? ? ? ? }@OverridepublicFragmentgetItem(intposition) {returnfragments.get(position);? ? ? ? }@OverridepublicvoiddestroyItem(ViewGroup container,intposition, Object object) {// 覆寫destroyItem并且空實現,這樣每個Fragment中的視圖就不會被銷毀// super.destroyItem(container, position, object);}@OverridepublicintgetItemPosition(Object object) {returnPagerAdapter.POSITION_NONE;? ? ? ? }? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
CommonTabLayout與上面SlidingTabLayout百分之99的相似度,重復的不在敘述,區別點在于setTabData和notifyDataSetChanged方法,notifyDataSetChanged根據Icon的Gravity屬性進入不同布局的View做Tab,雖然TextView有drawableLeftRightTopBottom的相關屬性,但是并不能讓我們那么自由的控制Ui。
/** 更新數據 */publicvoidnotifyDataSetChanged() {? ? ? ? mTabsContainer.removeAllViews();this.mTabCount = mTabEntitys.size();? ? ? ? View tabView;for(inti =0; i < mTabCount; i++) {if(mIconGravity == Gravity.LEFT) {? ? ? ? ? ? ? ? tabView = View.inflate(mContext, R.layout.layout_tab_left,null);? ? ? ? ? ? }elseif(mIconGravity == Gravity.RIGHT) {? ? ? ? ? ? ? ? tabView = View.inflate(mContext, R.layout.layout_tab_right,null);? ? ? ? ? ? }elseif(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();? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(newOnClickListener() {@OverridepublicvoidonClick(View v) {intposition = (Integer) v.getTag();if(mCurrentTab != position) {? ? ? ? ? ? ? ? ? ? setCurrentTab(position);if(mListener !=null) {? ? ? ? ? ? ? ? ? ? ? ? mListener.onTabSelect(position);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }else{if(mListener !=null) {? ? ? ? ? ? ? ? ? ? ? ? mListener.onTabReselect(position);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? });//setter and getterpublicvoidsetCurrentTab(intcurrentTab) {? ? ? ? mLastTab =this.mCurrentTab;this.mCurrentTab = currentTab;? ? ? ? updateTabSelection(currentTab);if(mFragmentChangeManager !=null) {? ? ? ? ? ? mFragmentChangeManager.setFragments(currentTab);? ? ? ? }if(mIndicatorAnimEnable) {? ? ? ? ? ? calcOffset();? ? ? ? }else{? ? ? ? ? ? invalidate();? ? ? ? }? ? }@OverridepublicvoidonAnimationUpdate(ValueAnimator animation) {? ? ? ? IndicatorPoint p = (IndicatorPoint) animation.getAnimatedValue();? ? ? ? mIndicatorRect.left = (int) p.left;? ? ? ? mIndicatorRect.right = (int) p.right;? ? ? ? invalidate();? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
首先說一點這里不提供demo,需要去官方https://github.com/H07000223/FlycoTabLayout?down ,其次呢這個庫看了之后還是有很大收獲的,比如自定義屬性的運用, setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList fragments)內部實例一個adapter適配器,最重要的是自定義屬性解析和屬性值代碼設置通過一個類來代理完成。