封裝實(shí)踐——仿微信底部Tab欄

可怕的用戶習(xí)慣

目前市面上很多App都采用底部一個(gè)Tab欄橄妆,管理四到五個(gè)Tab齐媒,然后選擇切換頁(yè)面的方式的設(shè)計(jì)巷帝,這雖然不太符合material design,但卻是一個(gè)不容易出錯(cuò)而又符合國(guó)人使用習(xí)慣的設(shè)計(jì)方式研侣。用戶習(xí)慣是個(gè)可怕的東西谱邪,早在4.0之前,Android幾乎無(wú)UI設(shè)計(jì)可言庶诡,于是乎各種仿IOS設(shè)計(jì)大行其道惦银,久而久之用戶也就習(xí)慣于斯。而Android真正推出material design時(shí)末誓,用戶反而不習(xí)慣扯俱。今天要封裝的這種底部Tab欄的展現(xiàn)方式,微信喇澡,支付寶蘸吓,網(wǎng)易新聞,簡(jiǎn)書(shū)等都采用這種設(shè)計(jì)撩幽。而所謂封裝一定是基于某種確定的業(yè)務(wù)需求库继,所以針對(duì)上述這種常見(jiàn)的設(shè)計(jì)方式,我們可以做一個(gè)比較通用的封裝窜醉。

為什么要做封裝

你可能會(huì)覺(jué)得宪萄,這就是一個(gè)選擇切換嘛拜英,我只要做些if else判斷就好了。但是Tab欄一般用在首頁(yè)居凶,紛繁蕪雜的業(yè)務(wù)邏輯和龐大代碼量就不用說(shuō)了,如果這時(shí)候不想被各種if else 侠碧, swich case 搞得心力交瘁,那么我們少寫(xiě)些冗余代碼又有何妨弄兜。畢竟代碼不止眼前的茍且,還有設(shè)計(jì)改版和需求變更替饿,某天產(chǎn)品經(jīng)理更你說(shuō)要改版,修改完xml布局,再去修改if else判斷视卢,然后再去修改click事件廊驼。颊埃。。想想也是醉了班利。所以這里要說(shuō)的封裝當(dāng)然不會(huì)是,一個(gè)LinearLayout塞幾個(gè)布局罗标,然后做swich case去切換fragment,我希望布局里只需要include一個(gè)view积蜻,代碼里也不需要N多findviewbyId闯割,更不想添加各種if else 判斷,就能實(shí)現(xiàn)上述需求竿拆。

官方的TabLayout

官方也有一個(gè)TabLayout宙拉,在android.widget包里。既然官方都有了丙笋,為什么還要重復(fù)造輪子呢谢澈。仔細(xì)看看官方源碼和使用說(shuō)明,這個(gè)TabLayout建議使用在頂部御板,配合Viewpager使用锥忿,甚至還可以左右滑動(dòng)。就像當(dāng)初這版不太被用戶接受的微信一樣(如下圖)怠肋,tab欄放在頂部敬鬓。當(dāng)然官方這個(gè)TabLayout非要放在底部,重寫(xiě)下樣式布局笙各,自己改造下也能滿足底部Tab欄的需求钉答,但是T恤改成底褲穿的感覺(jué)總是怪怪的,所以那要不然杈抢,我們還是自己造個(gè)輪子吧数尿。


2.jpg

化整為零

基于以上需求和分析,可以開(kāi)工編碼了春感。我們還是以微信為例吧砌创,假設(shè)底部Tab欄共有四個(gè)按鈕虏缸,上面icon鲫懒,下面文本。那么我們先把這一樣式的xml寫(xiě)出來(lái)刽辙,我這里先用merge標(biāo)簽窥岩,原因不說(shuō)了。

<?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>

上面是四個(gè)Tab按鈕的通用布局宰缤,上面一個(gè)icon,下面是文字颂翼,非常簡(jiǎn)單晃洒。我們還需要寫(xiě)個(gè)TabView來(lái)解析這個(gè)布局。

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) {

    }
}

化零為整

到這里我們已經(jīng)完成了單個(gè)TabView按鈕的解析醒第,但是我們現(xiàn)在有四個(gè)按鈕愕宋,要在xml里include四次嘛途乃,要在代碼里findviewById四次嘛寸莫,對(duì)于這樣的hard code我是拒絕的并思,我希望在xml里只include一個(gè)view宋彼,代碼里只findviewById一次,所以我們還需要給TabView再包一層输枯,給四個(gè)Tab按鈕一個(gè)父容器TabLayout桃熄,我們只需要include一個(gè)父容器型奥,就能達(dá)到現(xiàn)在一片頂過(guò)去五片,一口氣上五樓螟深,不費(fèi)勁的效果界弧。我們把一個(gè)TabView看做是一個(gè)對(duì)象垢箕,需要幾個(gè)就new幾個(gè)条获,然后add到TabLayout里帅掘。所以首先我需要一個(gè)TabView的對(duì)象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;
    }
}

