(2019年10月更新) Android 最全的底部導航欄實現方法

本文(爭取做到)Android 最全的底部導航欄實現方法.

現在寫了4個主要方法.

 

 

官方方法. 官方的 BottomNavigationActivity

使用Android studio 新建一個工程,可以選擇到這個BottomNavigationActivity。

或者在工程裏新建BottomNavigationActivity

會創建出這麼幾個文件,分別介紹一下。

navigation/mobile_navigation.xml 如下

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.yao.tab.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.yao.tab.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.yao.tab.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

這裏是使用 xml 配置聲明3個 Fragment 的地方。name 屬性表示對應這個 Fragment 的包名+類名,label 屬性表示標題欄顯示什麼文字,tools:layout 屬性表示這個 fragment 使用哪個佈局(tools屬性只是爲了 Android studio 的界面預覽,實際在每個Fragment之間會inflate這個佈局)。

 

menu/bottom_nav_menu.xml 如下

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>

聲明每個 Tab 的 icon 圖片,title 文字,還有 id 表示應該對應哪個fragment,這裏的 id 應該跟 navigation/mobile_navigation.xml  聲明的 fragment id 一樣。

 

layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

使用 BottomNavigationView 控件配合 fragment 使用。

BottomNavigationView 需要標明 app:menu 屬性 app:menu="@menu/bottom_nav_menu"

fragment 需要標明 app:navGraph 屬性 app:navGraph="@navigation/mobile_navigation"

 

MainActivity.java 如下

package com.yao.tab;

import android.os.Bundle;

import com.google.android.material.bottomnavigation.BottomNavigationView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BottomNavigationView navView = findViewById(R.id.nav_view);
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView, navController);
    }

}

findViewById 找到佈局裏面的 BottomNavigationView。

初始化並設置好 NavController,代表 各個fragment 與其切換的控制邏輯。

使用 NavigationUI 把 BottomNavigationView 和 NavController 關聯。

 

 

Bottom Navigation是5.0(API level 21)推出的一種符合MD規範的導航欄規範。

詳細規範在這篇文章可以有講。

原文: https://material.google.com/components/bottom-navigation.html

譯文:https://modao.cc/posts/3068

除了官方推出的這個 BottomNavigationView,還有一些民間的開源庫也挺不錯。

這些開源庫的第一優勢是有動畫效果,第二優勢是配合 CoordinatorLayout 和一些相應的控件,可以實現滑動伸縮的效果。

GitHub - aurelhubert/ahbottomnavigation

Github - Ashok-Varma/BottomNavigation

我有個小項目就用到了ahbottomnavigation。

 

 

方法一. RadioButton + ViewPager + FragmentPagerAdapter

它的xml如下

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

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="55dp"
        app:cardElevation="12dp">

        <RadioGroup
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:background="?android:attr/windowBackground"
            android:baselineAligned="false"
            android:orientation="horizontal">

            <RadioButton
                android:id="@+id/rb_home"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:button="@null"
                android:checked="true"
                android:drawableTop="@drawable/ic_home"
                android:gravity="center_horizontal"
                android:paddingTop="8dp"
                android:text="@string/title_home"
                android:textColor="@drawable/tab_text_view_color" />

            <RadioButton
                android:id="@+id/rb_dashboard"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:button="@null"
                android:drawableTop="@drawable/ic_dashboard"
                android:gravity="center_horizontal"
                android:paddingTop="8dp"
                android:text="@string/title_dashboard"
                android:textColor="@drawable/tab_text_view_color" />

            <RadioButton
                android:id="@+id/rb_notifications"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:button="@null"
                android:drawableTop="@drawable/ic_notifications"
                android:gravity="center_horizontal"
                android:paddingTop="8dp"
                android:text="@string/title_notifications"
                android:textColor="@drawable/tab_text_view_color" />
        </RadioGroup>
    </androidx.cardview.widget.CardView>

</LinearLayout>

Activity.java 如下

package com.yao.tab;

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.View;
import android.view.Window;
import android.widget.RadioButton;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;

