8月11日第 12 章 最佳的UI体验,Material Design实战

新建一个 MaterialTest 项目,然后我们马上开始

1.Toolbar

Toolbar是我们接触的第一个控件,是由AndroidX库提供的。

我们曾经在 3.4.1 小节为了使用一个自定义的标题栏,而把系统原生的 ActionBar 隐藏掉。

每个活动最顶部的那个标题栏就是 ActionBar,不过 ActionBar 由于其设计的原因,被限定只能位于活动的顶部,从而不能实现一些 Material Design 的效果,因此官方现在已经不建议使用 ActionBar 。

我直接讲解现在更加推荐使用的 Toolbar。

Toolbar 的强大之处在于,它不仅继承了 ActionBar 的所有功能,而且灵活性很高,可以配合其他控件来完成一些 Material Design 的效果。

首先,任何一个新建的项目,默认都是会显示 ActionBar 的。那么这个 ActionBar 到底是从哪里来的呢?其实这是根据项目中指定的主题来显示的,打开 AndroidManifest.xml 文件看一下

<application 
 android:allowBackup="true" 
 android:icon="@mipmap/ic_launcher" 
 android:label="@string/app_name" 
 android:roundIcon="@mipmap/ic_launcher_round" 
 android:supportsRtl="true" 
 android:theme="@style/AppTheme"> 
 //android:theme="@style/Theme.MaterialTest"我并没有上文的标题我的这里是这个东西
 ... 
</application> 

这里使用android:theme属性指定了一个AppTheme的主题。打开res/values/styles.xml(themes.xml)文件

这是我的文件内容不是书中的

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Base.Theme.MaterialTest" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- Customize your light theme here. -->
        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
    </style>

    <style name="Theme.MaterialTest" parent="Base.Theme.MaterialTest" />
</resources>

这里定义了一个叫Base.Theme.MaterialTest的主题,然后指定它的parent主题是 Theme.AppCompat.Light.DarkActionBar。这个DarkActionBar是一个深色的ActionBar 主题,我们之前所有的项目中自带的ActionBar就是因为指定了这个主题才出现的。

(我的是Theme.Material3.DayNight.NoActionBar,这个NoActionBar看起来就是没有的意思)

现在我们准备使用Toolbar来替代ActionBar,因此需要指定一个不带ActionBar的主题,通常有Theme.AppCompat.NoActionBar 和Theme.AppCompat.Light.NoActionBar这两种 主题可选。其中Theme.AppCompat.NoActionBar表示深色主题,它会将界面的主体颜色设 成深色,陪衬颜色设成浅色。而Theme.AppCompat.Light.NoActionBar表示浅色主题,它会将界面的主体颜色设成浅色,陪衬颜色设成深色。

我的看起来本身就没有,这里我就不改了

接下来看一看如何使用Toolbar来替代 ActionBar。修改activity_main.xml中的代码

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
    android:title="@string/app_name"
    />


</FrameLayout>

看一下第3行,这里使用xmlns:app指定了一个新的命名空间。

正是由于每个布局文件都会使 用xmlns:android来指定一个命名空间,我们才能一直使用android:id、android: layout_width等写法。这里指定了xmlns:app,也就是说现在可以使用app:attribute这 样的写法了。

这是由于许多Material属性是在新系统中新增的,老系统中并不存在,那么为了能够兼容老系统,我们就不能使用 android:attribute这样的写法了,而是应该使用app:attribute。

接下来定义了一个Toolbar控件,这个控件是由appcompat库提供的。这里我们给Toolbar指定了一个id,将它的宽度设置为match_parent,高度设置为actionBar的高度,背景色设置为 colorPrimary。

由于我们刚才在styles.xml中将程序的主题指定成了浅色主题

(虽然我并没有,但是它能够找到这些主题的属性,说明在某个地方定义过了),

因此Toolbar现在也是浅色主题,那么Toolbar上面的各种元素就会自动使用深色系,从而和主体颜色区别开。但是之前使用ActionBar时文字都是白色的,现在变成黑色的会很难看。那么为了能让Toolbar单独使用深色主题,这里我们使用了android:theme 属性,将Toolbar的主题指定成了ThemeOverlay.AppCompat.Dark.ActionBar。但是这样指定之后又会出现新的问题,如果Toolbar中有菜单按钮,那么弹出的菜单项也会变成深色主题,这样就再次变得十分难看了,于是这里又使用了 app:popupTheme属性,单独将弹出的菜单项指定成了浅色主题。

修改MainActivity

Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar);

(如果set方法报错请检查导入的包应是import androidx.appcompat.widget.Toolbar;)

再学习一些Toolbar比较常用的功能

书中修改显示文字的代码似乎没有了,不过title的改动一样

修改标题栏上显示的文字内容

只有一个标题的Toolbar太单调了,我们可以添加一些action按钮来让Toolbar 更加丰富。

