Jetpack Compose 底部导航栏

注意:您需要有Android Studio Arctic Fox 及更高版本才能在您的项目中使用Jetpack Compose。

添加库

转到您的项目级gradle.build文件,并添加以下扩展名:

buildscript {
    ext {
        compose_version = '1.0.2'
    }

    // ...
}

现在转到应用级gradle.build文件,并添加以下内容:

android {
    // ...

    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        // ...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
}

dependencies {
    // ...

    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation "androidx.navigation:navigation-compose:2.4.0-alpha08"
    implementation "androidx.activity:activity-compose:1.3.1"

    // ...
}

创建顶部栏

打开activity(例如MainActivity.kt),并在类之外添加以下可组合函数以创建topbar

@Composable
fun TopBar() {
    TopAppBar(
        title = { Text(text = stringResource(R.string.app_name), fontSize = 18.sp) },
        backgroundColor = colorResource(id = R.color.colorPrimary),
        contentColor = Color.White
    )
}

@Preview(showBackground = true)
@Composable
fun TopBarPreview() {
    TopBar()
}

创建底部导航栏

在创建底部导航栏之前,我们必须准备Item;创建一个Sealed Class并指定NavigationItem作为名称。并使用参数为每个条形项目创建一个模型:

  • route:必须是unique。用来从底部导航栏导航到视图
  • icon : 栏项的图标
  • title : 栏项的名称
sealed class NavigationItem(var route: String, var icon: Int, var title: String) {
    object Home : NavigationItem("home", R.mipmap.ic_home, "首页")
    object Recommend : NavigationItem("recommend", R.mipmap.ic_recommend, "关注")
    object Books : NavigationItem("books", R.mipmap.ic_book, "书城")
    object Profile : NavigationItem("profile", R.mipmap.ic_profile, "我的")
}

同样,在MainActivity.kt 中,添加以下可组合函数:

@Composable
fun BottomNavigationBar() {
    val items = listOf(
        NavigationItem.Home,
        NavigationItem.Recommend,
        NavigationItem.Books,
        NavigationItem.Profile
    )
    BottomNavigation(
        backgroundColor = colorResource(id = R.color.colorPrimary),
        contentColor = Color.White
    ) {
        items.forEach { item ->
            BottomNavigationItem(
                icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
                label = { Text(text = item.title) },
                selectedContentColor = Color.White,
                unselectedContentColor = Color.White.copy(0.4f),
                alwaysShowLabel = true,
                selected = false,
                onClick = {
                    /* Add code later */
                }
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun BottomNavigationBarPreview() {
    BottomNavigationBar()
}

创建包含顶部栏和底部导航栏的视图

这里我将使用Scaffold,创建一个名为MainScreen()的新组合函数,里面有一个Scaffold布局,并添加我们之前创建的TopBar()和BottomNavigationBar()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

@Composable
fun MainScreen() {
    Scaffold(
        topBar = { TopBar() },
        bottomBar = { BottomNavigationBar() }
    ) {
        /* Add code later */
    }
}

@Preview(showBackground = true)
@Composable
fun MainScreenPreview() {
    MainScreen()
}

将导航栏与视图连接起来(导航)

连接之前,需要先创建Views,我创建了4个名为*Screen的 Kotlin 文件
HomeScreen.kt

@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(colorResource(id = R.color.colorPrimaryDark))
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Home View",
            fontWeight = FontWeight.Bold,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
            fontSize = 25.sp
        )
    }
}

@Preview(showBackground = true)
@Composable
fun HomeScreenPreview() {
    HomeScreen()
}

RecommendScreen.kt

@Composable
fun RecommendScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(colorResource(id = R.color.colorPrimaryDark))
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Recommend View",
            fontWeight = FontWeight.Bold,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
            fontSize = 25.sp
        )
    }
}

@Preview(showBackground = true)
@Composable
fun RecommendScreenPreview() {
    RecommendScreen()
}