import com.yao.tab.ui.dashboard.DashboardFragment;
import com.yao.tab.ui.home.HomeFragment;
import com.yao.tab.ui.notifications.NotificationsFragment;
import com.yao.tab.util.ResUtil;

public class Tab1Activity extends AppCompatActivity implements View.OnClickListener {

    private static final int FRAGMENT_COUNT = 3;

    private ViewPager mViewPager;

    private RadioButton mRbHome;
    private RadioButton mRbDashboard;
    private RadioButton mRbNotifications;

    private SparseArray<Fragment> fragmentList = new SparseArray<>(3);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_tab_1);

        mViewPager = findViewById(R.id.view_pager);

        mRbHome = findViewById(R.id.rb_home);
        mRbDashboard = findViewById(R.id.rb_dashboard);
        mRbNotifications = findViewById(R.id.rb_notifications);

        //設置 RadioButton 圖標的大小
        Drawable drawableHome = ResUtil.getDrawable(R.drawable.ic_home);
        drawableHome.setBounds(0, 0, ResUtil.getDimen(R.dimen.tab_icon_size), ResUtil.getDimen(R.dimen.tab_icon_size));
        mRbHome.setCompoundDrawables(null, drawableHome, null, null);

        Drawable drawableDashboard = ResUtil.getDrawable(R.drawable.ic_dashboard);
        drawableDashboard.setBounds(0, 0, ResUtil.getDimen(R.dimen.tab_icon_size), ResUtil.getDimen(R.dimen.tab_icon_size));
        mRbDashboard.setCompoundDrawables(null, drawableDashboard, null, null);

        Drawable drawableNotifications = ResUtil.getDrawable(R.drawable.ic_notifications);
        drawableNotifications.setBounds(0, 0, ResUtil.getDimen(R.dimen.tab_icon_size), ResUtil.getDimen(R.dimen.tab_icon_size));
        mRbNotifications.setCompoundDrawables(null, drawableNotifications, null, null);

        mRbHome.setOnClickListener(this);
        mRbDashboard.setOnClickListener(this);
        mRbNotifications.setOnClickListener(this);

        //設置左邊和右邊各緩存多少個頁面。
        //設置成2後,可以保證3個Tab滑動到哪個Tab下,其他Tab都不會被回收。
        mViewPager.setOffscreenPageLimit(2);
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {

            @Override
            public void onPageSelected(int position) {
                if (position == 0) {
                    onClick(mRbHome);
                } else if (position == 1) {
                    onClick(mRbDashboard);
                } else if (position == 2) {
                    onClick(mRbNotifications);
                }
            }

            @Override
            public void onPageScrolled(int arg0, float arg1, int arg2) {}

            @Override
            public void onPageScrollStateChanged(int arg0) {}
        });

        FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            @Override
            public int getCount() {
                return FRAGMENT_COUNT;
            }

            @NonNull
            @Override
            public Fragment getItem(int position) {
                //https://mp.weixin.qq.com/s/MOWdbI5IREjQP1Px-WJY1Q
                if (position == 0) {
                    return new HomeFragment();
                } else if (position == 1) {
                    return new DashboardFragment();
                } else if (position == 2) {
                    return new NotificationsFragment();
                } else {
                    throw new IllegalStateException("FragmentPagerAdapter getItem position is illegal");
                }
            }
        };
        mViewPager.setAdapter(adapter);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.rb_home:
                mViewPager.setCurrentItem(0);
                mRbHome.setChecked(true);
                break;
            case R.id.rb_dashboard:
                mViewPager.setCurrentItem(1);
                mRbDashboard.setChecked(true);
                break;
            case R.id.rb_notifications:
                mViewPager.setCurrentItem(2);
                mRbNotifications.setChecked(true);
                break;
            default:
                break;
        }
    }
}

1.RadioButton 最噁心的地方在於,不能在xml中設置它的圖片展示大小。所以要使用 Drawable.setBounds 的方式在代碼中設置其大小。

2.mViewPager.setOffscreenPageLimit(2); 可以設置左邊和右邊各緩存多少個頁面,這樣3個Tab就不會被回收了。

3.ViewPager 有滑動事件,需要在 addOnPageChangeListener 的 onPageSelected 回調裏,切換對應的 Fragment。

 

 