这里提前准备几张图片作为按钮的图标,将它们放在了drawable目录下。现在右击res目录→New→Directory,创建一个menu文件 夹。然后右击menu文件夹→New→Menu resource file,创建一个toolbar.xml文件,并编写如下代码

<menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"> 
     <item 
     android:id="@+id/backup" 
     android:icon="@drawable/ic_backup" 
     android:title="Backup" 
     app:showAsAction="always" /> 
     <item 
         android:id="@+id/delete" 
         android:icon="@drawable/ic_delete" 
         android:title="Delete" 
         app:showAsAction="ifRoom" /> 
     <item 
         android:id="@+id/settings" 
         android:icon="@drawable/ic_settings" 
         android:title="Settings" 
         app:showAsAction="never" /> 
</menu> 

,我们通过标签来定义action按钮,android:id用于指定按钮的id, android:icon用于指定按钮的图标,android:title用于指定按钮的文字。

接着使用app:showAsAction来指定按钮的显示位置,这里之所以再次使用了app命名空间,同样是为了能够兼容低版本的系统。

showAsAction主要有以下几种值可选:

always表示永远 显示在Toolbar中,如果屏幕空间不够则不显示;

ifRoom表示屏幕空间足够的情况下显示在 Toolbar中,不够的话就显示在菜单当中;

never则表示永远显示在菜单当中。

Toolbar 中的action按钮只会显示图标,菜单中的action按钮只会显示文字。

修改MainActivity中的代码

public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.toolbar, menu);
    return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.backup){
        Toast.makeText(this, "You clicked Backup", Toast.LENGTH_SHORT).show();
    } else if (item.getItemId() == R.id.delete) {
        Toast.makeText(this, "You clicked Delete", Toast.LENGTH_SHORT).show();
    } else if (item.getItemId() == R.id.settings) {
        Toast.makeText(this, "You clicked Settings", Toast.LENGTH_SHORT).show();
    }
    return true;
}

我们在onCreateOptionsMenu()方法中加载了toolbar.xml这个菜单文件,然后 在onOptionsItemSelected()方法中处理各个按钮的点击事件。

Toolbar上现在显示了两个action按钮,这是因为Backup按钮指定的显示位置是 always,Delete按钮指定的显示位置是ifRoom,而现在屏幕空间很充足,因此两个按钮都会 显示在Toolbar中。另外一个Settings按钮由于指定的显示位置是never,所以不会显示在Toolbar中,点击一下最右边的菜单按钮来展开菜单项,你就能找到Settings按钮了。

2.滑动菜单

1.DrawerLayout

滑动菜单,就是将一些菜单选项隐藏起来,而不是放置在主屏幕上,然后可以通过滑动 的方式将菜单显示出来。这种方式既节省了屏幕空间,又实现了非常好的动画效果,是 Material Design中推荐的做法。

先它是一个布局,在布局中允许放入两个直接 子控件:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="#00ffff"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </FrameLayout>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#FFF"
        android:text="This is menu"
        android:textSize="30sp" />


</androidx.drawerlayout.widget.DrawerLayout>

这里最外层的控件使用了DrawerLayout。DrawerLayout中放置了两个直接子控件:第一个子控件是FrameLayout,用于作为主屏幕中显示的内容,当然里面还有我们刚刚定义的Toolbar;第二个子控件是一个TextView,用于作为滑动菜单中显示的内容,其实使用什 么都可以,DrawerLayout并没有限制只能使用固定的控件。

第二个子控件有一点需要注意,layout_gravity这个属性是必须指定的,因为我们需要告诉DrawerLayout滑动菜单是在屏幕的左边还是右边,指定left表示滑动菜单在左边,指 定right表示滑动菜单在右边。这里我指定了start,表示会根据系统语言进行判断,如果系统语 言是从左往右的,比如英语、汉语,滑动菜单就在左边,如果系统语言是从右往左的,比如阿拉伯语,滑动菜单就在右边。

在屏幕的左侧边缘向右拖动(全面屏手势记得关掉)

(虽然成功了但是不知道为什么内边距的设置没有生效导致被导航栏挡住)

Material Design建议在Toolbar的最左边加入一个导航按钮,点击按钮会将滑动菜单的内容展示出来

修改MainActivity中的代码

private DrawerLayout mDrawerLayout;

    mDrawerLayout = findViewById(R.id.main);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
    actionBar.setDisplayHomeAsUpEnabled(true);
    actionBar.setHomeAsUpIndicator(R.drawable.ic_launcher_foreground);
}

else if (item.getItemId() == android.R.id.home) {
    mDrawerLayout.openDrawer(GravityCompat.START);
}

调用getSupportActionBar()方法得到了ActionBar的实例,虽然这个ActionBar的具体实现是由Toolbar来完成的。

接着在ActionBar不为空的情况下调用setDisplayHomeAsUpEnabled()方法让导航按钮显示出来,调用 setHomeAsUpIndicator()方法来设置一个导航按钮图标。

