Android從零擼美團(三) - Android多標籤tab滑動切換 - 自定義View快速實現高度定製封裝

這是【從零擼美團】系列文章第三篇
【從零擼美團】是一個高仿美團的開源項目,旨在鞏固 Android 相關知識的同時,幫助到有須要的小夥伴。
GitHub 源碼地址:github.com/cachecats/L…java

Android從零擼美團(一) - 統一管理 Gradle 依賴 提取到單獨文件中android

Android從零擼美團(二) - 仿美團下拉刷新自定義動畫git

Android從零擼美團(四) - 美團首頁佈局解析及實現 - Banner+自定義View+SmartRefreshLayout下拉刷新上拉加載更多github


每一個項目基本都會有多個 Tab ,以期在有限的屏幕空間展示更多的功能。 有需求就會有市場,現在也出現了不少優秀的 tab 切換框架,使用者衆多。bash

可是深刻思考以後仍是決定本身造輪子~app

由於框架雖好,可不要貪杯哦~ 使用第三方框架最大的問題在於並不能徹底知足實際需求,有的是 icon 圖片 跟文字間距沒法調整,有的後期會出現各類各樣問題,不利於維護。 最重要的是本身寫一個也不是很複雜,有研究框架填坑的時間也就寫出來了。框架

先看怎麼用:一句代碼搞定ide

tabWidget.init(getSupportFragmentManager(), fragmentList);
複製代碼

再上效果圖: 函數

在這裏插入圖片描述

你沒看錯,長得跟美團如出一轍,畢竟這個項目就叫【從零擼美團】 ㄟ( ▔, ▔ )ㄏ佈局

1、思路

底部 tab 佈局有不少實現方式,好比 RadioButton、FragmentTabHost、自定義組合View等。這裏採用的是自定義組合View方式,由於可定製度更高。 滑動切換基本都是採用 ViewPager + Fragment ,集成簡單,方案較成熟。這裏一樣採用這種方式。

2、準備

開始以前須要準備兩樣東西:

  1. 五個 tab 的選中和未選中狀態的 icon 圖片共計10張
  2. 五個 Fragment

這是最基本的素材,有了素材以後就開始幹活吧~ 因爲要實現點擊選中圖片和文字都變色成選中狀態,沒有選中就變成灰色,因此要對每組 icon 創建一個 selector xml文件實現狀態切換。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_vector_home_pressed" android:state_activated="true" />
    <item android:drawable="@drawable/ic_vector_home_normal" android:state_activated="false" />
</selector>
複製代碼

這裏用了 android:state_activated 做爲狀態標記,由於最經常使用的 pressedfocused 都達不到長久保持狀態的要求,都是鬆開手指以後就恢復了。在代碼中手動設置 activated 值就好。 注意:此處設置的是 icon 圖片,因此用 android:drawable,與下面文字使用的 android:color 有區別。

設置完圖片資源後,該設置文字顏色的 selector 了,由於文字的顏色也要跟着變。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/meituanGreen" android:state_activated="true" />
    <item android:color="@color/gray666" android:state_activated="false" />
</selector>
複製代碼

注意圖片用 android:drawable,文字用 android:color

3、實現

準備工做作完以後,就開始正式的自定義View啦。

1. 寫佈局

首先是佈局文件:

widget_custom_bottom_tab.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_tab_widget"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <!--下面的tab標籤佈局-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="3dp"
        android:paddingTop="3dp"
        >

        <LinearLayout
            android:id="@+id/ll_menu_home_page"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_home"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_home" />

            <TextView
                android:id="@+id/tv_menu_home"
                style="@style/menuTextStyle"
                android:text="首頁" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_nearby"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_nearby"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_nearby" />

            <TextView
                android:id="@+id/tv_menu_nearby"
                style="@style/menuTextStyle"
                android:text="附近" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_discover"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_discover"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_discover" />

            <TextView
                android:id="@+id/tv_menu_discover"
                style="@style/menuTextStyle"
                android:text="發現" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_order"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_order"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_order" />

            <TextView
                android:id="@+id/tv_menu_order"
                style="@style/menuTextStyle"
                android:text="訂單" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_mine"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_mine"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_mine" />

            <TextView
                android:id="@+id/tv_menu_mine"
                style="@style/menuTextStyle"
                android:text="個人" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>
複製代碼

最外層用豎向排列的 LinearLayout 包裹,它有兩個子節點,上面是用於滑動和裝載 FragmentViewPager,下面是五個 Tab 的佈局。 爲了方便管理把幾個 ImageViewTextView 的共有屬性抽取到 styles.xml 裏了:

<!--菜單欄的圖標樣式-->
    <style name="menuIconStyle" >
        <item name="android:layout_width">25dp</item>
        <item name="android:layout_height">25dp</item>
    </style>

    <!--菜單欄的文字樣式-->
    <style name="menuTextStyle">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">@drawable/selector_menu_text_color</item>
        <item name="android:textSize">12sp</item>
        <item name="android:layout_marginTop">3dp</item>
    </style>
複製代碼

有了佈局文件以後,就開始真正的自定義 View 吧。

2. 寫 Java 代碼自定義View

新建 java 文件 CustomBottomTabWidget 繼承自 LinearLayout。爲何繼承 LinearLayout 呢?由於咱們的佈局文件根節點就是 LinearLayout 呀,根節點是什麼就繼承什麼。

先上代碼吧:

package com.cachecats.meituan.widget.bottomtab;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

import com.cachecats.meituan.R;
import com.cachecats.meituan.base.BaseFragment;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;


public class CustomBottomTabWidget extends LinearLayout {

    @BindView(R.id.ll_menu_home_page)
    LinearLayout llMenuHome;
    @BindView(R.id.ll_menu_nearby)
    LinearLayout llMenuNearby;
    @BindView(R.id.ll_menu_discover)
    LinearLayout llMenuDiscover;
    @BindView(R.id.ll_menu_order)
    LinearLayout llMenuOrder;
    @BindView(R.id.ll_menu_mine)
    LinearLayout llMenuMine;
    @BindView(R.id.vp_tab_widget)
    ViewPager viewPager;

    private FragmentManager mFragmentManager;
    private List<BaseFragment> mFragmentList;
    private TabPagerAdapter mAdapter;

    public CustomBottomTabWidget(Context context) {
        this(context, null, 0);
    }

    public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this);
        ButterKnife.bind(view);

        //設置默認的選中項
        selectTab(MenuTab.HOME);

    }

    /**
     * 外部調用初始化,傳入必要的參數
     *
     * @param fm
     */
    public void init(FragmentManager fm, List<BaseFragment> fragmentList) {
        mFragmentManager = fm;
        mFragmentList = fragmentList;
        initViewPager();
    }

    /**
     * 初始化 ViewPager
     */
    private void initViewPager() {
        mAdapter = new TabPagerAdapter(mFragmentManager, mFragmentList);
        viewPager.setAdapter(mAdapter);
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                //將ViewPager與下面的tab關聯起來
                switch (position) {
                    case 0:
                        selectTab(MenuTab.HOME);
                        break;
                    case 1:
                        selectTab(MenuTab.NEARBY);
                        break;
                    case 2:
                        selectTab(MenuTab.DISCOVER);
                        break;
                    case 3:
                        selectTab(MenuTab.ORDER);
                        break;
                    case 4:
                        selectTab(MenuTab.MINE);
                        break;
                    default:
                        selectTab(MenuTab.HOME);
                        break;
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });
    }

    /**
     * 點擊事件集合
     */
    @OnClick({R.id.ll_menu_home_page, R.id.ll_menu_nearby, R.id.ll_menu_discover, R.id.ll_menu_order, R.id.ll_menu_mine})
    public void onViewClicked(View view) {

        switch (view.getId()) {
            case R.id.ll_menu_home_page:
                selectTab(MenuTab.HOME);
                //使ViewPager跟隨tab點擊事件滑動
                viewPager.setCurrentItem(0);
                break;
            case R.id.ll_menu_nearby:
                selectTab(MenuTab.NEARBY);
                viewPager.setCurrentItem(1);
                break;
            case R.id.ll_menu_discover:
                selectTab(MenuTab.DISCOVER);
                viewPager.setCurrentItem(2);
                break;
            case R.id.ll_menu_order:
                selectTab(MenuTab.ORDER);
                viewPager.setCurrentItem(3);
                break;
            case R.id.ll_menu_mine:
                selectTab(MenuTab.MINE);
                viewPager.setCurrentItem(4);
                break;
        }
    }

    /**
     * 設置 Tab 的選中狀態
     *
     * @param tab 要選中的標籤
     */
    public void selectTab(MenuTab tab) {
        //先將全部tab取消選中,再單獨設置要選中的tab
        unCheckedAll();

        switch (tab) {
            case HOME:
                llMenuHome.setActivated(true);
                break;
            case NEARBY:
                llMenuNearby.setActivated(true);
                break;
            case DISCOVER:
                llMenuDiscover.setActivated(true);
                break;
            case ORDER:
                llMenuOrder.setActivated(true);
                break;
            case MINE:
                llMenuMine.setActivated(true);
        }

    }


    //讓全部tab都取消選中
    private void unCheckedAll() {
        llMenuHome.setActivated(false);
        llMenuNearby.setActivated(false);
        llMenuDiscover.setActivated(false);
        llMenuOrder.setActivated(false);
        llMenuMine.setActivated(false);
    }

    /**
     * tab的枚舉類型
     */
    public enum MenuTab {
        HOME,
        NEARBY,
        DISCOVER,
        ORDER,
        MINE
    }
}
複製代碼