方法二. TabLayout + ViewPager + FragmentPagerAdapter

它的xml如下,爲了實現分界線效果,加了一層CardView進行包裹。

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

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardElevation="12dp">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            style="@style/MyTabLayoutStyle"
            app:tabTextAppearance="@style/MyTabLayoutTextAppearance"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:background="?android:attr/windowBackground" />
    </androidx.cardview.widget.CardView>

</LinearLayout>

Activity.java 如下

package com.yao.tab;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;

import com.google.android.material.tabs.TabLayout;
import com.yao.tab.ui.dashboard.DashboardFragment;
import com.yao.tab.ui.home.HomeFragment;
import com.yao.tab.ui.notifications.NotificationsFragment;

import java.util.Objects;

public class Tab2Activity extends AppCompatActivity {

    private static final int FRAGMENT_COUNT = 3;

    private ViewPager mViewPager;
    private TabLayout mTabLayout;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_2);

        mViewPager = findViewById(R.id.view_pager);
        mTabLayout = findViewById(R.id.tab_layout);

        mTabLayout.addTab(mTabLayout.newTab());
        mTabLayout.addTab(mTabLayout.newTab());
        mTabLayout.addTab(mTabLayout.newTab());

        //設置左邊和右邊各緩存多少個頁面。
        //設置成2後,可以保證3個Tab滑動到哪個Tab下,其他Tab都不會被回收。
        mViewPager.setOffscreenPageLimit(2);

        mTabLayout.setupWithViewPager(mViewPager, false);
        FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            @Override
            public int getCount() {
                return FRAGMENT_COUNT;
            }

            @NonNull
            @Override
            public Fragment getItem(int position) {
                //https://mp.weixin.qq.com/s/MOWdbI5IREjQP1Px-WJY1Q
                if (position == 0) {
                    return new HomeFragment();
                } else if (position == 1) {
                    return new DashboardFragment();
                } else if (position == 2) {
                    return new NotificationsFragment();
                } else {
                    throw new IllegalStateException("FragmentPagerAdapter getItem position is illegal");
                }
            }
        };
        mViewPager.setAdapter(adapter);

        //需要放在 mTabLayout.setupWithViewPager 後面,否則標題會被替換掉。
        Objects.requireNonNull(mTabLayout.getTabAt(0)).setText(R.string.title_home).setIcon(R.drawable.ic_home);
        Objects.requireNonNull(mTabLayout.getTabAt(1)).setText(R.string.title_dashboard).setIcon(R.drawable.ic_dashboard);
        Objects.requireNonNull(mTabLayout.getTabAt(2)).setText(R.string.title_notifications).setIcon(R.drawable.ic_notifications);
    }
}

TabLayout.addTab 添加3個 Tab。執行 TabLayout.setupWithViewPager 後,再設置每個 Tab 的圖片和文字。ViewPager 和 FragmentPagerAdapter 配合使用作爲頁面。

TabLayout 還有 setCustomView(int) 和 setCustomView(View) 方法,可以自定義 Tab 佈局。

 

以前還有個 FragmentTabHost 可以使用的,現在已經聲明被廢棄了,被這個 TabLayout 取代。

 

ps:根據「https://mp.weixin.qq.com/s/MOWdbI5IREjQP1Px-WJY1Q」,請注意正確使用使用FragmentPagerAdapter寫法。

 

 

方法三. 使用 FragmentTransaction.hide(Fragment) 和 show(Fragment) 

