可怕的用戶習慣
目前市面上很多App都采用底部一個Tab欄,管理四到五個Tab,然后選擇切換頁面的方式的設計,這雖然不太符合material design,但卻是一個不容易出錯而又符合國人使用習慣的設計方式。用戶習慣是個可怕的東西,早在4.0之前,Android幾乎無UI設計可言,于是乎各種仿IOS設計大行其道,久而久之用戶也就習慣于斯。而Android真正推出material design時,用戶反而不習慣。今天要封裝的這種底部Tab欄的展現方式,微信,支付寶,網易新聞,簡書等都采用這種設計。而所謂封裝一定是基于某種確定的業務需求,所以針對上述這種常見的設計方式,我們可以做一個比較通用的封裝。
為什么要做封裝
你可能會覺得,這就是一個選擇切換嘛,我只要做些if else判斷就好了。但是Tab欄一般用在首頁,紛繁蕪雜的業務邏輯和龐大代碼量就不用說了,如果這時候不想被各種if else , swich case 搞得心力交瘁,那么我們少寫些冗余代碼又有何妨。畢竟代碼不止眼前的茍且,還有設計改版和需求變更,某天產品經理更你說要改版,修改完xml布局,再去修改if else判斷,然后再去修改click事件。。。想想也是醉了。所以這里要說的封裝當然不會是,一個LinearLayout塞幾個布局,然后做swich case去切換fragment,我希望布局里只需要include一個view,代碼里也不需要N多findviewbyId,更不想添加各種if else 判斷,就能實現上述需求。
官方的TabLayout
官方也有一個TabLayout,在android.widget包里。既然官方都有了,為什么還要重復造輪子呢。仔細看看官方源碼和使用說明,這個TabLayout建議使用在頂部,配合Viewpager使用,甚至還可以左右滑動。就像當初這版不太被用戶接受的微信一樣(如下圖),tab欄放在頂部。當然官方這個TabLayout非要放在底部,重寫下樣式布局,自己改造下也能滿足底部Tab欄的需求,但是T恤改成底褲穿的感覺總是怪怪的,所以那要不然,我們還是自己造個輪子吧。
化整為零
基于以上需求和分析,可以開工編碼了。我們還是以微信為例吧,假設底部Tab欄共有四個按鈕,上面icon,下面文本。那么我們先把這一樣式的xml寫出來,我這里先用merge標簽,原因不說了。
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/tab_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tab_lable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</merge>
上面是四個Tab按鈕的通用布局,上面一個icon,下面是文字,非常簡單。我們還需要寫個TabView來解析這個布局。
public class TabView extends LinearLayout implements View.OnClickListener{
private ImageView mTabImage;
private TextView mTabLable;
public TabView(Context context) {
super(context);
initView(context);
}
public TabView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public TabView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
private void initView(Context context){
setOrientation(VERTICAL);
setGravity(Gravity.CENTER);
LayoutInflater.from(context).inflate(R.layout.tab_view,this,true);
mTabImage=(ImageView)findViewById(R.id.tab_image);
mTabLable=(TextView)findViewById(R.id.tab_lable);
}
public void initData(TabItem tabItem){
mTabImage.setImageResource(tabItem.imageResId);
mTabLable.setText(tabItem.lableResId);
}
@Override
public void onClick(View v) {
}
}
化零為整
到這里我們已經完成了單個TabView按鈕的解析,但是我們現在有四個按鈕,要在xml里include四次嘛,要在代碼里findviewById四次嘛,對于這樣的hard code我是拒絕的,我希望在xml里只include一個view,代碼里只findviewById一次,所以我們還需要給TabView再包一層,給四個Tab按鈕一個父容器TabLayout,我們只需要include一個父容器,就能達到現在一片頂過去五片,一口氣上五樓,不費勁的效果。我們把一個TabView看做是一個對象,需要幾個就new幾個,然后add到TabLayout里。所以首先我需要一個TabView的對象TabItem。
/**
* Created by yx on 16/4/3.
*/
public class TabItem {
/**
* icon
*/
public int imageResId;
/**
* 文本
*/
public int lableResId;
public TabItem(int imageResId, int lableResId) {
this.imageResId = imageResId;
this.lableResId = lableResId;
}
}
然后再寫個父容器TabLayout,我們姑且也叫TabLayout吧。
public class TabLayout extends LinearLayout implements View.OnClickListener{
private ArrayList<TabItem> tabs;
private OnTabClickListener listener;
public TabLayout(Context context) {
super(context);
initView();
}
public TabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TabLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView(){
setOrientation(HORIZONTAL);
}
public void initData(ArrayList<TabItem>tabs,OnTabClickListener listener){
this.tabs=tabs;
this.listener=listener;
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
params.weight=1;
if(tabs!=null&&tabs.size()>0){
TabView mTabView=null;
for(int i=0;i< tabs.size();i++){
mTabView=new TabView(getContext());
mTabView.setTag(tabs.get(i));
mTabView.initData(tabs.get(i));
mTabView.setOnClickListener(this);
addView(mTabView,params);
}
}else{
throw new IllegalArgumentException("tabs can not be empty");
}
}
@Override
public void onClick(View v) {
listener.onTabClick((TabItem)v.getTag());
}
public interface OnTabClickListener{
void onTabClick(TabItem tabItem);
}
}
以上都是小學五年級水平的代碼,所以我就不寫注釋了,也不需要做過多講解,直接看代碼。到這里我們基本完成了底部TabLayout代碼的編寫,那我們寫個activity測試下效果先。
先把TabLayout include到布局中
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/tab_layout"
/>
<star.yx.tabview.TabLayout
android:id="@+id/tab_layout"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="50dp"
/>
</RelativeLayout>
是你代碼寫的丑,而不是產品狗故意讓你下班不能走
這里TabLayout實際上是一個容器,底部需要幾個Tab按鈕,就在MainActiviy里new幾個然后add到TabLayout即可。所以有一天產品經理跟你說需要增加一個按鈕,只需要再new一個add進去就好,又有一天boss說把底部Tab欄順序調整下唄,就只要調整下new出的TabView順序即可。這種兵來將擋水來土掩的感覺真好,再也不怕需求改來改去了,下班時間好像可以提前了呢。
public class MainActivity extends ActionBarActivity implements TabLayout.OnTabClickListener{
private TabLayout mTabLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView(){
mTabLayout=(TabLayout)findViewById(R.id.tab_layout);
}
private void initData(){
ArrayList<TabItem>tabs=new ArrayList<TabItem>();
tabs.add(new TabItem(R.drawable.selector_tab_msg,R.string.wechat));
tabs.add(new TabItem(R.drawable.selector_tab_contact,R.string.contacts));
tabs.add(new TabItem(R.drawable.selector_tab_moments,R.string.discover));
tabs.add(new TabItem(R.drawable.selector_tab_profile,R.string.me));
mTabLayout.initData(tabs, this);
}
@Override
public void onTabClick(TabItem tabItem) {
}
}
添加點擊事件
但是我們還沒有加點擊事件,重點來了,我又不想去做一大堆判斷,除了if else還有其他辦法嗎嘛?當然有啊!switch case啊!這不等于沒說嘛!我可不可以在點擊的時候動態的獲取當前Fragment,這樣就可以避免一大堆的判斷了,所以我們可以考慮用反射,JDK已經出到1.8了,我們這里就不要在計較反射的性能問題了。那么我們先在TabItem中增加一個Fragment變量繼承自BaseFragment,這個BaseFragment就是我在ViewPager+Fragment LazyLoad最優解中使用的BaseFragment。
public Class<? extends BaseFragment>tagFragmentClz;
然后構造函數里也加一個參數,先偷個懶姑且寫在構造函數里。
public TabItem(int imageResId, int lableResId, Class<? extends BaseFragment> tagFragmentClz) {
this.imageResId = imageResId;
this.lableResId = lableResId;
this.tagFragmentClz = tagFragmentClz;
}
相應的MainActivity里的引用也要修改下,第三個參數就傳入相應的Fragment。
ArrayList<TabItem>tabs=new ArrayList<TabItem>();
tabs.add(new TabItem(R.drawable.selector_tab_msg, R.string.wechat, WechatFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_contact, R.string.contacts, ContactsFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_moments, R.string.discover, DiscoverFragment.class));
tabs.add(new TabItem(R.drawable.selector_tab_profile, R.string.me, ProfileFragment.class));
然后點擊事件的方法如下:
@Override
public void onTabClick(TabItem tabItem) {
try {
BaseFragment fragment= tabItem.tagFragmentClz.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.fragment,fragment).commitAllowingStateLoss();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
滑動切換
我們只用兩行代碼就完成了Fragment的切換,這里我先用replace()做切換,以后有機會再探討replace()和Add(),hide()的區別,然后我們還需要再處理下按鈕的選中狀態。一個模仿微信的底部導航欄就初見雛形了,但是微信是可以滑動切換的,我們這個還不能滑動切換,所以我們還要對以上代碼做些調整,毫無疑問這個時候viewpager要出場了。我們把MainActivity中之前的Framelayout替換成Viewpager。
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_above="@id/tab_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
還要寫一個viewPager的適配器,這個時候我們選擇把adapter寫為內部類,這樣會更方便一點動態獲取Fragment。然后之前onTabClick()中通過反射獲取Fragment的方法挪到adapter中的getItem()方法中,代碼如下。
public class FragAdapter extends FragmentPagerAdapter {
public FragAdapter(FragmentManager fm) {
super(fm);
// TODO Auto-generated constructor stub
}
@Override
public Fragment getItem(int arg0) {
// TODO Auto-generated method stub
try {
return tabs.get(arg0).tagFragmentClz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return fragment;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return tabs.size();
}
}
切換狀態改變
適配好viewpager后,滑動的時候我們還需要對title欄和底部Tab欄做相應的狀態改變。這里viewPager只需要實現OnPageChangeListener接口,在onPageSelected(int position)方法中做相應的處理。我這里的title用了actionbar。
@Override
public void onPageSelected(int position) {
mTabLayout.setCurrentTab(position);
actionBar.setTitle(tabs.get(position).lableResId);
}
滑動的時候要改變狀態,那相應的點擊tab欄也要做類似操作。
@Override
public void onTabClick(TabItem tabItem) {
actionBar.setTitle(tabItem.lableResId);
mViewPager.setCurrentItem(tabs.indexOf(tabItem));
}
其中tabLayout中的setCurrentTab(int i)方法如下。我們聲明兩個變量,tabCount用來記錄底部tabView的個數,selectView用來標識被選中的View。
public void setCurrentTab(int i) {
if (i < tabCount && i >= 0) {
View view = getChildAt(i);
if (selectView != view) {
view.setSelected(true);
if (selectView != null) {
selectView.setSelected(false);
}
selectView = view;
}
}
}
自此一個模仿微信的底部Tab欄的封裝基本實現了。沒找到比較好的gif錄制軟件,所以看起來怪怪的。
本文首發:CSDN
次發:簡書
有需要代碼的點這里:GitHub。