Toolbar最左侧的这个按钮就叫作Home按钮,它默认的图标是一个返回的箭头,含义是返回上一个Activity。这里我们将它默认的样式和作用都进行了修改。

在onOptionsItemSelected()方法中对Home按钮的点击事件进行处理,Home按钮的id永远都是android.R.id.home。然后调用DrawerLayout的openDrawer()方法将滑 动菜单展示出来,注意,openDrawer()方法要求传入一个Gravity参数,为了保证这里的行为和XML中定义的一致,我们传入了GravityCompat.START。

2.NavigationView

目前我们已经成功实现了滑动菜单功能,其中滑动功能已经做得非常好了,但是菜单却还很丑,毕竟菜单页面仅仅使用了一个TextView,非常单调。

你可以在滑动菜单页面定制任意的布局,不过Google给我们提供了一种更好的方法:使用NavigationView。

NavigationView是Material库中提供的一个控件,它不仅是严格按照Material Design的要求来设计的,而且可以将滑动菜单页面的实现变得非常简单。

这个控件是Material库中提供的,那么我们就需要将这个库引入项目中才行。打开 app/build.gradle文件,在dependencies闭包中添加如下内容

implementation("com.google.android.material:material:1.1.0")
implementation ("de.hdodenhof:circleimageview:3.0.1")

这里添加了两行依赖关系:第一行就是Material库,第二行是一个开源项目 CircleImageView,它可以用来轻松实现图片圆形化的功能

,当你引入了Material库之后,还需要将res/values/styles.xml文件中 AppTheme的parent主题改成Theme.MaterialComponents.Light.NoActionBar,否则在 使用接下来的一些控件时可能会遇到崩溃问题。

在开始使用NavigationView之前,我们还需要准备好两个东西:menu和headerLayout。 menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在 NavigationView中显示头部布局的。

先来准备menu。这里我事先找了几张图片作为按钮的图标,并将它们放在了drawablexxhdpi目录下。右击menu文件夹→New→Menu resource file,创建一个nav_menu.xml文件,并编写如下代码

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/navCall"
            android:icon="@drawable/ic_backup"
            android:title="Call" />
        <item
            android:id="@+id/navFriends"
            android:icon="@drawable/ic_delete"
            android:title="Friends" />
        <item
            android:id="@+id/navLocation"
            android:icon="@drawable/ic_settings"
            android:title="Location" />
        <item
            android:id="@+id/navMail"
            android:icon="@drawable/ic_launcher_background"
            android:title="Mail" />
        <item
            android:id="@+id/navTask"
            android:icon="@drawable/nav_task"
            android:title="Tasks" />
    </group>
</menu>

我们首先在中嵌套了一个标签,然后将group的checkableBehavior属性指定为single。

group表示一个组,checkableBehavior指定为single表示组中的所有菜单项只能单选。

这里一共定义了5个item,分别使用android:id属性指定菜单项的id,android:icon属性指定菜单项的图标,android:title属性指定菜单项显示的文字。

接下来应该准备headerLayout了,这是一个可以随意定制的布局,不过我并不想将它做得太复杂。这里简单起见,我们就在headerLayout中放置头像、用户名、邮箱地址这3项内容.

右击layout文件夹→New→Layout resource file,创建一个nav_header.xml文件。 修改其中的代码

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="180dp"
    android:padding="10dp"
    android:background="#00ffff">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/iconImage"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="#ff00ff"
        android:layout_centerInParent="true" />
    <TextView
        android:id="@+id/mailText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="tonygreendev@gmail.com"
        android:textColor="#FFF"
        android:textSize="14sp" />
    <TextView
        android:id="@+id/userText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/mailText"
        android:text="Tony Green"
        android:textColor="#FFF"
        android:textSize="14sp" />
</RelativeLayout>

布局文件的最外层是一个RelativeLayout,我们将它的宽度设为match_parent, 高度设为180 dp,这是一个NavigationView比较适合的高度,然后指定它的背景色为 colorPrimary。

在RelativeLayout中我们放置了3个控件,CircleImageView是一个用于将图片圆形化的控件,它的用法非常简单,基本和ImageView是完全一样的,这里给它指定了一张图片作为头像,然后设置为居中显示。另外两个TextView分别用于显示用户名和邮箱地址,它们都用到了 一些RelativeLayout的定位属性

修改 activity_main.xml中的代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    >


    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#00ffff"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </FrameLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"/>


</androidx.drawerlayout.widget.DrawerLayout>

我们将之前的TextView换成了NavigationView,这样滑动菜单中显示的内容也就 变成NavigationView了。这里又通过app:menu和app:headerLayout属性将我们刚才准备 好的menu和headerLayout设置了进去,这样NavigationView就定义完成了。

修改 MainActivity中的代码

NavigationView navView = findViewById(R.id.navView);
navView.setCheckedItem(R.id.navCall);
navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        mDrawerLayout.closeDrawers();
        return true;
    }
});