它的xml如下,底部是4個 LinearLayout 作爲 Tab 使用,LinearLayout 裏面是一個 ImageView 和一個 TextView。寫成這種基礎佈局的好處是可以很方便地調節文字和圖片的各種屬性,還可以很方便在每個 Tab 裏添加內容,比如小紅點提醒或者數字提醒。同樣爲了實現分界線效果,加了一層CardView進行包裹。

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

    <FrameLayout
        android:id="@+id/layout_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="55dp"
        app:cardElevation="12dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:background="?android:attr/windowBackground"
            android:baselineAligned="false"
            android:orientation="horizontal">

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

                <ImageView
                    android:id="@+id/iv_home"
                    android:layout_width="@dimen/tab_icon_size"
                    android:layout_height="@dimen/tab_icon_size"
                    android:background="#00000000"
                    android:contentDescription="@string/app_name"
                    android:src="@drawable/ic_home" />

                <TextView
                    android:id="@+id/tv_home"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/title_home"
                    android:textColor="@drawable/tab_text_view_color" />
            </LinearLayout>

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

                <ImageView
                    android:id="@+id/iv_dashboard"
                    android:layout_width="@dimen/tab_icon_size"
                    android:layout_height="@dimen/tab_icon_size"
                    android:background="#00000000"
                    android:contentDescription="@string/app_name"
                    android:src="@drawable/ic_dashboard" />

                <TextView
                    android:id="@+id/tv_dashboard"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/title_dashboard"
                    android:textColor="@drawable/tab_text_view_color" />
            </LinearLayout>

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

                <ImageView
                    android:id="@+id/iv_notifications"
                    android:layout_width="@dimen/tab_icon_size"
                    android:layout_height="@dimen/tab_icon_size"
                    android:background="#00000000"
                    android:contentDescription="@string/app_name"
                    android:src="@drawable/ic_notifications" />

                <TextView
                    android:id="@+id/tv_notifications"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/title_notifications"
                    android:textColor="@drawable/tab_text_view_color" />
            </LinearLayout>

        </LinearLayout>
    </androidx.cardview.widget.CardView>

</LinearLayout>

需要注意的是,ImageView 的 src 和 TextView 的 textColor 都需要用 selector ,可以實現選中後變色的效果。

 

Activity.java 如下,使用了自己封裝好的 FragmentSwitchTool,裏面就是使用 FragmentTransaction.hide(Fragment) 和 show(Fragment)實現的 Fragment 切換。

package com.yao.tab;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.LinearLayout;

import com.yao.tab.ui.dashboard.DashboardFragment;
import com.yao.tab.ui.home.HomeFragment;
import com.yao.tab.ui.notifications.NotificationsFragment;
import com.yao.tab.util.FragmentSwitchTool;

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

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

public class Tab3Activity extends AppCompatActivity {

    private static final String TAG = "Tab3Activity";

    private List<Fragment> mFragments = new ArrayList<>();

    private LinearLayout mLayoutHome;
    private LinearLayout mLayoutDashboard;
    private LinearLayout mLayoutNotifications;
    //當前被選中的Layout,包括圖片和文字。圖片需要是個Selector才能實現選中狀態。
    private LinearLayout mLayoutCurrent;

    private FragmentSwitchTool mFragmentSwitchTool;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_tab_3);

        mLayoutHome = findViewById(R.id.layout_home);
        mLayoutDashboard = findViewById(R.id.layout_dashboard);
        mLayoutNotifications = findViewById(R.id.layout_notifications);

        mFragmentSwitchTool = new FragmentSwitchTool.Builder()
                .fragmentManager(getSupportFragmentManager())
                .containerId(R.id.layout_container)
                .clickableViews(mLayoutHome, mLayoutDashboard, mLayoutNotifications)
                .fragments(new HomeFragment(), new DashboardFragment(), new NotificationsFragment())
                .showAnimator(true)
                .onClickListener(new FragmentSwitchTool.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Log.e(TAG, "Tab3Activity.java - onClick() ----- view:" + getResources().getResourceName(view.getId()));
                    }
                })
                .onDoubleClickListener(new FragmentSwitchTool.OnDoubleClickListener() {
                    @Override
                    public void onDoubleClick(View view) {
                        String viewName = getResources().getResourceName(view.getId());
                        Log.e(TAG, "Tab3Activity.java - onDoubleClick() ----- view:" + viewName);
                        Log.e(TAG, viewName + " 被雙擊,可以執行列表頁面的回到頂部操作");
                    }
                })
                .build();

        mFragmentSwitchTool.changeTag(mLayoutHome);
    }

}

封裝好的 FragmentSwitchTool 如下:

package com.yao.tab.util;

import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;

import com.yao.tab.R;

import java.util.Arrays;
import java.util.List;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

public class FragmentSwitchTool implements OnClickListener {

