【Android、IOS、Flutter、鸿蒙、ReactNative 】底部导航

173 阅读8分钟

Android Java 底部导航

创建 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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <FrameLayout
        android:id="@+id/home_container"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        app:layout_constraintBottom_toTopOf="@+id/bottom_tab_layout"
        android:layout_height="0dp" />

    <View android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:alpha="0.6"
        app:layout_constraintBottom_toTopOf="@+id/bottom_tab_layout"
        android:background="@android:color/darker_gray"/>

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/bottom_tab_layout"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        app:tabIndicatorHeight="0dp"
        app:tabPaddingTop="4dp"
        app:tabPaddingBottom="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:tabSelectedTextColor="@android:color/black"
        app:tabTextColor="@android:color/darker_gray"/>


</androidx.constraintlayout.widget.ConstraintLayout>

创建 HomeFragment

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

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="Fragment Home"
        android:textSize="28sp"
        android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>
package com.bottom.navigation;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class HomeFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_home, container, false);
    }
}

创建 MineFragment

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

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="Fragment Mine"
        android:textSize="28sp"
        android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>
package com.bottom.navigation;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class MineFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_mine, container, false);
    }
}

创建 MainActivity 实现导航切换 Fragment

package com.bottom.navigation;

import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.tabs.TabLayout;


public class MainActivity extends AppCompatActivity implements TabLayout.OnTabSelectedListener {

    private FrameLayout mFrame;
    private TabLayout mTab;
    private HomeFragment mHomeFragment;
    private MineFragment mMineFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initTab(); //设置TabLayout的标题
        initFragmentReplace();// 设置开启页面时fragment的显示和影藏
        mTab.addOnTabSelectedListener(this);// 设置TabLayout选择监听
    }

    private void initTab() {
        mTab.addTab(mTab.newTab().setText("首页").setIcon(R.drawable.ic_home));
        mTab.addTab(mTab.newTab().setText("上传").setIcon(R.drawable.ic_mine));
        mTab.getTabAt(0).select();
        mTab.getTabAt(0).getIcon().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
        mTab.getTabAt(1).getIcon().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
    }

    private void initFragmentReplace() {

        // 获取到fragment碎片管理器
        FragmentManager manager = getSupportFragmentManager();
        // 开启事务
        FragmentTransaction transaction = manager.beginTransaction();

        // 获取到fragment的对象
        mHomeFragment = new HomeFragment();
        mMineFragment = new MineFragment();

        // 设置要显示的fragment 和 隐藏的fragment
        transaction.add(R.id.home_container, mHomeFragment, "home").show(mHomeFragment);
        transaction.add(R.id.home_container, mMineFragment, "mine").hide(mMineFragment);

        // 提交事务
        transaction.commit();
    }

    private void initView() {
        // 获取控件对象
        mTab = (TabLayout) findViewById(R.id.bottom_tab_layout);
        mFrame = (FrameLayout) findViewById(R.id.home_container);
    }

    @Override
    public void onTabSelected(TabLayout.Tab tab) {

        // 设置选中时fragment的显示和影藏
        switch (tab.getPosition()) {
            case 0:
                FragmentManager fragmentManager = getSupportFragmentManager();
                FragmentTransaction transaction = fragmentManager.beginTransaction();
                transaction.show(mHomeFragment).hide(mMineFragment).commit();
                mTab.getTabAt(0).getIcon().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
                mTab.getTabAt(1).getIcon().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
                break;
            case 1:
                FragmentManager fragmentManager1 = getSupportFragmentManager();
                FragmentTransaction transaction1 = fragmentManager1.beginTransaction();
                transaction1.show(mMineFragment).hide(mHomeFragment).commit();
                mTab.getTabAt(0).getIcon().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
                mTab.getTabAt(1).getIcon().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
                break;
            default:
                break;
        }
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }
}

预览布局

创建 Bottom Navigation Views Activity

使用 Android Studio 创建

image.png

配置 org.jetbrains.kotlin:kotlin-bom 依赖

image.png

创建完成后代码结构

image.png

预览布局

Android Kotlin 底部导航

创建 HomeFragment

package com.kotlin.bottom.navigation

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup



class HomeFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

}

创建 MineFragment

package com.kotlin.bottom.navigation

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment


class MineFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_mine, container, false)
    }
}

创建 MainActivity 实现导航切换 Fragment

package com.kotlin.bottom.navigation

import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener


class MainActivity : AppCompatActivity(), OnTabSelectedListener {
    private var mFrame: FrameLayout? = null
    private var mTab: TabLayout? = null
    private var mHomeFragment: HomeFragment? = null
    private var mMineFragment: MineFragment? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
        initTab() //设置TabLayout的标题
        initFragmentReplace() // 设置开启页面时fragment的显示和影藏
        mTab!!.addOnTabSelectedListener(this) // 设置TabLayout选择监听
    }

    private fun initTab() {
        mTab!!.addTab(mTab!!.newTab().setText("首页").setIcon(R.drawable.ic_home))
        mTab!!.addTab(mTab!!.newTab().setText("上传").setIcon(R.drawable.ic_mine))
        mTab!!.getTabAt(0)!!.select()
        mTab!!.getTabAt(0)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)
        mTab!!.getTabAt(1)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN)
    }

    private fun initFragmentReplace() {

        // 获取到fragment碎片管理器
        val manager = supportFragmentManager
        // 开启事务
        val transaction = manager.beginTransaction()

        // 获取到fragment的对象
        mHomeFragment = HomeFragment()
        mMineFragment = MineFragment()

        // 设置要显示的fragment 和 隐藏的fragment
        transaction.add(R.id.home_container, mHomeFragment!!, "home").show(mHomeFragment!!)
        transaction.add(R.id.home_container, mMineFragment!!, "mine").hide(mMineFragment!!)

        // 提交事务
        transaction.commit()
    }

    private fun initView() {
        // 获取控件对象
        mTab = findViewById<View>(R.id.bottom_tab_layout) as TabLayout
        mFrame = findViewById<View>(R.id.home_container) as FrameLayout
    }

    override fun onTabSelected(tab: TabLayout.Tab) {

        // 设置选中时fragment的显示和影藏
        when (tab.position) {
            0 -> {
                val fragmentManager = supportFragmentManager
                val transaction = fragmentManager.beginTransaction()
                transaction.show(mHomeFragment!!).hide(mMineFragment!!).commit()
                mTab!!.getTabAt(0)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)
                mTab!!.getTabAt(1)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN)
            }

            1 -> {
                val fragmentManager1 =
                    supportFragmentManager
                val transaction1 = fragmentManager1.beginTransaction()
                transaction1.show(mMineFragment!!).hide(mHomeFragment!!).commit()
                mTab!!.getTabAt(0)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN)
                mTab!!.getTabAt(1)!!.icon!!.colorFilter= PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)
            }

            else -> {}
        }
    }

    override fun onTabUnselected(tab: TabLayout.Tab) {}
    override fun onTabReselected(tab: TabLayout.Tab) {}
}

IOS Object-c实现底部导航

创建底部导航并设置为根视图

image.png

修改底部导航图标大小

配置依赖 UIImage-Resize

pod 'UIImage-Resize'

修改图片大小

image.png

自定义底部导航样式

image.png

预览底部导航布局

仿淘宝闲鱼的TabBar

预览布局

IOS Swift实现底部导航

创建底部导航并设置为根视图

image.png

定义修改UIImage大小的函数

image.png

修改底部导航图标大小

image.png

预览底部导航布局

鸿蒙底部导航

添加导航使用的图标

image.png

创建底部导航

@Entry
@Component
struct Index {
  @State currentIndex: number = 0
  private controller: TabsController = new TabsController()

  // 自定义导航页签的样式
  @Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
        .size({ width: 25, height: 25 })
      Text(title)
        .fontColor(this.currentIndex === targetIndex ? '#28bff1' : '#8a8a8a')
    }
    .width('100%')
    .height(50)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.currentIndex = targetIndex
      this.controller.changeIndex(this.currentIndex)
    })
  }

  build() {
    Column() {
      Tabs({
        barPosition: BarPosition.End,
        controller: this.controller
      }) {
        TabContent() {
          Column() {
            // 标题栏
            Text("首页")
              .size({ width: '100%', height: 50 })
              .backgroundColor("#28bff1")
              .fontColor("#ffffff")
              .textAlign(TextAlign.Center)
              .fontSize("18fp")
            // 内容项
            Text("首页").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp")
          }.size({ width: '100%', height: '100%' })
        }.tabBar(this.TabBuilder('首页', 0, $r('app.media.icon_indexed'), $r('app.media.icon_index')))

        TabContent() {
          Column() {
            // 标题栏
            Text("列表")
              .size({ width: '100%', height: 50 })
              .backgroundColor("#28bff1")
              .fontColor("#ffffff")
              .textAlign(TextAlign.Center)
              .fontSize("18fp")
            Text("列表").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp")
          }.size({ width: '100%', height: '100%' })
        }.tabBar(this.TabBuilder('列表', 1, $r('app.media.icon_listed'), $r('app.media.icon_list')))

        TabContent() {
          Column() {
            // 标题栏
            Text("更多")
              .size({ width: '100%', height: 50 })
              .backgroundColor("#28bff1")
              .fontColor("#ffffff")
              .textAlign(TextAlign.Center)
              .fontSize("18fp")
            Text("更多").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp")
          }.size({ width: '100%', height: '100%' })
        }.tabBar(this.TabBuilder('更多', 2, $r('app.media.icon_othered'), $r('app.media.icon_other')))
      }.scrollable(false) // 禁止滑动切换
    }
    .width('100%')
    .height('100%')
  }
}