代码还是比较简单的,这里我们首先调用了NavigationView的setCheckedItem()方法将 Call菜单项设置为默认选中。接着调用了setNavigationItemSelectedListener()方法 来设置一个菜单项选中事件的监听器,当用户点击了任意菜单项时,就会回调到传入的 Lambda表达式当中,我们可以在这里编写具体的逻辑处理。这里调用了DrawerLayout的 closeDrawers()方法将滑动菜单关闭,并返回true表示此事件已被处理。

3.悬浮按钮和可交互提示

1.FloatingActionButton

FloatingActionButton是Material库中提供的一个控件,这个控件可以帮助我们比较轻松地实现悬浮按钮的效果。

修改activity_main.xml中的代码

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffff"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@drawable/ic_backup" />
</FrameLayout>

我们在主屏幕布局中加入了一个FloatingActionButton。这个控件的用法并没有什么特别的地方,layout_width和layout_height属性都指定成wrap_content, layout_gravity属性指定将这个控件放置于屏幕的右下角。其中end的工作原理和之前的 start是一样的,即如果系统语言是从左往右的,那么end就表示在右边,如果系统语言是从右往左的,那么end就表示在左边。

通过layout_margin属性给控件的四周留点边距,紧贴着屏幕边缘肯定是不好看的,最后通过src属性给FloatingActionButton设置了一个图标。

一个漂亮的悬浮按钮就在屏幕的右下方出现了

现这个悬浮按钮的下面还有一点阴影,因为 FloatingActionButton是悬浮在当前界面上的,既然是悬浮,那么理所应当会有投影

我们还可以指定FloatingActionButton的悬浮高度

<com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/ic_backup"
            app:elevation="8dp" />

这里使用app:elevation属性给FloatingActionButton指定一个高度值。高度值越大,投影 范围也越大,但是投影效果越淡;高度值越小,投影范围也越小,但是投影效果越浓。当然这 些效果的差异其实并不怎么明显,我个人感觉使用默认的FloatingActionButton效果就已经足够了。

它和普通 的Button其实没什么两样,都是调用setOnClickListener()方法来设置按钮的点击事件, 我们来弹出一个Toast

FloatingActionButton button = findViewById(R.id.fab);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Toast.makeText(MainActivity.this,"FAB被点击",Toast.LENGTH_SHORT).show();
    }
});

2.Snackbar

Snackbar并不是Toast的替代品,它们有着不同的应用场景。Toast的作用是告诉 用户现在发生了什么事情,但用户只能被动接收这个事情,因为没有什么办法能让用户进行选 择。而Snackbar则在这方面进行了扩展,它允许在提示中加入一个可交互按钮,当用户点击按钮的时候,可以执行一些额外的逻辑操作。

Snackbar的用法也非常简单,它和Toast是基本相似的,只不过可以额外增加一个按钮的点击事件。修改MainActivity中的代码

FloatingActionButton button = findViewById(R.id.fab);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Snackbar.make(view,"删除",Snackbar.LENGTH_SHORT).setAction("放弃", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this,"已恢复",Toast.LENGTH_SHORT).show();    
            }
        }).show();
    }
});

调用了Snackbar的make()方法来创建一个Snackbar对象。

make()方法的第一个参数需要传入一个View,只要是当前界面布局的任意一个View都可以,Snackbar会使用这个View自动查找最外层的布局,用于展示提示信息;

第二个参数就是Snackbar中显示的内容;

第三个参数是Snackbar显示的时长,这些和Toast都是类似的。

接着这里又调用了一个setAction()方法来设置一个动作,从而让Snackbar不仅仅是一个提示,而是可以和用户进行交互的。

我们在动作按钮的点击事件里面弹出一个Toast提 示。最后调用show()方法让Snackbar显示出来

不管是出现还是消失,Snackbar都是带有动画效果的,因此视觉体验也会比较好

3.CoordinatorLayout

CoordinatorLayout可以说是一个加强版的FrameLayout,由AndroidX库提供。它在普通情 况下的作用和FrameLayout基本一致,但是它拥有一些额外的Material能力。

CoordinatorLayout可以监听其所有子控件的各种事件,并自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的Snackbar提示将悬浮按钮遮挡住了,而如果我们能让 CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的 FloatingActionButton向上偏移,从而确保不会被Snackbar遮挡。

CoordinatorLayout的使用也非常简单,我们只需要将原来的FrameLayout替换一下就可以了。修改activity_main.xml中的代码

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffff"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@drawable/ic_backup"
        app:elevation="8dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

由于CoordinatorLayout本身就是一个加强版的FrameLayout,因此这种替换不会有任何的副作用。

悬浮按钮自动向上偏移了Snackbar的同等高度,从而确保不会被遮挡。当 Snackbar消失的时候,悬浮按钮会自动向下偏移回到原来的位置

刚才说的是CoordinatorLayout可以监听其所有子控件的各种 事件,但Snackbar并不是CoordinatorLayout的子控件