然后再寫(xiě)個(gè)父容器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);
    }
}

以上都是小學(xué)五年級(jí)水平的代碼计维,所以我就不寫(xiě)注釋了鲫惶,也不需要做過(guò)多講解欠母,直接看代碼赏淌。到這里我們基本完成了底部TabLayout代碼的編寫(xiě)六水,那我們寫(xiě)個(gè)activity測(cè)試下效果先辣卒。
先把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>

是你代碼寫(xiě)的丑想帅,而不是產(chǎn)品狗故意讓你下班不能走

這里TabLayout實(shí)際上是一個(gè)容器啡莉,底部需要幾個(gè)Tab按鈕咧欣,就在MainActiviy里new幾個(gè)然后add到TabLayout即可该押。所以有一天產(chǎn)品經(jīng)理跟你說(shuō)需要增加一個(gè)按鈕,只需要再new一個(gè)add進(jìn)去就好烟具,又有一天boss說(shuō)把底部Tab欄順序調(diào)整下唄朝聋,就只要調(diào)整下new出的TabView順序即可冀痕。這種兵來(lái)將擋水來(lái)土掩的感覺(jué)真好狸演,再也不怕需求改來(lái)改去了宵距,下班時(shí)間好像可以提前了呢满哪。

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

添加點(diǎn)擊事件

但是我們還沒(méi)有加點(diǎn)擊事件民宿,重點(diǎn)來(lái)了活鹰,我又不想去做一大堆判斷只估,除了if else還有其他辦法嗎嘛仅乓?當(dāng)然有袄抵邸宾抓!switch case笆础紧显!這不等于沒(méi)說(shuō)嘛!我可不可以在點(diǎn)擊的時(shí)候動(dòng)態(tài)的獲取當(dāng)前Fragment招驴,這樣就可以避免一大堆的判斷了别厘,所以我們可以考慮用反射拥诡,JDK已經(jīng)出到1.8了渴肉,我們這里就不要在計(jì)較反射的性能問(wèn)題了。那么我們先在TabItem中增加一個(gè)Fragment變量繼承自BaseFragment披蕉,這個(gè)BaseFragment就是我在ViewPager+Fragment LazyLoad最優(yōu)解中使用的BaseFragment嚣艇。

public Class<? extends BaseFragment>tagFragmentClz;

然后構(gòu)造函數(shù)里也加一個(gè)參數(shù)食零,先偷個(gè)懶姑且寫(xiě)在構(gòu)造函數(shù)里贰谣。

public TabItem(int imageResId, int lableResId, Class<? extends BaseFragment> tagFragmentClz) {
    this.imageResId = imageResId;
    this.lableResId = lableResId;
    this.tagFragmentClz = tagFragmentClz;
}

相應(yīng)的MainActivity里的引用也要修改下吱抚,第三個(gè)參數(shù)就傳入相應(yīng)的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));

然后點(diǎn)擊事件的方法如下:

@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

滑動(dòng)切換

我們只用兩行代碼就完成了Fragment的切換既绕,這里我先用replace()做切換凄贩,以后有機(jī)會(huì)再探討replace()和Add()疲扎,hide()的區(qū)別,然后我們還需要再處理下按鈕的選中狀態(tài)壹甥。一個(gè)模仿微信的底部導(dǎo)航欄就初見(jiàn)雛形了盹廷,但是微信是可以滑動(dòng)切換的久橙,我們這個(gè)還不能滑動(dòng)切換淆衷,所以我們還要對(duì)以上代碼做些調(diào)整祝拯,毫無(wú)疑問(wèn)這個(gè)時(shí)候viewpager要出場(chǎng)了她肯。我們把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"/>

還要寫(xiě)一個(gè)viewPager的適配器籽前,這個(gè)時(shí)候我們選擇把a(bǔ)dapter寫(xiě)為內(nèi)部類(lèi),這樣會(huì)更方便一點(diǎn)動(dòng)態(tài)獲取Fragment肄梨。然后之前onTabClick()中通過(guò)反射獲取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();
        }

    }

切換狀態(tài)改變