    public interface OnClickListener {
        void onClick(View view);
    }
    public interface OnDoubleClickListener {
        void onDoubleClick(View view);
    }


    private FragmentManager mFragmentManager;
    private int mContainerId;
    private boolean mShowAnimator;
    private List<View> mClickableViews; //傳入用於被點擊的view,比如是一個LinearLayout
    private Fragment[] mFragments;
    private OnClickListener mOnClickListener;
    private OnDoubleClickListener mOnDoubleOnClickListener;

    private Fragment mCurrentFragment;
    private View mCurrentSelectedView;

    public void setClickableViews(View... clickableViews) {
        this.mClickableViews = Arrays.asList(clickableViews);
        for (View view : clickableViews) {
            view.setOnClickListener(this);
        }
    }

    public void setClickableViewsAndListener(OnClickListener onClickListener, View... clickableViews) {
        mOnClickListener = onClickListener;
    }

    public void setFragments(Fragment... fragments) {
        this.mFragments = fragments;
    }

    public void changeTag(View targetView) {

        if (targetView == mCurrentSelectedView) {
            return;
        }

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
        Fragment targetFragment = mFragmentManager.findFragmentByTag(String.valueOf(targetView.getId()));
        int targetPosition = mClickableViews.indexOf(targetView);
        int currentPosition = mClickableViews.indexOf(mCurrentSelectedView);

        if (mShowAnimator) {
            if (targetPosition > currentPosition) {
                fragmentTransaction.setCustomAnimations(R.animator.slide_right_in, R.animator.slide_left_out);
            } else if (targetPosition < currentPosition) {
                fragmentTransaction.setCustomAnimations(R.animator.slide_left_in, R.animator.slide_right_out);
            }
        }


        if (targetFragment == null) {
            if (mCurrentFragment != null) {
                fragmentTransaction.hide(mCurrentFragment);
                mCurrentSelectedView.setSelected(false);
            }
            targetFragment = mFragments[targetPosition];

            fragmentTransaction.add(mContainerId, targetFragment, String.valueOf(targetView.getId()));
        } else {
            fragmentTransaction.hide(mCurrentFragment);
            mCurrentSelectedView.setSelected(false);
            fragmentTransaction.show(targetFragment);
        }

        fragmentTransaction.commit();
        mCurrentFragment = targetFragment;
        mCurrentSelectedView = targetView;
        mCurrentSelectedView.setSelected(true);
    }

    @Override
    public void onClick(View v) {
        changeTag(v);
    }

    public static final class Builder {
        FragmentManager mFragmentManager;
        int mContainerId;
        boolean mShowAnimator;
        List<View> mClickableViews; //傳入用於被點擊的view,比如是一個LinearLayout
        Fragment[] mFragments;
        OnClickListener mOnClickListener;
        OnDoubleClickListener mOnDoubleClickListener;

        /**
         * 必要參數 Fragment管理類
         * @param fragmentManager fragmentManager
         * @return Builder
         */
        public Builder fragmentManager(FragmentManager fragmentManager) {
            this.mFragmentManager = fragmentManager;
            return this;
        }

        /**
         * 必要參數 fragment容器的佈局id
         * @param containerId containerId
         * @return Builder
         */
        public Builder containerId(int containerId) {
            this.mContainerId = containerId;
            return this;
        }

        /**
         * 必要參數 Tab裏的可點擊控件
         * @param clickableViews clickableViews
         * @return Builder
         */
        public Builder clickableViews(View... clickableViews) {
            this.mClickableViews = Arrays.asList(clickableViews);
            return this;
        }

        /**
         * 必要參數 Tab對應的Fragment
         * @param fragments fragments
         * @return Builder
         */
        public Builder fragments(Fragment... fragments) {
            this.mFragments = fragments;
            return this;
        }

        /**
         * 可選參數 是否顯示動畫
         * @param showAnimator showAnimator
         * @return Builder
         */
        public Builder showAnimator(boolean showAnimator) {
            this.mShowAnimator = showAnimator;
            return this;
        }