我们在Snackbar的make()方法中传入的第一个参数是用来指定Snackbar是基于哪个View触发的,刚才我们传入的是FloatingActionButton本身,而FloatingActionButton是CoordinatorLayout中的子控件,因此这个事件就能被监听到了

4.卡片式布局

卡片式 布局是Materials Design中提出的一个新概念,它可以让页面中的元素看起来就像在卡片一样,并且还能拥有圆角和投影

1.MaterialCardView

MaterialCardView是用于实现卡片式布局效果的重要控件,由Material库提供。实际上, MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有 立体的感觉。

我们需要用到RecyclerView,因此必须在app/build.gradle文件中声明库的依赖

implementation("androidx.recyclerview:recyclerview:1.0.0")
implementation("com.github.bumptech.glide:glide:4.9.0")

上述声明的第二行是添加了Glide库的依赖。Glide是一个超级强大的开源图片加载库,它不仅 可以用于加载本地图片,还可以加载网络图片、GIF图片甚至是本地视频。最重要的是,Glide 的用法非常简单,只需几行代码就能轻松实现复杂的图片加载功能,因此这里我们准备用它来 加载水果图片。Glide的项目主页地址是:https://github.com/bumptech/glide。

修改activity_main.xml中的代码

<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#00ffff"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

这里我们在CoordinatorLayout中添加了一个RecyclerView,给它指定一个id,然后将宽度和 高度都设置为match_parent,这样RecyclerView就占满了整个布局的空间。

着定义一个实体类Fruit

package com.example.materialtest;

public class Fruit {
    private String name;
    private int imageId;

    public int getImageId() {
        return imageId;
    }
    
    public String getName() {
        return name;
    }
    
    public Fruit(int imageId, String name) {
        this.imageId = imageId;
        this.name = name;
    }
    
}

Fruit类中只有两个字段:name表示水果的名字,imageId表示水果对应图片的资源id

然后需要为RecyclerView的子项指定一个我们自定义的布局,在layout目录下新建 fruit_item.xml,代码如下所示

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    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="wrap_content"
    android:layout_margin="5dp"
    app:cardCornerRadius="4dp">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/fruitImage"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="centerCrop" />
        <TextView
            android:id="@+id/fruitName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="5dp"
            android:textSize="16sp" />
    </LinearLayout>
</com.google.android.material.card.MaterialCardView> 

这里使用了MaterialCardView来作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中的。由于MaterialCardView是一个FrameLayout,因此它没有什么方便的定位方式,这里我们只好在MaterialCardView中再嵌套一个LinearLayout,然后在 LinearLayout中放置具体的内容。

内容定义了一个ImageView用于显示水果的图片,又定义了一 个TextView用于显示水果的名称,并让TextView在水平方向上居中显示。

ImageView中使用了一个scaleType属性,这个属性可以指定图片的缩放模式。这里使用 了centerCrop模式,它可以让图片保持原有比例填充满ImageView,并将超出屏幕的部分裁剪掉。

接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自 RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder

 
package com.example.materialtest;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;


import com.bumptech.glide.Glide;

import java.time.Instant;
import java.util.List;

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {

    private Context mContext;
    private List<Fruit> mFruitList;
    static class ViewHolder extends RecyclerView.ViewHolder {
        CardView cardView;
        ImageView fruitImage;
        TextView fruitName;
        public ViewHolder(View view) {
            super(view);
            cardView = (CardView) view;
            fruitImage = view.findViewById(R.id.fruitImage);
            fruitName = view.findViewById(R.id.fruitName);
        }
    }
    public FruitAdapter(List<Fruit> fruitList) {
        mFruitList = fruitList;
    }
    @NonNull
    @Override
    public FruitAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (mContext == null) {
            mContext = parent.getContext();
        }
        View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,
                parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull FruitAdapter.ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitName.setText(fruit.getName());
        Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage);
    }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}

在onBindViewHolder()方法中我们使用了Glide来加载水果图片

调用Glide.with()方法并传入一个Context、Activity或Fragment参数, 然后调用load()方法加载图片,可以是一个URL地址,也可以是一个本地路径,或者是一个资 源id,最后调用into()方法将图片设置到具体某一个ImageView中就可以了

这样我们将RecyclerView的适配器也准备好了,最后修改MainActivity中的代码

private Fruit[] fruits = {new Fruit("Apple", R.drawable.ic_backup),
        new Fruit("Banana", R.drawable.ic_delete),
        new Fruit("Watermelon", R.drawable.ic_settings)};
private List<Fruit> fruitList = new ArrayList<>();
private FruitAdapter adapter;





    initFruits();
    RecyclerView recyclerView = findViewById(R.id.recyclerView);
    GridLayoutManager layoutManager = new GridLayoutManager(this,2);
    recyclerView.setLayoutManager(layoutManager);
    adapter = new FruitAdapter(fruitList);
    recyclerView.setAdapter(adapter);
    
    
    