预览导航

image.png

React Native 底部导航

执行如下命令安装相关依赖:参考

yarn add @react-navigation/native
yarn add @react-navigation/bottom-tabs
yarn add --save react-native-vector-icons

实现底部导航

import {name as appName} from './app.json';

import * as React from 'react';
import { Button, View, AppRegistry,Text } from 'react-native';
import { NavigationContainer,useNavigation } from '@react-navigation/native';

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import Icon from 'react-native-vector-icons/FontAwesome';
import Ionicons from 'react-native-vector-icons/Ionicons'

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen}
            options={{
                tabBarIcon: ({ focused }) => (
                  <Ionicons focused={focused}
                  name="home"
                  color={focused ? 'blue' : 'gray'}
                  size={30}
                  />
                ),
            }}
      />
      <Tab.Screen name="Profile" component={ProfileScreen}
            options={{
             tabBarIcon: ({ focused }) => (
                  <Ionicons
                    focused={focused}
                    name="settings"
                    color={focused ? 'blue' : 'gray'}
                    size={30}
                  />
                ),
             }}
      />
    </Tab.Navigator>
  );
}

function HomeScreen() {
  const navigation = useNavigation();

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Profile"
        onPress={() => navigation.navigate('Profile')}
      />
    </View>
  );
}

function ProfileScreen() {
  const navigation = useNavigation();

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile Screen</Text>
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
    </View>
  );
}

const MyApp =() => {
  return (
    <NavigationContainer>
          <MyTabs />
     </NavigationContainer>
  );
}

AppRegistry.registerComponent(appName, () => MyApp);

预览导航

Flutter 底部导航

实现底部导航

import 'package:flutter/material.dart';
import 'package:flutter_bottom_navigation/navigation_bar_widget.dart';
import 'package:flutter_bottom_navigation/stack_widget.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, this.title}) : super(key: key);

  final String? title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<StackWidgetState> _stackGk = GlobalKey<StackWidgetState>();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title!),
        ),
        bottomNavigationBar: NavigationBarWidget(
          stackValue: (int currentIndex, List<int> tabInt) {
            _stackGk.currentState?.changeStack(currentIndex, tabInt);
          },
        ),
        body: StackWidget(
          key: _stackGk,
        ));
  }
}
import 'package:flutter/material.dart';

class NavigationBarWidget extends StatefulWidget {
  final ValueChanged? stackValue;

  const NavigationBarWidget({Key? key, this.stackValue}) : super(key: key);
  @override
  _BotNavBarWidgetState createState() => _BotNavBarWidgetState();
}

class _BotNavBarWidgetState extends State<NavigationBarWidget> {
  int _currentIndex = 0;
  List<int> tabInt = [0];
  final List<BottomNavigationBarItem> _botNavBarItems =
      <BottomNavigationBarItem>[
    const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Page1'),
    const BottomNavigationBarItem(icon: Icon(Icons.payment), label: 'Page2'),
    const BottomNavigationBarItem(icon: Icon(Icons.monetization_on), label: 'Page3'),
    const BottomNavigationBarItem(icon: Icon(Icons.accessibility), label: 'Page4')
  ];
  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      type: BottomNavigationBarType.fixed,
      onTap: onTabTapped,
      currentIndex: _currentIndex,
      backgroundColor: Colors.lightBlueAccent,
      //selectedItemColor: Colors.black,
      unselectedItemColor: Colors.red,
      elevation: 0.0,
      fixedColor: Colors.black,
      showUnselectedLabels: true,
      items: _botNavBarItems,
    );
  }

  onTabTapped(int index) {
    setState(() {});
    _currentIndex = index;
    if (!tabInt.contains(index)) {
      tabInt.add(index);
    }
    if (widget.stackValue != null) {
      widget.stackValue!(_currentIndex,tabInt);
    }
  }
}

typedef ValueChanged = void Function(
  int currentIndex,
  List<int> tabInt,
);
import 'package:flutter/widgets.dart';
import 'package:flutter_bottom_navigation/page_four.dart';
import 'package:flutter_bottom_navigation/page_one.dart';
import 'package:flutter_bottom_navigation/page_three.dart';
import 'package:flutter_bottom_navigation/page_two.dart';

class StackWidget extends StatefulWidget {
  StackWidget({Key? key, this.currentIndex = 0, this.tabInt}) : super(key: key);

  final int currentIndex;
  final List<int>? tabInt;

  @override
  StackWidgetState createState() => StackWidgetState();
}