        /**
         * 可選參數 Tab的單擊事件
         * @param onClickListener onClickListener
         * @return Builder
         */
        public Builder onClickListener(OnClickListener onClickListener) {
            this.mOnClickListener = onClickListener;
            return this;
        }

        /**
         * 可選參數 Tab的雙擊事件
         * @param onDoubleClickListener onDoubleClickListener
         * @return Builder
         */
        public Builder onDoubleClickListener(OnDoubleClickListener onDoubleClickListener) {
            this.mOnDoubleClickListener = onDoubleClickListener;
            return this;
        }

        public FragmentSwitchTool build() {
            final FragmentSwitchTool fragmentSwitchTool = new FragmentSwitchTool();
            fragmentSwitchTool.mFragmentManager = mFragmentManager;
            fragmentSwitchTool.mContainerId = mContainerId;
            fragmentSwitchTool.mShowAnimator = mShowAnimator;
            fragmentSwitchTool.mClickableViews = mClickableViews;
            fragmentSwitchTool.mFragments = mFragments;
            fragmentSwitchTool.mOnClickListener = mOnClickListener;
            fragmentSwitchTool.mOnDoubleOnClickListener = mOnDoubleClickListener;

            if (mOnDoubleClickListener == null) {
                for (View view : mClickableViews) {
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            fragmentSwitchTool.changeTag(view);
                            if (mOnClickListener != null) {
                                mOnClickListener.onClick(view);
                            }
                        }
                    });
                }
            } else {
                for (final View view : mClickableViews) {
                    final GestureDetector gestureDetector = new GestureDetector(mClickableViews.get(0).getContext(), new GestureDetector.SimpleOnGestureListener(){
                        @Override
                        public boolean onSingleTapUp(MotionEvent e) {
                            Log.e("YAO", "onSingleTapUp:" + e.getAction());
                            fragmentSwitchTool.changeTag(view);
                            if (fragmentSwitchTool.mOnClickListener != null) {
                                fragmentSwitchTool.mOnClickListener.onClick(view);
                            }
                            return true;
                        }

                        @Override
                        public boolean onDown(MotionEvent e) {
                            return true;
                        }

                        @Override
                        public boolean onDoubleTap(MotionEvent e) {
                            fragmentSwitchTool.mOnDoubleOnClickListener.onDoubleClick(view);
                            return true;
                        }
                    });

                    view.setOnTouchListener(new View.OnTouchListener() {
                        @Override
                        public boolean onTouch(View v, MotionEvent event) {
                            return gestureDetector.onTouchEvent(event);
                        }
                    });
                }
            }
            return fragmentSwitchTool;
        }
    }
}

使用 Builder 模式傳入參數,4個必要參數包括 FragmentManager、fragment容器的佈局 containerId、Tab裏的可點擊控件 clickableViews 和 Tab對應的Fragment。一些可選的參數比如是否有左右切換的動畫,單擊事件和雙擊事件回調。雙擊事件回調用在 RecyclerView 或者 ScrollView 的回到頂部尤其實用。

 

核心部分在於 changeTag(View) 方法,含有以下邏輯:

如果目前被點擊的 View 是選中的 View,則不做處理。

嘗試通過 FragmentManager.findFragmentByTag 獲取目標 Fragment。

如果沒獲取到,則使用 fragmentTransaction.add 方法把目標 Fragment 添加進去。同時隱藏當前 Fragment。

如果獲取到了,則執行讓被點擊 Fragment 顯示,讓當前 Fragment 隱藏。

 

中間有設置 View 的 selector 狀態,還有可展示也可以不展示的動畫邏輯。

 

 

總結: 

官網方法當前不錯,跟着用就完事了。

雖然老大哥 微信 還保留着能左右滑動切換4個頁面,但是市面上大部分的App都沒有這種滑動切換的功能了,考慮不使用ViewPager了。

如果需要考慮雙擊事件,小紅點提醒,或者更多的自定義效果。使用方法三的這種靈活的佈局,配合 FragmentSwitchTool 使用當然是不錯的。

對MD設計的這種底部欄伸縮很看重,可以使用這兩個第三方庫或者參考其實現。

 

 

Github代碼地址:

https://github.com/yaodiwei/Tab

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章