private void initFruits() {
    fruitList.clear();
    for (int i = 0; i < 50; i++) {
        Random random = new Random();
        int index = random.nextInt(fruits.length);
        fruitList.add(fruits[index]);
    }
}

在MainActivity中,我们首先定义了一个水果集合,集合里面存放了很多个Fruit的实例,每 个实例都代表一种水果。然后在initFruits()方法中,先是清空了一下fruitList中的数 据,接着使用一个随机函数,从刚才定义的Fruit数组中随机挑选一个水果放入fruitList当 中,这样每次打开程序看到的水果数据都会是不同的。为了让界面上的数据多一些,这里使用 了一个循环,随机挑选 50 个水果

目前Glide下载3.7.0版本可用,但运行会报错,目前没有找到解决办法暂时搁置

因此我将Glide这一行代码进行了修改

holder.fruitImage.setImageResource(fruit.getImageId());

2.AppBarLayout

RecyclerView和Toolbar都是放置在CoordinatorLayout中的,而前面已经说过, CoordinatorLayout就是一个加强版的FrameLayout,那么FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而产生了遮挡的现象

使用偏移是唯一的解决办 法,即让RecyclerView向下偏移一个Toolbar的高度,从而保证不会遮挡到Toolbar。不过我们 使用的并不是普通的FrameLayout,而是CoordinatorLayout,因此自然会有一些更加巧妙的解决办法。

使用Material库中提供的另外一个工具——AppBarLayout。AppBarLayout实际 上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些 Material Design的设计理念。

第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局行为。

修改 activity_main.xml中的代码

<com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffff"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

首先定义了一个 AppBarLayout,并将 Toolbar 放置在了 AppBarLayout 里面,然后在 RecyclerView 中使用 app:layout_behavior 属性 指定了一个布局行为。其中 appbar_scrolling_view_behavior 这个字符串也是由 Design Support 库提供的。

当AppBarLayout接收到滚动事件的时候,它内部的子控件是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就能实现。Toolbar的代码添加

app:layout_scrollFlags="scroll|enterAlways|snap"

在Toolbar中添加了一个app:layout_scrollFlags属性,并将这个属性的值指定成了 scroll|enterAlways|snap。其中,scroll表示当RecyclerView向上滚动的时候, Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时 候,Toolbar会跟着一起向下滚动并重新显示;snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。

5.下拉刷新

SwipeRefreshLayout是用于实现下拉刷新功能的核心类,我们把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。

在 MaterialTest项目中,应该支持下拉刷新功能的控件是RecyclerView

使用SwipeRefreshLayout之前首先需要在app/build.gradle文件中添加如下依赖

implementation ("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")

修改 activity_main.xml中的代码

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipeRefresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

我们在 RecyclerView 的外面又嵌套了一层 SwipeRefreshLayout,这样 RecyclerView 就自动拥有下拉刷新功能了。

由于 RecyclerView 现在变成了 SwipeRefreshLayout 的子控件,因此之前使用 app:layout_behavior 声明的布局行为现在也要移到 SwipeRefreshLayout 中

不过这还没有结束,虽然RecyclerView已经支持下拉刷新功能了,但是我们还要在代码中处理 具体的刷新逻辑才行。修改MainActivity中的代码

private SwipeRefreshLayout swipeRefresh;

swipeRefresh = findViewById(R.id.swipeRefresh);
    swipeRefresh.setColorSchemeResources(R.color.black);
    swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            refreshFruits();
        }
    });
}

private void refreshFruits() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    initFruits();
                    adapter.notifyDataSetChanged();
                    swipeRefresh.setRefreshing(false);
                }
            });
        }
    }).start();
}

首先调用SwipeRefreshLayout的 setColorSchemeResources()方法来设置下拉刷新进度条的颜色,接着调用setOnRefreshListener()方法来设置一 个下拉刷新的监听器,当用户进行了下拉刷新操作时,就会回调到Lambda表达式当中,然后我们在这里去处理具体的刷新逻辑就可以了。

refreshFruits()方法中先是开启了一个线程,然后将线程沉睡两秒 钟。之所以这么做,是因为本地刷新操作速度非常快,如果不将线程沉睡的话,刷新立刻就结 束了,从而看不到刷新的过程。沉睡结束之后,这里使用了runOnUiThread()方法将线程切 换回主线程,然后调用initFruits()方法重新生成数据,接着再调用FruitAdapter的 notifyDataSetChanged()方法通知数据发生了变化,最后调用SwipeRefreshLayout的 setRefreshing()方法并传入false,表示刷新事件结束,并隐藏刷新进度条。

6.可折叠式标题栏

1.CollapsingToolbarLayout

CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由 Material库提供的。CollapsingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而且能够实现非常华丽的效果。

CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为 AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局

首先我们需要一个额外的Activity作为水果的详情展示界面,右击com.example.materialtest 包→New→Activity→Empty Activity,创建一个FruitActivity,并将布局名指定成 activity_fruit.xml,然后我们开始编写水果详情展示界面的布局。