適配好viewpager后甜害,滑動(dòng)的時(shí)候我們還需要對(duì)title欄和底部Tab欄做相應(yīng)的狀態(tài)改變尔店。這里viewPager只需要實(shí)現(xiàn)OnPageChangeListener接口,在onPageSelected(int position)方法中做相應(yīng)的處理嚣州。我這里的title用了actionbar。

@Override          
public void onPageSelected(int position) {
       mTabLayout.setCurrentTab(position);
       actionBar.setTitle(tabs.get(position).lableResId);
}

滑動(dòng)的時(shí)候要改變狀態(tài)情竹,那相應(yīng)的點(diǎn)擊tab欄也要做類(lèi)似操作秦效。

@Override
public void onTabClick(TabItem tabItem) {
    actionBar.setTitle(tabItem.lableResId);
    mViewPager.setCurrentItem(tabs.indexOf(tabItem));
}

其中tabLayout中的setCurrentTab(int i)方法如下阱州。我們聲明兩個(gè)變量法梯,tabCount用來(lái)記錄底部tabView的個(gè)數(shù)立哑,selectView用來(lái)標(biāo)識(shí)被選中的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

自此一個(gè)模仿微信的底部Tab欄的封裝基本實(shí)現(xiàn)了。沒(méi)找到比較好的gif錄制軟件至耻,所以看起來(lái)怪怪的。

本文首發(fā):CSDN
次發(fā):簡(jiǎn)書(shū)
有需要代碼的點(diǎn)這里:GitHub走触。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末互广,一起剝皮案震驚了整個(gè)濱河市卧土,隨后出現(xiàn)的幾起案子尤莺,更是在濱河造成了極大的恐慌,老刑警劉巖媳谁,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柔纵,居然都是意外死亡搁料,警方通過(guò)查閱死者的電腦和手機(jī)郭计,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)觉啊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杠人,“玉大人嗡善,你說(shuō)我怎么就攤上這事≌忠” “怎么了袁铐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵剔桨,是天一觀的道長(zhǎng)徙融。 經(jīng)常有香客問(wèn)我欺冀,道長(zhǎng),這世上最難降的妖魔是什么饺饭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任瘫俊,我火速辦了婚禮,結(jié)果婚禮上仅淑,老公的妹妹穿的比我還像新娘涯竟。我一直安慰自己空厌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布筐钟。 她就那樣靜靜地躺著篓冲,像睡著了一般壹将。 火紅的嫁衣襯著肌膚如雪毛嫉。 梳的紋絲不亂的頭發(fā)上承粤,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音仙粱,去河邊找鬼浪讳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛口猜,可吹牛的內(nèi)容都是我干的济炎。 我是一名探鬼主播须尚,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼密幔!你這毒婦竟也來(lái)了胯甩?” 一聲冷哼從身側(cè)響起堪嫂,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皆串,失蹤者是張志新(化名)和其女友劉穎恶复,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體塔插,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了违诗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疮蹦。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愕乎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绅项,到底是詐尸還是另有隱情快耿,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布掀亥,位于F島的核電站搪花,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丁稀。R本人自食惡果不足惜倚聚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一惑折、第九天 我趴在偏房一處隱蔽的房頂上張望授账。 院中可真熱鬧惨驶,春花似錦白热、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至续扔,卻和暖如春攻臀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纱昧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工刨啸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人识脆。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓离例,卻偏偏與公主長(zhǎng)得像粘招,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子袍冷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評(píng)論 25 707
  • 當(dāng)開(kāi)始一個(gè)新項(xiàng)目的時(shí)候,有一個(gè)很重要的步驟就是確定我們的APP首頁(yè)框架,也就是用戶從桌面點(diǎn)擊APP 圖標(biāo)婿崭,進(jìn)入AP...
    依然范特稀西閱讀 51,248評(píng)論 16 239
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,708評(píng)論 22 664
  • 昨天晚上我夢(mèng)見(jiàn)了他婉陷,很清晰的夢(mèng),夢(mèng)見(jiàn)我們又在一起了担神,他回來(lái)找我,我很開(kāi)心亥贸,我們一起吃著自己都叫不出名字的水果,我拉...
    小生活小滋味閱讀 358評(píng)論 0 0
  • 第一章 第三章 第二章:冤家路窄 我們那一屆剛碰上市南中學(xué)特設(shè)藝體班,陳素素做為音樂(lè)特長(zhǎng)生被招了進(jìn)來(lái)。一心只想和過(guò)...
    離別情閱讀 387評(píng)論 0 1