BooksScreen.kt

@Composable
fun BooksScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(colorResource(id = R.color.colorPrimaryDark))
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Books View",
            fontWeight = FontWeight.Bold,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
            fontSize = 25.sp
        )
    }
}

@Preview(showBackground = true)
@Composable
fun BooksScreenPreview() {
    BooksScreen()
}

ProfileScreen.kt

@Composable
fun ProfileScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(colorResource(id = R.color.colorPrimaryDark))
            .wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = "Profile View",
            fontWeight = FontWeight.Bold,
            color = Color.White,
            modifier = Modifier.align(Alignment.CenterHorizontally),
            textAlign = TextAlign.Center,
            fontSize = 25.sp
        )
    }
}

@Preview(showBackground = true)
@Composable
fun ProfileScreenPreview() {
    ProfileScreen()
}

接下来在BottomNavigationBar中,添加参数navController,并创建navBackStackEntry和currentRoute。使用currentRoute,我们检查是否必须突出显示栏项目,然后使用navigate()方法导航到视图

@Composable
fun BottomNavigationBar(navController: NavController) {
    val items = listOf(
        NavigationItem.Home,
        NavigationItem.Recommend,
        NavigationItem.Books,
        NavigationItem.Profile
    )
    BottomNavigation(
        backgroundColor = colorResource(id = R.color.colorPrimary),
        contentColor = Color.White
    ) {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { item ->
            BottomNavigationItem(
                icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
                label = { Text(text = item.title) },
                selectedContentColor = Color.White,
                unselectedContentColor = Color.White.copy(0.4f),
                alwaysShowLabel = true,
                selected = currentRoute == item.route,
                onClick = {
                    navController.navigate(item.route) {                        
                        navController.graph.startDestinationRoute?.let { route ->
                            popUpTo(route) {
                                saveState = true
                            }
                        }
                       
                        launchSingleTop = true
                        // 重新选择同一项目时避免同一目的地的多个副本
                        restoreState = true
                    }
                }
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun BottomNavigationBarPreview() {
    // BottomNavigationBar()
}

创建一个新的可组合函数并将其命名为Navigation(),参数为NavHostController类型的参数navController。在这里,我们创建了一个NavHost,我们将Home视图设置为startDestination,我们将每个View的路由设置为可组合的。

@Composable
fun Navigation(navController: NavHostController) {
    NavHost(navController, startDestination = NavigationItem.Home.route) {
        composable(NavigationItem.Home.route) {
            HomeScreen()
        }
        composable(NavigationItem.Recommend.route) {
            RecommendScreen()
        }
        composable(NavigationItem.Books.route) {
            BooksScreen()
        }
        composable(NavigationItem.Profile.route) {
            ProfileScreen()
        }
    }
}

在MainScreen()中创建NavigationController,并将其传递给BottomNavigationBar和Navigation

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    Scaffold(
        topBar = { TopBar() },
        bottomBar = { BottomNavigationBar(navController) }
    ) {
        Navigation(navController)
    }
}

adb shell input keyevent

通过adb shell input keyevent,event_code发送到设备

usage: input [text|keyevent]
  input text <string>
  input keyevent <event_code>

event_code:

0 -->  "KEYCODE_UNKNOWN" 
1 -->  "KEYCODE_MENU" 
2 -->  "KEYCODE_SOFT_RIGHT" 
3 -->  "KEYCODE_HOME" 
4 -->  "KEYCODE_BACK" 
5 -->  "KEYCODE_CALL" 
6 -->  "KEYCODE_ENDCALL" 
7 -->  "KEYCODE_0" 
8 -->  "KEYCODE_1" 
9 -->  "KEYCODE_2" 
10 -->  "KEYCODE_3" 
11 -->  "KEYCODE_4" 
12 -->  "KEYCODE_5" 
13 -->  "KEYCODE_6" 
14 -->  "KEYCODE_7" 
15 -->  "KEYCODE_8" 
16 -->  "KEYCODE_9" 
17 -->  "KEYCODE_STAR" 
18 -->  "KEYCODE_POUND" 
19 -->  "KEYCODE_DPAD_UP" 
20 -->  "KEYCODE_DPAD_DOWN" 
21 -->  "KEYCODE_DPAD_LEFT" 
22 -->  "KEYCODE_DPAD_RIGHT" 
23 -->  "KEYCODE_DPAD_CENTER" 
24 -->  "KEYCODE_VOLUME_UP" 
25 -->  "KEYCODE_VOLUME_DOWN" 
26 -->  "KEYCODE_POWER" 
27 -->  "KEYCODE_CAMERA" 
28 -->  "KEYCODE_CLEAR" 
29 -->  "KEYCODE_A" 
30 -->  "KEYCODE_B" 
31 -->  "KEYCODE_C" 
32 -->  "KEYCODE_D" 
33 -->  "KEYCODE_E" 
34 -->  "KEYCODE_F" 
35 -->  "KEYCODE_G" 
36 -->  "KEYCODE_H" 
37 -->  "KEYCODE_I" 
38 -->  "KEYCODE_J" 
39 -->  "KEYCODE_K" 
40 -->  "KEYCODE_L" 
41 -->  "KEYCODE_M" 
42 -->  "KEYCODE_N" 
43 -->  "KEYCODE_O" 
44 -->  "KEYCODE_P" 
45 -->  "KEYCODE_Q" 
46 -->  "KEYCODE_R" 
47 -->  "KEYCODE_S" 
48 -->  "KEYCODE_T" 
49 -->  "KEYCODE_U" 
50 -->  "KEYCODE_V" 
51 -->  "KEYCODE_W" 
52 -->  "KEYCODE_X" 
53 -->  "KEYCODE_Y" 
54 -->  "KEYCODE_Z" 
55 -->  "KEYCODE_COMMA" 
56 -->  "KEYCODE_PERIOD" 
57 -->  "KEYCODE_ALT_LEFT" 
58 -->  "KEYCODE_ALT_RIGHT" 
59 -->  "KEYCODE_SHIFT_LEFT" 
60 -->  "KEYCODE_SHIFT_RIGHT" 
61 -->  "KEYCODE_TAB" 
62 -->  "KEYCODE_SPACE" 
63 -->  "KEYCODE_SYM" 
64 -->  "KEYCODE_EXPLORER" 
65 -->  "KEYCODE_ENVELOPE" 
66 -->  "KEYCODE_ENTER" 
67 -->  "KEYCODE_DEL" 
68 -->  "KEYCODE_GRAVE" 
69 -->  "KEYCODE_MINUS" 
70 -->  "KEYCODE_EQUALS" 
71 -->  "KEYCODE_LEFT_BRACKET" 
72 -->  "KEYCODE_RIGHT_BRACKET" 
73 -->  "KEYCODE_BACKSLASH" 
74 -->  "KEYCODE_SEMICOLON" 
75 -->  "KEYCODE_APOSTROPHE" 
76 -->  "KEYCODE_SLASH" 
77 -->  "KEYCODE_AT" 
78 -->  "KEYCODE_NUM" 
79 -->  "KEYCODE_HEADSETHOOK" 
80 -->  "KEYCODE_FOCUS" 
81 -->  "KEYCODE_PLUS" 
82 -->  "KEYCODE_MENU" 
83 -->  "KEYCODE_NOTIFICATION" 
84 -->  "KEYCODE_SEARCH" 
85 -->  "TAG_LAST_KEYCODE"

kotlin-android-extensions 已被弃用,如何使用 @Parcelize?

第 1 步。更新到最新的 kotlin 版本 -1.4.20并替换

apply plugin: 'kotlin-android-extensions'

to

apply plugin: 'kotlin-parcelize'

或者

plugins {
    ..
    id 'kotlin-parcelize'
}

第 2 步。从 android {} 中删除以下代码

androidExtensions {
    experimental = true
}

第 3 步。最后,替换旧的 import ->

import kotlinx.android.parcel.Parcelize

to

import kotlinx.parcelize.Parcelize

新插件:https : //plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.parcelize

迁移已弃用的 Kotlin Android Extension 使用ViewBinding

最近出现在了一个这样的警告:

The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.

kotlinx.android.synthetic 不再是推荐的做法,删除支持显式findViewById,用ViewBinding替代

ViewBinding

与 Kotlin Extensions相比,它增加了视图查找和类型安全的编译时检查。但视图绑定后,可让您更轻松地编写与视图交互的代码。启用视图绑定后,它会为该模块中存在的每个 XML 布局文件生成一个绑定类。

如何启用视图ViewBinding?

在build.gradle中添加:

android {
..
    buildFeatures {
       viewBinding true
    }
}

如何使用ViewBinding?

  • 如果为模块启用了视图绑定,则会为模块包含的每个 XML 布局文件生成一个绑定类。
  • 每个绑定类都包含对根视图和所有具有 ID 的视图的引用。
  • 绑定类的名称是通过将 XML 文件的名称转换为 Pascal 大小写并Binding在末尾添加单词来生成的。

在Activity中使用ViewBinding

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

使用binding对象访问View

binding.name.text = "this is ViewBinding"

在Fragment中使用ViewBinding

在Fragment中使用ViewBinding需要注意,因为ViewBinding不能很好地与Fragment一起使用,如果不在OnDestroy清除,则它不会从内存中清除,导致内存泄漏。

private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = FragmentMainBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

像在activity中一样使用对象访问视图

binding.name.text = "this is ViewBinding"

备注

ViewBinding将为模块中的每个XML布局生成一个绑定对象,例如: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">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ViewBinding将生成 ActivityMainBinding.java

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final TextView textView;
...
}

ViewBinding将为每个具有指定id. 在ActivityMainBinding.java中,ViewBinding生成一个公共inflate方法。
它调用bind将绑定属性的位置,并进行一些错误检查。

Git添加多个远程仓库命令

方法一:默认只能从config中的第一个仓库pull代码

git remote set-url --add gitee https://git.gitee.com/xxxx(仓库地址)

// 此时只需要一次push就能同步到多个远程仓库
git push

方法二:可以选择任一仓库pull

// 添加github
git remote add origin https://github.com/xxx(仓库地址)
// 添加gitee
git remote add gitee https://git.gitee.com/xxxx(仓库地址)

// 删除origin仓库
git remote rm origin

// 提交到github
git push origin 
// 提交到gitee
git push gitee

// 从github更新
git pull origin master
// 从gitee更新
git pull gitee master

GIT忽略而不提交文件的3种情形

1:从未提交过的文件可以用.gitignore 也就是添加之后从来没有提交(commit)过的文件,可以使用.gitignore忽略该文件;
2:已经推送(push)过的文件,想从git远程库中删除,并在以后的提交中忽略,但是却还想在本地保留这个文件,执行:

git rm --cached log/config.xml

log/config.xml要从远程库中删除的文件的路径,支持通配符*

3:已经推送(push)过的文件,想在以后的提交时忽略此文件,即使本地已经修改过,而且不删除git远程库中相应文件 执行:

git update-index --assume-unchanged log/config.xml

log/config.xml 是要忽略的文件的路径。如果要忽略一个目录,打开 git bash,cd到 目标目录下,执行: git update-index --assume-unchanged $(git ls-files | tr 'n' ' ') 比如有一个配置文件记录数据库的链接信息,每个人的链接信息肯定不一样,但是又要提供一个标准的模板,用来告知如何填写链接信息,那么就需要在git远程库上有一个标准配置文件,然后每个人根据自己的具体情况,修改一份链接信息自用,而且不会将该配置文件提交到库。