由于整个布局文件比较复杂,这里我准备采用分段编写的方式。activity_fruit.xml中的内容主要分为两部分,一个是水果标题栏,一个是水果内容详情

首先实现标题栏部分,这里使用CoordinatorLayout作为最外层布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FruitActivity">

</androidx.coordinatorlayout.widget.CoordinatorLayout>

记得定义一个 xmlns:app的命名空间,在Material Design的开发中会经常用到它。

接着我们在CoordinatorLayout中嵌套一个AppBarLayout

<com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:id="@+id/appBar">
    
</com.google.android.material.appbar.AppBarLayout>

给AppBarLayout定义了一个id,将它的宽度指定为 match_parent,高度指定为250 dp

在AppBarLayout中再嵌套一个CollapsingToolbarLayout

<com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:id="@+id/appBar">

    <com.google.android.material.appbar.CollapsingToolbarLayout
        android:id="@+id/collapsingToolbar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:contentScrim="#00ff66"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">
        
    </com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

android:theme属性指定了一个ThemeOverlay.AppCompat.Dark.ActionBar的主题,之前在activity_main.xml中给Toolbar指定的也是这个主题,只不过这里要实现更加高级的Toolbar效果,因此需要将这个主题的指定提到上一层来。 app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的 背景色,其实CollapsingToolbarLayout在折叠之后就是一个普通的Toolbar,具体的效果我们待会儿就能看到。app:layout_scrollFlags属 性我们也是见过的,只不过之前是给Toolbar指定的,现在也移到外面来了。

scroll表示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动,exitUntilCollapsed 表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。

在CollapsingToolbarLayout中定义标题栏的具体内容

<com.google.android.material.appbar.CollapsingToolbarLayout
    android:id="@+id/collapsingToolbar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:contentScrim="#00ff66"
    app:layout_scrollFlags="scroll|exitUntilCollapsed">

    <ImageView
        android:id="@+id/fruitImageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        app:layout_collapseMode="parallax" />
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_collapseMode="pin" />

</com.google.android.material.appbar.CollapsingToolbarLayout>

我们在CollapsingToolbarLayout中定义了一个ImageView和一个Toolbar,这个高级版的标题栏将是由普通的标题栏加上图片组合而成的。

app:layout_collapseMode用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,

Toolbar指定成 pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移

下面开始编写水果内容详情部分。继续修改 activity_fruit.xml中的代码

</com.google.android.material.appbar.AppBarLayout>

<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
</androidx.core.widget.NestedScrollView>

水果内容详情的最外层布局使用了一个NestedScrollView,

它和AppBarLayout是平级的。它允许使用滚动的方式来查看屏幕以外的 数据,NestedScrollView在此基础之上还增加了嵌套响应滚动事件的功能。

由于 CoordinatorLayout本身已经可以响应滚动事件了,因此我们在它的内部就需要使用 NestedScrollView或RecyclerView这样的布局。

这里还通过app:layout_behavior 属性指定了一个布局行为,这和之前在RecyclerView中的用法是一模一样的。

不管是ScrollView还是NestedScrollView,它们的内部都只允许存在一个直接子布局。

如果我们想要在里面放入很多东西的话,通常先嵌套一个LinearLayout,然后再在 LinearLayout中放入具体的内容

<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </LinearLayout>


</androidx.core.widget.NestedScrollView>

这里我们嵌套了一个垂直方向的LinearLayout,并将layout_width设置为 match_parent,将layout_height设置为wrap_content。

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.google.android.material.card.MaterialCardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="15dp"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="35dp"
        app:cardCornerRadius="4dp">
    </com.google.android.material.card.MaterialCardView>
    <TextView
        android:id="@+id/fruitContentText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp" />
    
</LinearLayout>

为了让界面更 加美观,我在MaterialCardView和TextView上都加了一些边距。

MaterialCardView 的marginTop加了35 dp的边距,这是为下面要编写的东西留出空间。

这样就把水果标题栏和水果内容详情的界面都编写完了,不过我们还可以在界面上再添加一个悬浮按钮。这个悬浮按钮并不是必需的,根据具体的需求添加就可以了,如果加入的 话,我们将获得一些额外的动画效果。

修改 activity_fruit.xml中的代码

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:src="@drawable/ic_backup"
    app:layout_anchor="@id/appBar"
    app:layout_anchorGravity="bottom|end"/>

加入了一个FloatingActionButton,它和AppBarLayout以及 NestedScrollView是平级的。FloatingActionButton中使用app:layout_anchor属性指定 了一个锚点,我们将锚点设置为AppBarLayout,这样悬浮按钮就会出现在水果标题栏的区域 内,接着又使用app:layout_anchorGravity属性将悬浮按钮定位在标题栏区域的右下角

修改FruitActivity中的代码

