封裝實踐——仿微信底部Tab欄

可怕的用戶習慣

目前市面上很多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恤改成底褲穿的感覺總是怪怪的,所以那要不然,我們還是自己造個輪子吧。


2.jpg

化整為零

基于以上需求和分析,可以開工編碼了。我們還是以微信為例吧,假設底部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) {

    }
}
3.jpg

添加點擊事件

但是我們還沒有加點擊事件,重點來了,我又不想去做一大堆判斷,除了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();
    }
}

device-2016-04-08-002202.gif

滑動切換

我們只用兩行代碼就完成了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;
        }
        }
    }
device-2016-04-11-095800.gif

自此一個模仿微信的底部Tab欄的封裝基本實現了。沒找到比較好的gif錄制軟件,所以看起來怪怪的。

本文首發:CSDN
次發:簡書
有需要代碼的點這里:GitHub

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容