註釋應該寫的很清楚了,這裏再強調幾個點:

  1. 實現了三個構造方法,這三個構造方法分別對應於不一樣的建立方式。若是不肯定怎麼建立它就都實現吧,不會出錯。 既然不肯定到底走哪一個方法,那把初始化方法寫到哪一個裏面呢?這兒有個小技巧,就是把一個參數的 super(context),和兩個參數的 super(context, attrs) 分別改爲:this(context, null, 0)this(context, attrs, 0)。這樣不管走的哪一個構造函數,最終都會走到三個參數的構造函數裏,咱們只要把初始化操做放在這個函數裏就好了。
  2. 構造函數裏的這行代碼:
    View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this);
    複製代碼
    widget_custom_bottom_tab.xml 文件與 java 代碼綁定了起來,注意最後 一個參數是 this 而不是 null
  3. 本項目用到了 ButterKnifefindViewById() 解脫出來。
  4. 切換選中未選中狀態的原理是每次點擊的時候,先調用 unCheckedAll () 將全部 tab 都置爲未選中狀態,再單獨設置要選中的 tab 爲選中狀態 llMenuHome.setActivated(true);
  5. 實現 tab 的點擊事件與 ViewPager 的滑動綁定須要在兩個地方寫邏輯: 1)tab 的點擊回調裏執行下面兩行代碼,分別使 tab 變爲選中狀態和讓 ViewPager 滑動到相應位置。
    selectTab(MenuTab.HOME);
    //使ViewPager跟隨tab點擊事件滑動
    viewPager.setCurrentItem(0);
    複製代碼
    2)在 ViewPager 的監聽方法 onPageSelected() 中,每滑動到一個頁面,就調用 selectTab(MenuTab.HOME) 方法將對應的 tab 設置爲選中狀態。
  6. 記得在構造方法裏設置默認的選中項:
    //設置默認的選中項
     selectTab(MenuTab.HOME);
    複製代碼

好啦,到這自定義 View 已經完成了。下面看看怎麼使用。

4、使用

在主頁的佈局文件裏直接引用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.cachecats.meituan.app.MainActivity">

    <com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget
        android:id="@+id/tabWidget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
複製代碼

而後在 Activity 裏一句話調用:

tabWidget.init(getSupportFragmentManager(), fragmentList);
複製代碼

就是這麼簡單! 是否是很爽很清新?

貼出 MainActivity 完整代碼:

package com.cachecats.meituan.app;

import android.os.Bundle;

import com.cachecats.meituan.MyApplication;
import com.cachecats.meituan.R;
import com.cachecats.meituan.app.discover.DiscoverFragment;
import com.cachecats.meituan.app.home.HomeFragment;
import com.cachecats.meituan.app.mine.MineFragment;
import com.cachecats.meituan.app.nearby.NearbyFragment;
import com.cachecats.meituan.app.order.OrderFragment;
import com.cachecats.meituan.base.BaseActivity;
import com.cachecats.meituan.base.BaseFragment;
import com.cachecats.meituan.di.DIHelper;
import com.cachecats.meituan.di.components.DaggerActivityComponent;
import com.cachecats.meituan.di.modules.ActivityModule;
import com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends BaseActivity {

    @BindView(R.id.tabWidget)
    CustomBottomTabWidget tabWidget;
    private List<BaseFragment> fragmentList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        DaggerActivityComponent.builder()
                .applicationComponent(MyApplication.getApplicationComponent())
                .activityModule(new ActivityModule(this))
                .build().inject(this);

        //初始化
        init();
    }

    private void init() {
        //構造Fragment的集合
        fragmentList = new ArrayList<>();
        fragmentList.add(new HomeFragment());
        fragmentList.add(new NearbyFragment());
        fragmentList.add(new DiscoverFragment());
        fragmentList.add(new OrderFragment());
        fragmentList.add(new MineFragment());

        //初始化CustomBottomTabWidget
        tabWidget.init(getSupportFragmentManager(), fragmentList);
    }
}

複製代碼

整個代碼很簡單,只須要構造出 Fragment 的列表傳給 CustomBottomTabWidget 就好啦。

總結:本身造輪子可能前期封裝花些時間,但本身寫的代碼本身最清楚,幾個月後再改需求改代碼能快速的定位到要改的地方,便於維護。 而且最後封裝完用起來也很簡單啊,不用在 Activity 裏寫那麼多配置代碼,總體邏輯更清晰,耦合度更低。


以上就是用自定義 View 的方式實現高度定製化的多 tab 標籤滑動切換實例。
源碼地址:github.com/cachecats/L…
歡迎下載,歡迎 star,歡迎點贊~

相關文章
相關標籤/搜索