本文(争取做到)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设计的这种底部栏伸缩很看重,可以使用这两个第三方库或者参考其实现。