public static final String FRUIT_NAME = "fruit_name";
public static final String FRUIT_IMAGE_ID = "fruit_image_id";






Intent intent = getIntent();
    String fruitName = intent.getStringExtra(FRUIT_NAME);
    int fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0);
    Toolbar toolbar = findViewById(R.id.toolbar);
    CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout)
            findViewById(R.id.collapsingToolbar);
    ImageView fruitImageView = findViewById(R.id.fruitImageView);
    TextView fruitContentText = findViewById(R.id.fruitContentText);
    setSupportActionBar(toolbar);
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
        actionBar.setDisplayHomeAsUpEnabled(true);
    }
    collapsingToolbar.setTitle(fruitName);
    fruitImageView.setImageResource(fruitImageId);
    String fruitContent = generateFruitContent(fruitName);
    fruitContentText.setText(fruitContent);
}
private String generateFruitContent(String fruitName) {
    StringBuilder fruitContent = new StringBuilder();
    for (int i = 0; i < 500; i++) {
        fruitContent.append(fruitName);
    }
    return fruitContent.toString();
}

@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            finish();
            return true;
    }
    return super.onOptionsItemSelected(item);
}

FruitActivity中的代码并不是很复杂。首先,在onCreate()方法中,我们通过Intent获取了 传入的水果名和水果图片的资源id。接着使用了Toolbar的标准用法,将它作为ActionBar显示,并启用Home按钮。由于Home按钮的默认图标就是一个返回箭头,这正是我们所期望的, 因此就不用额外设置别的图标了

接下来开始填充界面上的内容,调用CollapsingToolbarLayout的setTitle()方法,将水果名设置成当前界面的标题,然后使用Glide加载传入的水果图片,并设置到标题栏的 ImageView上面。

接着需要填充水果的内容详情,我使用了一个generateFruitContent()方法将水果名循环拼接500次,从而生成了一个比较长的字符串,将它设置到了TextView上面。

最后在onOptionsItemSelected()方法中处理了Home按钮的点击事件,当点击这个按钮时,就调用finish()方法关闭当前的Activity,从而返回上一个Activity。

处理RecyclerView的点击事件,修改FruitAdapter中的代码

@Override
public FruitAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    if (mContext == null) {
        mContext = parent.getContext();
    }
    View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,
            parent, false);
    final ViewHolder holder = new ViewHolder(view);
    holder.cardView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            int position = holder.getAdapterPosition();
            Fruit fruit = mFruitList.get(position);
            Intent intent = new Intent(mContext, FruitActivity.class);
            intent.putExtra(FruitActivity.FRUIT_NAME, fruit.getName());
            intent.putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.getImageId());
            mContext.startActivity(intent);
        }
    });
    return holder;

给fruit_item.xml的最外层布局注册了一个点击事 件监听器,然后在点击事件中获取当前点击项的水果名和水果图片资源id,把它们传入Intent 中,最后调用startActivity()方法启动FruitActivity。

2.充分利用系统状态栏空间

要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows这个属性来实现。在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout这种嵌套结构的布局中,将控件的android:fitsSystemWindows属性指定成true,就表示该控件会出现在系统状态栏里。对应到我们的程序,那就是水果标题栏中的ImageView应该设置这个属性了。 不过只给ImageView设置这个属性是没有用的,我们必须将ImageView布局结构中的所有父布局都设置上这个属性才可以,修改activity_fruit.xml中的代码

<androidx.coordinatorlayout.widget.CoordinatorLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FruitActivity"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:id="@+id/appBar"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:contentScrim="#00ff66"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            android:fitsSystemWindows="true"
            >

还必须在 程序的主题中将状态栏颜色指定成透明色才行

指定成透明色的方法很简单,在主题中将 android:statusBarColor属性的值指定成@android:color/transparent就可以了

打开(新建)res/values/styles.xml文件,对主题的内容进行修改

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/white</item>
        <item name="colorPrimaryDark">@color/black</item>
        <item name="colorAccent">@color/white</item>
    </style>

    <style name="FruitActivityTheme" parent="AppTheme">
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>


</resources>

需要让FruitActivity使用这个主题才可以,修改AndroidManifest.xml中的代码:

<activity
    android:name=".FruitActivity"
    android:exported="false"
    android:theme="@style/FruitActivityTheme"/>

这里使用android:theme属性单独给FruitActivity指定了FruitActivityTheme这个主题

点击就会报错,排查后发现是主题设置的问题,目前还没有解决办法,似乎是因为新版的主题与书中的不一致?因为书中的所有颜色设置即@color/colorAccent,@color/colorPrimary无法找到会报错

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- Customize your theme here. -->
        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
    </style>

    <style name="FruitActivityTheme" parent="AppTheme">
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>


</resources>

我将样式文件改为这样,倒是可以运行了,但并没有与状态栏融合,并且标题显示很差劲,这应该是新版的代码导致的删除FruitActivity中的以下代码

ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
    Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
    v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
    return insets;
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值