class StackWidgetState extends State<StackWidget> {
  final List<StatefulWidget> _children = [
    const PageOne(
      title: "Page1",
    ),
    const PageTwo(
      title: "Page2",
    ),
    const PageThree(
      title: "Page3",
    ),
    const PageFour(
      title: "Page4",
    ),
  ];

  int _currentIndex = 0;
  List<int>? _tabInt;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _tabInt = widget.tabInt ?? [0];
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: MediaQuery.of(context).size.width,
      child: _stack(),
    );
  }

  void changeStack(int currentIndex, List<int> tabInt) {
    setState(() {});
    _currentIndex = currentIndex;
    _tabInt = tabInt;
  }

  Stack _stack() {
    return Stack(
      children: <Widget>[
        _child(0),
        _child(1),
        _child(2),
        _child(3),
      ],
    );
  }

  //Page的显示和隐藏
  _child(int _index) {
    return Offstage(
      offstage: !(_currentIndex == _index),
      child: _tabInt!.contains(_index) ? _children[_index] : Container(),
    );
  }
}

预览布局

Android Compose 底部导航

实现底部导航

添加依赖

implementation("androidx.navigation:navigation-compose:2.7.5")
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.ShoppingCart
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController

sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
    data object Home : Screen("home", "Home", Icons.Outlined.Home)
    data object Favorite : Screen("favorite", "Favorite", Icons.Outlined.FavoriteBorder)
    data object Profile : Screen("profile", "Profile", Icons.Outlined.Person)
    data object Cart : Screen("cart", "Cart", Icons.Outlined.ShoppingCart)
}

val items = listOf(
    Screen.Home,
    Screen.Favorite,
    Screen.Profile,
    Screen.Cart
)

@Composable
fun NavBottomBar() {
    val navController = rememberNavController()
    Column(modifier = Modifier.fillMaxSize()) {
        NavHost(
            navController,
            startDestination = Screen.Home.route,
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
        ) {
            composable(Screen.Home.route) { HomeScreen(navController) }
            composable(Screen.Favorite.route) { FavoriteScreen(navController) }
            composable(Screen.Profile.route) { ProfileScreen(navController) }
            composable(Screen.Cart.route) { CartScreen(navController) }
        }
        BottomBar(navController, items, modifier = Modifier.fillMaxWidth())
    }
}

@Composable
fun BottomBar(
    navController: NavHostController,
    items: List<Screen>,       //导航路线
    modifier: Modifier = Modifier
) {
    //获取当前的 NavBackStackEntry 来访问当前的 NavDestination
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination
    Row(
        modifier = modifier.background(color = Color.White),
        horizontalArrangement = Arrangement.SpaceAround,
        verticalAlignment = Alignment.CenterVertically
    ) {
        items.forEachIndexed { index, screen ->
            BottomBarItem(
                item = screen,
                //与层次结构进行比较来确定是否被选中
                isSelected = currentDestination?.hierarchy?.any { it.route == screen.route },
                onItemClicked = {
                    //加这个可解决问题:按back键会返回2次,第一次先返回home, 第二次才会退出
                    navController.popBackStack()
                    //点击item时,清空栈内 popUpTo ID到栈顶之间的所有节点,避免站内节点持续增加
                    navController.navigate(screen.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            //跳转时保存页面状态
                            saveState = true
                        }
                        //栈顶复用,避免重复点击同一个导航按钮,回退栈中多次创建实例
                        launchSingleTop = true
                        //回退时恢复页面状态
                        restoreState = true
                        //通过使用 saveState 和 restoreState 标志,当在底部导航项之间切换时,
                        //系统会正确保存并恢复该项的状态和返回堆栈。
                    }
                }
            )
        }
    }
}

@Composable
private fun BottomBarItem(
    item: Screen,
    isSelected: Boolean?,   //是否选中
    onItemClicked: () -> Unit,  //按钮点击监听
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.clickableWithoutInteraction { onItemClicked.invoke() },
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Icon(
            modifier = Modifier.size(30.dp),
            imageVector = item.icon,
            contentDescription = item.title,
            tint = if (isSelected == true) Color.Blue else Color.Gray,
        )
        Text(
            text = item.title,
            color = if (isSelected == true) Color.Blue else Color.Gray,
            fontSize = 10.sp,
        )
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            text = "HomeScreen"
        )
    }
}

@Composable
fun FavoriteScreen(navController: NavController) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            text = "FavoriteScreen"
        )
    }
}

@Composable
fun ProfileScreen(navController: NavController) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            text = "ProfileScreen"
        )
    }
}

@Composable
fun CartScreen(navController: NavController) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(
            text = "CartScreen"
        )
    }
}

/**
 * clickable禁用点击涟漪效应
 */
inline fun Modifier.clickableWithoutInteraction(crossinline onClick: () -> Unit): Modifier =
    this.composed {
        clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            onClick()
        }
    }

预览布局

案例

image.png