Android学习记录——12.Material Design

1.什么是Material Design

Material Design是由谷歌的设计工程师们基于传统优秀的设计原则,结合丰富的创意和科学技术所发明的一套全新的界面设计语言,包含了视觉、运动、互动效果等特性。那么谷歌凭什么认为Material Design就能解决Android平台界面风格不统一的问题呢? 一言以蔽之, 好看!
没错,这次谷歌在界面设计上确实是下足了功夫,很多媒体评论,Material Design的出现使得Android首次在UI方面超越了iOS。按照正常的思维来想,如果各个公司都无法设计出比 Material Design更出色的界面风格,那么它们就应该理所当然地使用Material Design来设计界面, 从而也就能解决Android平台界面风格不统一的问题了。
为了做出表率,谷歌从Android 5.0系统开始,就将所有内置的应用都使用Material Design 风格来进行设计。
不过,在重磅推出之后,Material Design的普及程度却不能说是特别理想。因为这只是一个推荐的设计规范,主要是面向UI设计人员的,而不是面向开发者的。很多开发者可能根本就搞不清楚什么样的界面和效果才叫Material Design,就算搞清楚了,实现起来也会很费劲,因为不少Material Design的效果是很难实现的,而Android中却几乎没有提供相应的API支持,一切都要靠开发者自己从零写起。
谷歌当然也意识到了这个问题,于是在2015年的Google I/O大会上推出了一个Design Support库,这个库将Material Design中最具代表性的一些控件和效果进行了封装,使得开发者 在即使不了解Material Design的情况下也能非常轻松地将自己的应用Material化。本篇博客中我们就将对Design Support这个库进行深入的学习,并且配合一些其他的控件来完成一个优秀的Material Design 应用。
新建一个项目,然后我们马上开始学习吧!

2.Toolbar

Toolbar将会是我们接触的第一个Material控件。虽说对于Toolbar你暂时应该还是比较陌生的,但是对于它的另一个相关控件ActionBar,你就应该有点熟悉了。
回忆一下,我们曾经在之前为了使用一个自定义的标题栏,而把系统原生的ActionBar隐藏掉。没错,每个活动最顶部的那个标题栏其实就是ActionBar,之前我们编写的所有程序里 一直都有ActionBar的身影。
不过ActionBar由于其设计的原因,被限定只能位于活动的顶部,从而不能实现一些Material Design的效果,因此官方现在已经不再建议使用ActionBar 了。那么本篇博客中我也就不准备再介绍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:supportsRtl="true"
    android:theme="@style/AppTheme">

可以看到,这里使用android:theme属性指定了一个AppTheme的主题。那么这个AppTheme又是在哪里定义的呢?打开res/values/styles.xml文件,代码如下所示:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

这里定义了一个叫AppTheme的主题,然后指定它的parent主题是Theme.AppCompat.Light.DarkActionBar。这个DarkActionBar是一个深色的ActionBar主题,我们之前所有的项目中自带的ActionBar就是因为指定了这个主题才出现的。
而现在我们准备使用Toolbar来替代ActionBar,因此需要指定一个不带ActionBar的主题,通常有 Theme.AppCompat.NoActionBar和 Theme.AppCompat.Light.NoActionBar 这两种主题可选。 其中Theme.AppCompat.NoActionBar 表示深色主题,它会将界面的主体颜色设成深色,陪衬颜色设成淡色。而Theme.AppCompat.Light.NoActionBar表示淡色主题,它会将界面的主体颜色设成淡色,陪衬颜色设成深色。具体的效果你可以自己动手试一试,这里由于我们之前的程序一直都是以淡色为主的,那么我就选用淡色主题了,如下所示:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

然后观察一下AppTheme中的属性重写,这里重写了 colorPrimary、colorPrimaryDark 和colorAccent这3个属性的颜色。那么这3个属性分别代表着什么位置的颜色呢?我用语言比较难描述清楚,还是通过一张图来理解一下吧,如图所示:
在这里插入图片描述
可以看到,每个属性所指定颜色的位置直接一目了然了。
除了上述3个属性之外,我们还可以通过textColorPrimary、windowBackground和navigationBarColor等属性来控制更多位置的颜色。不过唯独colorAccent这个属性比较难理解,它不只是用来指定这样一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色。
现在我们已经将ActionBar隐藏起来了,那么接下来看一看如何使用Toolbar来替代ActionBar。修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">

    <android.support.v7.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" />

</FrameLayout>

虽然这段代码不长,但是里面着实有不少技术点是需要我们去仔细琢磨一下的。首先看一下第2行,这里使用xmlns:app指定了一个新的命名空间。思考一下,正是由于每个布局文件都会使用xmlns:android来指定一个命名空间,因此我们才能一直使用android:id,android:layout_width等写法,那么这里指定了 xmlns:app,也就是说现在可以使用app:attribute 这样的写法了。但是为什么这里要指定一个xmlns: app的命名空间呢?这是由于Material Design 是在Android 5.0系统中才出现的,而很多的Material属性在5.0之前的系统中并不存在,那么为了能够兼容之前的老系统,我们就不能使用android:attribute这样的写法了,而是应该使用 app:attribute。
接下来定义了一个Toolbar控件,这个控件是由appcompat-v7库提供的。这里我们给Toolbar指定了一个id,将它的宽度设置为match_parent,高度设置为actionBar的高度,背景色设置为 colorPrimary。不过下面的部分就稍微有点难理解了,由于我们刚才在styles.xml中将程序的主题指定成了淡色主题,因此Toolbar现在也是淡色主题,而Toolbar上面的各种元素就会自动使用深色系,这是为了和主体颜色区别开。但是这个效果看起来就会很差,之前使用ActionBar时文字都是白色的,现在变成黑色的会很难看。那么为了能让Toolbar单独使用深色主题,这里我们使用 android: theme 属性,将 Toolbar 的主题指定成了 ThemeOverlay.AppCompat.Dark.ActionBar。 但是这样指定完了之后又会岀现新的问题,如果Toolbar中有菜单按钮,那么弹出的菜单项也会变成深色主题,这样就再次变得十分难看,于是这里使用了 app:popupTheme属性单独将弹出的菜单项指定成了淡色主题。之所以使用app: popupTheme,是因为popupTheme这个属性是在Android 5.0系统中新增的,我们使用app:popupTheme的话 就可以兼容Android 5.0以下的系统了。
如果你觉得上面的描述很绕的话,可以自己动手做一做试验,看看不指定上述主题会是什么样的效果,这样你会理解得更加深刻。
写完了布局,接下来我们修改MainActivity,代码如下所示:

package com.example.materialtest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }
}

这里关键的代码只有两句,首先通过findViewByld()得到Toolbar的实例,然后调用 setSupportActionBar()方法并将Toolbar的实例传入,这样我们就做到既使用了Toolbar,又让它的外观与功能都和ActionBar—致了。
现在运行一下程序,效果如图所示:
在这里插入图片描述
这个标题栏我们再熟悉不过了,虽然看上去和之前的标题栏没什么两样,但其实它已经是Toolbar而不是ActionBar了。因此它现在也具备了实现Material Design效果的能力,这个我们在后面就会学到。
接下来我们再学习一些Toolbar比较常用的功能吧,比如修改标题栏上显示的文字内容。这段文字内容是在AndroidManifest.xml中指定的,如下所示:

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

    <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">
        <activity android:name=".MainActivity"
            android:label="Fruits">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

这里给activity增加了一个android: label属性,用于指定在Toolbar中显示的文字内容,如果没有指定的话,会默认使用application中指定的label内容,也就是我们的应用名称。
不过只有一个标题的Toolbar看起来太单调了,我们还可以再添加一些action按钮来让Toolbar更加丰富一些,这里我提前准备了几张图片来作为按钮的图标,将它们放在drawable-xxhdpi目录下。现在右击res目录—>New—>Directory,创建一个menu文件夹。然后右击menu文件夹—>New—>Menu resource file,创建一个toolbar.xml文件,并编写如下代码:

<?xml version="1.0" encoding="utf-8"?>
<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="返回"
        app:showAsAction="always"/>
    <item
        android:id="@+id/delete"
        android:icon="@drawable/ic_delete"
        android:title="删除"
        app:showAsAction="ifRoom"/>
    <item
        android:id="@+id/settings"
        android:icon="@drawable/ic_settings"
        android:title="设置"
        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中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

非常简单,我们在onCreateOptionsMenu()方法中加载了 toolbar.xml这个菜单文件,然后在onOptionsItemSelected()方法中处理各个按钮的点击事件。现在重新运行一下程序,效果如图所示:
在这里插入图片描述
可以看到,Toolbar上面现在显示了两个action按钮,这是因为Backup按钮指定的显示位置是always, Delete按钮指定的显示位置是ifRoom,而现在屏幕空间很充足,因此两个按钮都会显示在Toolbar中。另外一个Settings按钮由于指定的显示位置是never,所以不会显示在Toolbar 中,点击一下最右边的菜单按钮来展开菜单项,你就能找到Settings按钮了。另外这些action按钮都是可以响应点击事件的,你可以自己去试一试。
好了,关于Toolbar的内容就先讲这么多吧。当然Toolbar的功能还远远不只这些,不过我 显然无法在一节当中就把所有的用法全部学完,后面会结合其他控件来挖掘Toolbar的更多功能。

3.滑动菜单

滑动菜单可以说是Material Design中最常见的效果之一了,在许多著名的应用(如Gmail、 Google+等)中,都有滑动菜单的功能。虽说这个功能看上去好像挺复杂的,不过借助谷歌提供的各种工具,我们可以很轻松地实现非常炫酷的滑动菜单效果,那么我们马上开始吧。

3.1 DrawerLayout

所谓的滑动菜单就是将一些菜单选项隐藏起来,而不是放置在主屏幕上,然后可以通过滑动的方式将菜单显示出来。这种方式既节省了屏幕空间,又实现了非常好的动画效果,是Material Design中推荐的做法。
不过如果我们全靠自己去实现上述功能的话,难度恐怕就很大了。幸运的是,谷歌提供了一 个DrawerLayout控件,借助这个控件,实现滑动菜单简单又方便。
先来简单介绍一下DrawerLayout的用法吧。首先它是一个布局,在布局中允许放入两个直接子控件,第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。因此, 我们就可以对activity_main.xml中的代码做如下修改:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout 
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout 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">

        <android.support.v7.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" />

    </FrameLayout>
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent" 
        android:layout_gravity="start"
        android:text="这是菜单"
        android:textSize="30sp"
        android:background="#FFF"/>
    
    
</android.support.v4.widget.DrawerLayout>

可以看到,这里最外层的控件使用了 DrawerLayout,这个控件是由support-v4库提供的。 DrawerLayout中放置了两个直接子控件,第一个子控件是FrameLayout,用于作为主屏幕中显示的内容,当然里面还有我们刚刚定义的Toolbar。第二个子控件这里使用了一个TextView,用于作为滑动菜单中显示的内容,其实使用什么都可以,DrawerLayout并没有限制只能使用固定的控件。
但是关于第二个子控件有一点需要注意,layout_gravity这个属性是必须指定的,因为我们需要告诉DrawerLayout滑动菜单是在屏幕的左边还是右边,指定left表示滑动菜单在左边,指 定right表示滑动菜单在右边。这里我指定了 start,表示会根据系统语言进行判断,如果系统语言是从左往右的,比如英语、汉语,滑动菜单就在左边,如果系统语言是从右往左的,比如阿拉伯语,滑动菜单就在右边。
没错,只需要改动这么多就可以了,现在重新运行一下程序,然后在屏幕的左侧边缘向右拖动,就可以让滑动菜单显示出来了,如图所示:
在这里插入图片描述
然后向左滑动菜单,或者点击一下菜单以外的区域,都可以让滑动菜单关闭,从而回到主界面。无论是展示还是隐藏滑动菜单,都是有非常流畅的动画过渡的。
可以看到,我们只是稍微改动了一下布局文件,就能实现如此炫酷的效果,是不是觉得挺激动呢?不过现在的滑动菜单还有点问题,因为只有在屏幕的左侧边缘进行拖动时才能将菜单拖出来,而很多用户可能根本就不知道有这个功能,那么该怎么提示他们呢?
Material Design建议的做法是在Toolbar的最左边加入一个导航按钮,点击了按钮也会将滑动菜单的内容展示出来。这样就相当于给用户提供了两种打开滑动菜单的方式,防止一些用户不 知道屏幕的左侧边缘是可以拖动的。
下面我们开始来实现这个功能。首先我准备了一张导航按钮的图标ic_menu.png,将它放在 drawable-xxhdpi目录下。然后修改MainActivity中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    
    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

这里我们并没有改动多少代码,首先调用findViewByld()方法得到了 DrawerLayout的实例,然后调用getSupportActionBar()方法得到了ActionBar的实例,虽然这个ActionBar的具体实现是由Toolbar来完成的。接着调用ActionBar的setDisplayHomeAsUpEnabled()方法让导航按钮显示出来,又调用了 setHomeAsUpIndicator()方法来设置一个导航按钮图标。实际上, Toolbar最左侧的这个按钮就叫作HomeAsUp按钮,它默认的图标是一个返回的箭头,含义是返回上一个活动。很明显,这里我们将它默认的样式和作用都进行了修改。
接下来在onOptionsItemSelected()方法中对HomeAsUp按钮的点击事件进行处理,HomeAsUp按钮的id永远都是 android.R.id.home。然后调用 DrawerLayout的openDrawer() 方法将滑动菜单展示出来,注意openDrawer()方法要求传入一个Gravity参数,为了保证这里的行为和XML中定义的一致,我们传入了 GravityCompat.START。
现在重新运行一下程序,效果如图所示:
在这里插入图片描述
可以看到,在Toolbar的最左边出现了一个导航按钮,用户看到这个按钮就知道这肯定是可以点击的。现在点击一下这个按钮,滑动菜单界面就会再次展示出来了。

3.2 NavigationView

事实上,你可以在滑动菜单页面定制任意的布局,不过谷歌给我们提供了一种更好的方法一一 使用NavigationView。NavigationView是Design Support库中提供的一个控件,它不仅是严格按照Material Design的要求来进行设计的,而且还可以将滑动菜单页面的实现变得非常简单。接下来我们就学习一下NavigationView的用法。
首先,既然这个控件是Design Support库中提供的,那么我们就需要将这个库引入到项目中才行。打开app/build.gradle文件,在dependencies闭包中添加如下内容:

implementation 'com.android.support:design:27.1.1'
implementation 'de.hdodenhof:circleimageview:2.1.0'

这里添加了两行依赖关系,第一行就是Design Support库,第二行是一个开源项目 CirclelmageView,它可以用来轻松实现图片圆形化的功能,我们待会就会用到它。Circlelmage View的项目主页地址是:https://github.com/hdodenhof/CirclelmageView。
在开始使用NavigationView之前,我们还需要提前准备好两个东西:menu和headerLayout。menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在NavigationView 中显示头部布局的。
我们先来准备menu,这里我事先找了几张图片来作为按钮的图标,并将它们放在了drawable-xxhdpi 目录下。然后右击 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/nav_call"
            android:icon="@drawable/nav_call"
            android:title="呼叫"/>
        <item
            android:id="@+id/nav_friend"
            android:icon="@drawable/nav_friends"
            android:title="朋友"/>
        <item
            android:id="@+id/nav_location"
            android:icon="@drawable/nav_location"
            android:title="定位"/>
        <item
            android:id="@+id/nav_mail"
            android:icon="@drawable/nav_mail"
            android:title="邮件"/>
        <item
            android:id="@+id/task"
            android:icon="@drawable/nav_task"
            android:title="工作"/>
    </group>
</menu>

我们首先在

中嵌套了一个标签,然后将group的checkableBehavior属性指定为single。group表示一个组,checkableBehavior指定为single表示组中的所有菜单项只能单选。
那么下面我们来看一下这些菜单项吧。这里一共定义了 5个item,分别使用android:id属性指定菜单项的id,android:icon属性指定菜单项的图标,android:title属性指定菜单项显示的文字。就是这么简单,现在我们已经把menu准备好了。
接下来应该准备headerLayout了,这是一个可以随意定制的布局,不过我并不想将它做得太复杂。这里简单起见,我们就在headerLayout中放置头像、用户名、邮箱地址这3项内容吧。
说到头像,那我们还需要再准备一张图片,这里我找了一张宠物图片,并把它放在了 drawable—>xxhdpi目录下。另外这张图片最好是一张正方形图片,因为待会我们会把它圆形化。然 后右击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="?attr/colorPrimary">

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/icon_image"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@drawable/nav_icon"
        android:layout_centerInParent="true"/>

    <TextView
        android:id="@+id/mail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="545646733@qq.com"
        android:textColor="#FFF"
        android:textSize="14sp"/>

    <TextView
        android:id="@+id/uesrname"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/mail"
        android:text="莫息涛"
        android:textColor="#FFF"
        android:textSize="14sp"/>

</RelativeLayout>

可以看到,布局文件的最外层是一个RelativeLayout,我们将它的宽度设为match_parent, 高度设为180dp。这是一个NavigationView比较适合的高度,然后指定它的背景色为colorPrimary。
在RelativeLayout中我们放置了3个控件,CirclelmageView是一个用于将图片圆形化的控件,它的用法非常简单,基本和ImageView是完全一样的,这里给它指定了一张图片作为头像,然后设置为居中显示。另外两个TextView分别用于显示用户名和邮箱地址,它们都用到了一些RelativeLayout的定位属性,相信肯定难不倒你吧?
现在menu和headerLayout都准备好了,我们终于可以使用NavigationView 了。修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout 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">

        <android.support.v7.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" />

    </FrameLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header" />


</android.support.v4.widget.DrawerLayout>

可以看到,我们将之前的TextView换成了 NavigationView,这样滑动菜单中显示的内容也就 变成NavigationView了。这里又通过app:menu和app:headerLayout属性将我们刚才准备好的menu和headerLayout设置了进去,这样NavigationView就定义完成了。
NavigationView虽然定义完成了,但是我们还要去处理菜单项的点击事件才行。修改MainActivity中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        NavigationView navView = (NavigationView)findViewById(R.id.nav_view);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
        navView.setCheckedItem(R.id.nav_call);
        navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem item) {
                mDrawerLayout.closeDrawers();
                return true;
            }
        });
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

代码还是比较简单的,这里首先获取到了 NavigationView的实例,然后调用它的 setCheckedltem()方法将Call菜单项设置为默认选中。接着调用了 setNavigationitemSelectedListener()方法来设置一个菜单项选中事件的监听器,当用户点击了任意菜单项时, 就会回调到onNavigationItemSelected()方法中。我们可以在这个方法中写相应的逻辑处理,不过这里我并没有附加任何逻辑,只是调用了 DrawerLayout的closeDrawers()方法将滑动菜单关闭,这也是合情合理的做法。
现在可以重新运行一下程序了,点击一下Toolbar左侧的导航按钮,效果如图所示:
在这里插入图片描述
相信你对现在做出来的效果也一定十分满意吧?不过不要满足于现状,后面我们会实现更加炫酷的效果。跟紧脚步,继续学习。

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

立面设计是Material Design中一条非常重要的设计思想,也就是说,按照Material Design的理念,应用程序的界面不仅仅只是一个平面,而应该是有立体效果的。在官方给出的示例中,最简单且最具代表性的立面设计就是悬浮按钮了,这种按钮不属于主界面平面的一部分,而是位于另外一个维度的,因此就会给人一种悬浮的感觉。
本节中我们会对这个悬浮按钮的效果进行学习,另外还会学习一种可交互式的提示工具。关于提示工具,我们之前一直都是使用的Toast,但是Toast只能用于告知用户某某事情已经发生了,用户却不能对此做出任何的响应,那么今天我们就将在这一方面进行扩展。

4.1 FloatingActionButton

FloatingActionButton是Design Support库中提供的一个控件,这个控件可以帮助我们比较轻松地实现悬浮按钮的效果。其实在之前的图中,我们就已经预览过悬浮按钮是长什么样子的了,它默认会使用colorAccent来作为按钮的颜色,我们还可以通过给按钮指定一个图标来表明这个按钮的作用是什么。
下面开始来具体实现。首先仍然需要提前准备好一个图标,这里我放置了一张ic_done.png到drawable-xxhdpi目录下。然后修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout 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">

        <android.support.v7.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.support.design.widget.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_done" />

    </FrameLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header" />


</android.support.v4.widget.DrawerLayout>

可以看到,这里我们在主屏幕布局中加入了一个FloatingActionButton>这个控件的用法并没有什么特别的地方,layout_width和layout_height属性都指定成 wrap_content,layout_gravity属性指定将这个控件放置于屏幕的右下角,其中end的工作原理和之前的start是一样的,即如果系统语言是从左往右的,那么end就表示在右边,如果系统语言是从右往左的,那么end就表示在左边。然后通过layout_margin属性给控件的四周留点边距,紧贴着屏幕边缘肯定是不好看的,最后通过src属性给FloatingActionButton设置了一个图标。
没错,就是这么简单,现在我们就可以来运行一下了,效果如图所示:
在这里插入图片描述
一个漂亮的悬浮按钮就在屏幕的右下方出现了。
如果你仔细观察的话,会发现这个悬浮按钮的下面还有一点阴影。其实这很好理解,因为FloatingActionButton是悬浮在当前界面上的,既然是悬浮,那么就理所应当会有投影,Design Support库连这种细节都帮我们考虑到了。
说到悬浮,其实我们还可以指定FloatingActionButton的悬浮高度,如下所示:

<android.support.design.widget.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_done"
    app:elevation="8dp"/>

这里使用app:elevation属性来给FloatingActionButton指定一个高度值,高度值越大, 投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓。当然这些效果的差异其实都不怎么明显,我个人感觉使用默认的FloatingActionButton效果就已经足够了。
接下来我们看一下FloatingActionButton是如何处理点击事件的,毕竟,一个按钮首先要能点击才有意义。修改MainActivity中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        NavigationView navView = (NavigationView)findViewById(R.id.nav_view);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
        navView.setCheckedItem(R.id.nav_call);
        navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem item) {
                mDrawerLayout.closeDrawers();
                return true;
            }
        });
        FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this,"FAB Clicked",Toast.LENGTH_SHORT).show();
            }
        });
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

如果你在期待FloatingActionButton会有什么特殊用法的话,那可能就要让你失望了,它和普通的Button其实没什么两样,都是调用setOnClickListener()方法来注册一个监听器,当点击按钮时,就会执行监听器中的onClick()方法,这里我们在onClick()方法中弹出了一个Toast。
现在重新运行一下程序,并点击FloatingActionButton,效果如图所示:
在这里插入图片描述

5.卡片式布局

虽然现在MaterialTest中已经应用了非常多的Material Design效果,不过你会发现,界面上最主要的一块区域还处于空白状态。这块区域通常都是用来放置应用的主体内容的,我准备使用一些精美的水果图片来填充这部分区域。
那么为了要让水果图片也能Material化,本节中我们将会学习如何实现卡片式布局的效果。
卡片式布局也是Materials Design中提岀的一个新的概念,它可以让页面中的元素看起来就像在卡片中一样,并且还能拥有圆角和投影,下面我们就开始具体学习一下。

5.1 CardView

CardView是用于实现卡片式布局效果的重要控件,由appcompat-v7库提供。实际上,CardView 也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。
我们先来看一下CardView的基本用法吧,其实非常简单,如下所示:

<android.support.v7.widget.CardView
android:layout_width="matchparent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp" 
app:elevation="5dp"> 
<TextView
android: id="@+id/info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.CardView>

这里定义了一个CardView布局,我们可以通过app:cardCornerRadius属性指定卡片圆角的弧度,数值越大,圆角的弧度也越大。另外还可以通过app:elevation属性指定卡片的高度,高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓,这一点和FloatingActionButton是一致的。
然后我们在CardView布局中放置了一个TextView,那么这个TextView就会显示在一张卡片当中了,CardView的用法就是这么简单。
但是我们显然不可能在如此宽阔的一块空白区域内只放置一张卡片,为了能够充分利用屏幕的空间,这里我准备综合运用一下之前学到的知识,使用RecyclerView来填充项目的主界面部分。还记得之前实现过的水果列表效果吗?这次我们将升级一下,实现一个高配版的水果列表效果。
既然是要实现水果列表,那么首先肯定需要准备许多张水果图片,这里我从网上挑选了一些精美的水果图片,将它们复制到了项目当中。
然后由于我们还需要用到RecyclerView CardView这几个控件,因此必须在app/build.gradle 文件中声明这些库的依赖才行:

implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.github.bumptech.glide:glide:3.7.0'

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

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <android.support.v7.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.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <android.support.design.widget.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_done"
            app:elevation="8dp"/>

    </android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header" />


</android.support.v4.widget.DrawerLayout>

这里我们在CoordinatorLayout中添加了一个RecyclerView,给它指定一个id,然后将宽度和高度都设置为match_parent,这样RecyclerView也就占满了整个布局的空间。
接着定义一个实体类Fruit,代码如下所示:

package com.example.materialtest;

/**
 * Created by Administrator on 2018-10-18.
 */

public class Fruit {

    private String name;

    private int imageId;

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

    public String getName(){
        return name;
    }

    public int getImageId(){
        return imageId;
    }
}

Fruit类中只有两个字段,name表示水果的名字,imageld表示水果对应图片的资源id。然后需要为RecyclerView的子项指定一个我们自定义的布局,在layout目录下新建fruit_item.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    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/fruit_image"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="centerCrop"/>

        <TextView
            android:id="@+id/fruit_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="5dp"
            android:textSize="16sp"/>
    </LinearLayout>

</android.support.v7.widget.CardView>

这里使用了CardView来作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中的。CardView由于是一个FrameLayout,因此它没有什么方便的定位方式,这里我们只好在CardView中再嵌套一个LinearLayout,然后在LinearLayout中放置具体的内容。
内容倒也没有什么特殊的地方,就是定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称,并让TextView在水平方向上居中显示。注意在ImageView中我们使用了一个scaleType属性,这个属性可以指定图片的缩放模式。由于各张水果图片的长宽比例可能都不一致,为了让所有的图片都能填充满整个ImageView,这里使用了 centerCrop 模式,它可以让图片保持原有比例填充满ImageView,并将超出屏幕的部分裁剪掉。
接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为 FruitAdapter.ViewHolder,代码如下所示:

package com.example.materialtest;

import android.content.Context;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;

import java.util.List;

/**
 * Created by Administrator on 2018-10-18.
 */

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 = (ImageView)view.findViewById(R.id.fruit_image);
            fruitName = (TextView)view.findViewById(R.id.fruit_name);
        }
    }

    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }

    @Override
    public ViewHolder onCreateViewHolder(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(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();
    }
}

上述代码相信你一定很熟悉,和我们在之前编写的FruitAdapter几乎一模一样。唯一需要注意的是,在onBindViewHolder()方法中我们使用了Glide来加载水果图片。
那么这里就顺便来看一下Glide的用法吧,其实并没有太多好讲的,因为Glide的用法实在是太简单了。首先调用Glide.with()方法并传入一个Context. Activity或Fragment参数, 然后调用load()方法去加载图片,可以是一个URL地址,也可以是一个本地路径,或者是一个资源id,最后调用into()方法将图片设置到具体某一个ImageView中就可以了。
那么我们为什么要使用Glide而不是传统的设置图片方式呢?因为这次我从网上找的这些水 果图片像素都非常高,如果不进行压缩就直接展示的话,很容易就会引起内存溢出。而使用Glide 就完全不需要担心这回事,因为Glide在内部做了许多非常复杂的逻辑操作,其中就包括了图片压缩,我们只需要安心按照Glide的标准用法去加载图片就可以了。
这样我们就将RecyclerView的适配器也准备好了,最后修改MainActivity中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.design.widget.Snackbar;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

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

public class MainActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    private Fruit[] fruits = {new Fruit("苹果",R.drawable.apple),
            new Fruit("香蕉",R.drawable.banana),
            new Fruit("橘子",R.drawable.orange),
            new Fruit("西瓜",R.drawable.watermelon),
            new Fruit("梨子",R.drawable.pear),
            new Fruit("葡萄",R.drawable.grape),
            new Fruit("菠萝",R.drawable.pineapple),
            new Fruit("草莓",R.drawable.strawberry),
            new Fruit("樱桃",R.drawable.cherry),
            new Fruit("芒果",R.drawable.mango)};

    private List<Fruit> fruitList = new ArrayList<>();

    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        NavigationView navView = (NavigationView)findViewById(R.id.nav_view);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
        navView.setCheckedItem(R.id.nav_call);
        navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem item) {
                mDrawerLayout.closeDrawers();
                return true;
            }
        });
        FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
        fab.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 v) {
                                Toast.makeText(MainActivity.this, "数据恢复", Toast.LENGTH_SHORT).show();
                            }
                        })
                        .show();
            }
        });
        initFruits();
        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
        GridLayoutManager layoutManager = new GridLayoutManager(this,2);
        recyclerView.setLayoutManager(layoutManager);
        adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    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]);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

在MainActivity中我们首先定义了一个数组,数组里面存放了很多个Fruit的实例,每个实例都代表着一种水果。然后在initFruits()方法中,先是清空了一下fruitList中的数据,接着使用一个随机函数,从刚才定义的Fruit数组中随机挑选一个水果放入到fruitList当中, 这样每次打开程序看到的水果数据都会是不同的。另外,为了让界面上的数据多一些,这里使用了一个循环,随机挑选50个水果。
之后的用法就是RecyclerView的标准用法了,不过这里使用了 GridLayoutManager这种布局方式。在之前我们已经学过了 LinearLayoutManager和StaggeredGridLayoutManager,现在终 于将所有的布局方式都补齐了。GridLayoutManager的用法也没有什么特别之处,它的构造函数接收两个参数,第一个是Context,第二个是列数,这里我们希望每一行中会有两列数据。
现在重新运行一下程序,效果如图所示:
在这里插入图片描述
可以看到,精美的水果图片成功展示出来了。每个水果都是在一张单独的卡片当中的,并且还拥有圆角和投影,是不是非常美观?另外,由于我们是使用随机的方式来获取水果数据的,因此界面上会有一些重复的水果出现,这属于正常现象。
当你陶醉于当前精美的界面的时候,你是不是忽略了一个细节?哎呀,我们的Toolbar怎么不见了!仔细观察一下原来是被RecyclerView给挡住了。这个问题又该怎么解决呢?这就需要借助到另外一个工具了AppBarLayout。

5.2 AppBarLayout

首先我们来分析一下为什么RecyclerView会把Toolbar给遮挡住吧。其实并不难理解,由于RecyclerView 和Toolbar都是放置在 CoordinatorLayout 中的,而前面已经说过,CoordinatorLayout 就是一个加强版的FrameLayout,那么FrameLayout中的所有控件在不进行明确定位的情况下, 默认都会摆放在布局的左上角,从而也就产生了遮挡的现象。其实这已经不是你第一次遇到这种情况了,我们在之前学习FrameLayout的时候就早已见识过了控件与控件之间遮挡的效果。
既然已经找到了问题的原因,那么该如何解决呢?传统情况下,使用偏移是唯一的解决办法, 即让RecyclerView向下偏移一个Toolbar的高度,从而保证不会遮挡到Toolbar。不过我们使用的并不是普通的FrameLayout,而是CoordinatorLayout,因此自然会有一些更加巧妙的解决办法。
这里我准备使用Design Support库中提供的另外一个工具 AppBarLayouto。AppBarLayout 实际上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些Material Design的设计理念。
那么我们怎样使用AppBarLayout才能解决前面的覆盖问题呢?其实只需要两步就可以了,第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局行为。修改 activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <android.support.v7.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.support.design.widget.AppBarLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

        <android.support.design.widget.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_done"
            app:elevation="8dp"/>

    </android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header" />


</android.support.v4.widget.DrawerLayout>

可以看到,布局文件并没有什么太大的变化。我们首先定义了一个AppBarLayout,并将 Toolbar放置在了 AppBarLayout里面,然后在 RecyclerView中使用 app: layout_behavior 属性指定了一个布局行为。其中appbar_scrolling_view_behavior这个字符串也是由Design Support库提供的。
现在重新运行一下程序,你就会发现一切都正常了,如图所示:
在这里插入图片描述
虽说使用AppBarLayout已经成功解决了RecyclerView遮挡Toolbar的问题,但是刚才有提到 过,说AppBarLayout中应用了一些Material Design的设计理念,好像从上面的例子完全体现不出来呀。事实上,当RecyclerView滚动的时候就已经将滚动事件都通知给AppBarLayout了,只是我们还没进行处理而已。那么下面就让我们来进一步优化,看看AppBarLayout到底能实现什么样的Material Design效果。
当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去影响这些事件的,通过app:layout_scrollFlags属性就能实现。修改activity_main.xml中的代码,如 下所示:

<android.support.v7.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"
    app:layout_scrollFlags="scroll|enterAlways|snap"/>

这里在Toolbar中添加了一个app: layout_scrollFlags属性,并将这个属性的值指定成了 scroll | enterAlways | snapo 其中,scroll 表示当 RecyclerView 向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示。snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。
我们要改动的就只有这一行代码而已,现在重新运行一下程序,并向上滚动RecyclerView,效果如图所示。
在这里插入图片描述
可以看到,随着我们向上滚动RecyclerView,Toolbar竟然消失了,而向下滚动RecyclerView,Toolbar又会重新出现。这其实也是Material Design中的一项重要设计思想,因为当用户在向上滚动RecyclerView的时候,其注意力肯定是在RecyclerView的内容上面的,这个时候如果Toolbar还占据着屏幕空间,就会在一定程度上影响用户的阅读体验,而将Toolbar隐藏则可以让阅读体验达到最佳状态。当用户需要操作Toolbar上的功能时,只需要轻微向下滚动,Toolbar就会重新出现。这种设计方式,既保证了用户的最佳阅读效果,又不影响任何功能上的操作,MaterialDesign 考虑得就是这么细致入微。
当然了,像这种功能,如果是使用ActionBar的话,那就完全不可能实现了,Toolbar的出现为我们提供了更多的可能。

6.下拉刷新

下拉刷新这种功能早就不是什么新鲜的东西了,几乎所有的应用里都会有这个功能。不过市面上现有的下拉刷新功能在风格上都各不相同,并且和Material Design还有些格格不入的感觉。 因此,谷歌为了让Android的下拉刷新风格能有一个统一的标准,于是在Material Design中制定了一个官方的设计规范。当然,我们并不需要去深入了解这个规范到底是什么样的,因为谷歌早就提供好了现成的控件,我们只需要在项目中直接使用就可以了。
SwipeRefreshLayout就是用于实现下拉刷新功能的核心类,它是由suppor.v4库提供的。我们把想要实现下拉刷新功能的控件放置到SwipeRefireshLayout中,就可以迅速让这个控件支持下 拉刷新。那么在MaterialTest项目中,应该支持下拉刷新功能的控件自然就是RecyclerView 了。
由于SwipeRefreshLayout的用法也比较简单,下面我们就直接开始使用了。修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <android.support.v7.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"
            app:layout_scrollFlags="scroll|enterAlways|snap"/>

        </android.support.design.widget.AppBarLayout>
        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/swipe_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">
            
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        </android.support.v4.widget.SwipeRefreshLayout>

        <android.support.design.widget.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_done"
            app:elevation="8dp"/>
        
        

    </android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header" />


</android.support.v4.widget.DrawerLayout>

可以看到,这里我们在RecyclerView的外面又嵌套了一层SwipeRefreshLayout,这样 RecyclerView就自动拥有下拉刷新功能了。另外需要注意,由于RecyclerView现在变成了 SwipeRefreshLayout的子控件,因此之前使用app:layout behavior声明的布局行为现在也要移到 SwipeRefreshLayout 中才行。
不过这还没有结束,虽然RecyclerView已经支持下拉刷新功能了,但是我们还要在代码中处理具体的刷新逻辑才行。修改MainActivity中的代码,如下所示:

package com.example.materialtest;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.design.widget.Snackbar;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

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

public class MainActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;
    
    private SwipeRefreshLayout swipeRefresh;

    private Fruit[] fruits = {new Fruit("苹果",R.drawable.apple),
            new Fruit("香蕉",R.drawable.banana),
            new Fruit("橘子",R.drawable.orange),
            new Fruit("西瓜",R.drawable.watermelon),
            new Fruit("梨子",R.drawable.pear),
            new Fruit("葡萄",R.drawable.grape),
            new Fruit("菠萝",R.drawable.pineapple),
            new Fruit("草莓",R.drawable.strawberry),
            new Fruit("樱桃",R.drawable.cherry),
            new Fruit("芒果",R.drawable.mango)};

    private List<Fruit> fruitList = new ArrayList<>();

    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        NavigationView navView = (NavigationView)findViewById(R.id.nav_view);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
        navView.setCheckedItem(R.id.nav_call);
        navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem item) {
                mDrawerLayout.closeDrawers();
                return true;
            }
        });
        FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
        fab.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 v) {
                                Toast.makeText(MainActivity.this, "数据恢复", Toast.LENGTH_SHORT).show();
                            }
                        })
                        .show();
            }
        });
        initFruits();
        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
        GridLayoutManager layoutManager = new GridLayoutManager(this,2);
        recyclerView.setLayoutManager(layoutManager);
        adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
        swipeRefresh = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh);
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
        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();
    }

    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.toolbar,menu);
        return true;
    }

    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]);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            case R.id.backup:
                Toast.makeText(this,"你点击了返回",Toast.LENGTH_SHORT).show();
                break;
            case R.id.delete:
                Toast.makeText(this,"你点击了删除",Toast.LENGTH_SHORT).show();
                break;
            case R.id.settings:
                Toast.makeText(this,"你点击了设置",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
        return true;
    }
}

这段代码应该还是比较好理解的,首先通过findViewById()方法拿到SwipeRefreshLayout的实例,然后调用setColorSchemeResources()方法来设置下拉刷新进度条的颜色,这里我们就使用主题中的colorPrimary作为进度条的颜色了。接着调用setOnRefreshListener()方法来设置一个下拉刷新的监听器,当触发了下拉刷新操作的时候就会回调这个监听器的0nRefresh()方法,然后我们在这里去处理具体的刷新逻辑就可以了。
通常情况下,onRefresh()方法中应该是去网络上请求最新的数据,然后再将这些数据展示出来。这里简单起见,我们就不和网络进行交互了,而是调用一个refreshFruits()方法进行本地刷新操作。refreshFruits()方法中先是开启了一个线程,然后将线程沉睡两秒钟。之所以这么做,是因为本地刷新操作速度非常快,如果不将线程沉睡的话,刷新立刻就结束了,从而看不到刷新的过程。沉睡结束之后,这里使用了 runOnUiThread()方法将线程切换回主线程,然后调用initFruits()法重新生成数据,接着再调用FruitAdapter的notifyDataSetChanged() 方法通知数据发生了变化,最后调用SwipeRefreshLayout的setRefreshing()方法并传入false,用于表示刷新事件结束,并隐藏刷新进度条。
现在可以重新运行一下程序了,在屏幕的主界面向下拖动,会有一个下拉刷新的进度条出现,松手后就会自动进行刷新了,效果如图所示:
在这里插入图片描述
下拉刷新的进度条只会停留两秒钟,之后就会自动消失,界面上的水果数据也会随之更新。
这样我们就把下拉刷新的功能也成功实现了,并且这就是Material Design中规定的最标准的下拉刷新效果,还有什么会比这个更好看呢?目前我们的项目中已经应用了众多Material Design 的效果,Design Support库中的常用控件也学了大半了。不过本篇博客的学习之旅还没有结束,在最后的尾声部分,我们再来实现一个非常震撼的Material Design效果——可折叠式标题栏。

7.可折叠式标题栏

虽说我们现在的标题栏是使用Toolbar来编写的,不过它看上去和传统的ActionBar其实没什么两样,只不过可以响应RecyclerView的滚动事件来进行隐藏和显示。而Material Design中并没有限定标题栏必须是长这个样子的,事实上,我们可以根据自己的喜好随意定制标题栏的样式。那么本节中我们就来实现一个可折叠式标题栏的效果,需要借助CollapsingToolbarLayout这个工具。

7.1 CollapsingToolbarLayout

顾名思义,CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Design Support库提供的。CollapsingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。
不过,CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为 AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局,因此本节中我们要实现的功能其实需要综合运用前面所学的各种知识。那么话不多说,这就开始吧。
首先我们需要一个额外的活动来作为水果的详情展示界面,右击《项目名》—>New—>Activity—>Empty Activity,创建一个 FruitActivity,并将布局名指定成 activity_fruit.xml,然后我们开始编写水果详情展示界面的布局。
由于整个布局文件比较复杂,这里我准备采用分段编写的方式。activity _fruit.xml中的内容主要分为两部分,一个是水果标题栏,一个是水果内容详情,我们来一步步实现。
首先实现标题栏部分,这里使用CoordinatorLayout来作为最外层布局,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">
    
</android.support.design.widget.CoordinatorLayout>

一开始的代码还是比较简单的,相信没有什么需要解释的地方。注意始终记得要定义一个xmlns : app的命名空间,在Material Design的开发中会经常用到它。
接着我们在CoordinatorLayout中嵌套一个AppBarLayout,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">
    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

目前为止也没有什么难理解的地方,我们给AppBarLayout定义了一个id,将它的宽度指定match_parent,高度指定为250dp。当然这里的高度值你可以随意指定,不过我尝试之后发现250dp的视觉效果比较好。
接下来我们在AppBarLayout中再嵌套一个CollapsingToolbarLayout,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
        </android.support.design.widget.CollapsingToolbarLayout>
        
    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

从现在开始就稍微有点难理解了,这里我们使用了新的布局CollapsingToolbarLayouto其中, id、layout_width和layout_height这几个属性比较简单,我就不解释了。android:theme 属性指定了一个ThemeOverlay.AppCompat.Dark.ActionBar的主题,其实对于这部分我们也并不陌生,因为之前在activity_main.xmI中给Toolbar指定的也是这个主题,只不过这里要实现更加高级的Toolbar效果,因此需要将这个主题的指定提到上一层来。app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色,其实Collapsing- ToolbarLayout在折叠之后就是一个普通的Toolbar,那么背景色肯定应该是colorPrimary了,具体的效果我们待会儿就能看到。app:layout_scrollFlags属性我们也是见过的,只不过之前是给Toolbar指定的,现在也移到外面来了。其中,scroll表示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动,exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
接下来,我们在CollapsingToolbarLayout中定义标题栏的具体内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>
        
    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

可以看到,我们在CollapsingToolbarLayout中定义了一个ImageView和一个Toolbar,也就意味着,这个高级版的标题栏将是由普通的标题栏加上图片组合而成的。这里定义的大多数属性我们都是见过的,就不再解释了,只有一个app:layout_collapseMode比较陌生。它用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好。
这样我们就将水果标题栏的界面编写完成了,下面开始编写水果内容详情部分。继续修改 activity_fruit.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

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

</android.support.design.widget.CoordinatorLayout>

水果内容详情的最外层布局使用了一个NestedScrollView,注意它和AppBarLayout是平级的。 我们之前学过其用法,它允许使用滚动的方式来查看屏幕以外的数据, 而NestedScrollView在此基础之上还增加了嵌套响应滚动事件的功能。由于CoordinatorLayout本身已经可以响应滚动事件了,因此我们在它的内部就需要使用NestedScrollView或RecyclerView这样的布局。另外,这里还通过app:layout_behavior属性指定了一个布局行为,这和之前在 RecyclerView中的用法是一模一样的。
不管是ScrollView还是NestedScrollView,它们的内部都只允许存在一个直接子布局。因此,如果我们想要在里面放入很多东西的话,通常都会先嵌套一个LinearLayout,然后再在 LinearLayout中放入具体的内容就可以了,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.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>
    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

这里我们嵌套了一个垂直方向的LinearLayout,并将layout_width设置为match_parent, 将 layout_height设置为 wrap_content。
接下来在LinearLayout中放入具体的内容,这里我准备使用一个TextView来显示水果的内容详情,并将TextView放在一个卡片式布局当中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.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">

            <android.support.v7.widget.CardView
                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">

                <TextView
                    android:id="@+id/fruit_content_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="10dp"/>
            </android.support.v7.widget.CardView>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

这段代码也没有什么难理解的地方,都是我们学过的知识。需要注意的是,这里为了让界面更加美观,我在CardView和TextView上都加了一些边距。其中,CardView的marginTop加了 35dp的边距,这是为下面要编写的东西留岀空间。
好的,这样就把水果标题栏和水果内容详情的界面都编写完了,不过我们还可以在界面上再添加一个悬浮按钮。这个悬浮按钮并不是必需的,根据具体的需求添加就可以了,如果加入的话,我们将免费获得一些额外的动画效果。
为了做出示范,我就准备在activity_fruit.xml中加入一个悬浮按钮了。这个界面是一个水果详情展示界面,那么我就加入一个表示评论作用的悬浮按钮吧。首先需要提前准备好一个图标, 这里我放置了一张ic_comment.piig到drawable-xxhdpi目录下。然后修改activity_fruit.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.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">

            <android.support.v7.widget.CardView
                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">

                <TextView
                    android:id="@+id/fruit_content_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="10dp"/>
            </android.support.v7.widget.CardView>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_comment"
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end"/>

</android.support.design.widget.CoordinatorLayout>

可以看到,这里加入了一个 FloatingActionButton,它和 AppBarLayout 以及 NestedScrollView是平级的。FloatingActionButton中使用app: layout_anchor属性指定了一个锚点,我们将锚点设置为AppBarLayout,这样悬浮按钮就会出现在水果标题栏的区域内,接着又使用app:layout_ anchorGravity属性将悬浮按钮定位在标题栏区域的右下角。其他一些属性都比较简单,就不再进行解释了。
好了,现在我们终于将整个activity_fruit.xml布局都编写完了,内容虽然比较长,但由于是分段编写的,并且每一步我都进行了详细的说明,相信你应该看得很明白吧。
界面完成了之后,接下来我们开始编写功能逻辑,修改FruitActivity中的代码,如下所示:

package com.example.materialtest;

import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;

public class FruitActivity extends AppCompatActivity {

    public static final String FRUIT_NAME = "fruit_name";

    public static final String FRUIT_IMAGE_ID = "fruit_image_id";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fruit);
        Intent intent = getIntent();
        String fruitName = intent.getStringExtra(FRUIT_NAME);
        int fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID,0);
        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
        CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout)findViewById(R.id.collapsing_toolbar);
        ImageView fruitImageView = (ImageView)findViewById(R.id.fruit_image_view);
        TextView fruitContentText = (TextView)findViewById(R.id.fruit_content_text);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        collapsingToolbarLayout.setTitle(fruitName);
        Glide.with(this).load(fruitImageId).into(fruitImageView);
        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(MenuItem item){
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

FruitActivity中的代码并不是很复杂。首先,在onCreate()方法中,我们通过Intent获取到传入的水果名和水果图片的资源id,然后通过findViewByld()方法拿到刚才在布局文件中定义的各个控件的实例。接着就是使用了 Toolbar的标准用法,将它作为ActionBar显示,并启用HomeAsUp按钮。由于HomeAsUp按钮的默认图标就是一个返回箭头,这正是我们所期望的,因此就不用再额外设置别的图标了。
接下来开始填充界面上的内容,调用CollapsingToolbarLayout的setTitle()方法将水果名设置成当前界面的标题,然后使用Glide加载传入的水果图片,并设置到标题栏的ImageView上面。接着需要填充水果的内容详情,由于这只是一个示例程序,并不需要什么真实的数据,所以我使用了一个generateFruitContent()方法将水果名循环拼接500次,从而生成了一个比较长 的字符串,将它设置到了TextView上面。
最后,我们在onOptionsItemSelected()方法中处理了 HomeAsUp按钮的点击事件,当点击了这个按钮时,就调用finish()方法关闭当前的活动,从而返回上一个活动。
所有工作都完成了吗?其实还差最关键的一步,就是处理RecyclerView的点击事件,不然的话我们根本就无法打开FruitActivity。修改FruitAdapter中的代码,如下所示:

package com.example.materialtest;

import android.content.Context;
import android.content.Intent;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;

import java.util.List;

/**
 * Created by Administrator on 2018-10-18.
 */

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 = (ImageView)view.findViewById(R.id.fruit_image);
            fruitName = (TextView)view.findViewById(R.id.fruit_name);
        }
    }

    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }

    @Override
    public ViewHolder onCreateViewHolder(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 v) {
                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;
    }

    @Override
    public void onBindViewHolder(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();
    }
}

最关键的一步其实也是最简单的,这里我们给CardView注册了一个点击事件监听器,然后在点击事件中获取当前点击项的水果名和水果图片资源id,把它们传入到Intent中,最后调用startActivity()方法启动FruitActivity。
见证奇迹的时刻到了,现在重新运行一下程序,并点击界面上的任意一个水果,效果如图所示:
在这里插入图片描述
你没有看错,如此精美的界面就是我们亲手敲出来的。这个界面上的内容分为三部分,水果标题栏、水果内容详情和悬浮按钮,相信你一眼就能将它们区分出来吧。Toolbar和水果背景图完美地融合到了一起,既保证了图片的展示空间,又不影响Toolbar的任何功能,那个向左的箭头就是用来返回上一个活动的。
不过这并不是全部,真正的好戏还在后头。我们尝试向上拖动水果内容详情,你会发现水果背景图上的标题会慢慢缩小,并且背景图会产生一些错位偏移的效果。
这是由于用户想要查看水果的内容详情,此时界面的重点在具体的内容上面,因此标题栏就会自动进行折叠,从而节省屏幕空间。
继续向上拖动,直到标题栏变成完全折叠状态。
可以看到,标题栏的背景图片不见了,悬浮按钮也自动消失了,现在水果标题栏变成了一个 最普通的Toolbar。这是由于用户正在阅读具体的内容,我们需要给他们提供最充分的阅读空间。 而如果这个时候向下拖动水果内容详情,就会执行一个完全相反的动画过程。
不知道你有没有被这个效果所感动呢?在这里,我真心地感谢Material Design送给我们的礼物。

7.2 充分利用系统状态栏空间

虽说现在水果详情展示界面的效果已经非常华丽了,但这并不代表我们不能再进一步地提升。观察一下,你会发现水果的背景图片和系统的状态栏总有一些不搭的感觉,如果我们能将背景图和状态栏融合到一起,那这个视觉体验绝对能提升好几个档次。
只不过很可惜的是,在Android 5.0系统之前,我们是无法对状态栏的背景或颜色进行操作的,那个时候也还没有Material Design的概念。但是Android 5.0及之后的系统都是支持这个功能的,因此这里我们就来实现一个系统差异型的效果,在Android 5.0及之后的系统中,使用背景图和状态栏融合的模式,在之前的系统中使用普通的模式。
想要让背景图能够和系统状态栏融合,需要借助android: fitsSystemWindows这个属性来实现。在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout这种嵌套结构的布局中,将控件的android:fitsSystemWindows属性指定成true,就表示该控件会出现在系统状态栏里。对应到我们的程序,那就是水果标题栏中的ImageView应该设置这个属性了。不过只给ImageView设置这个属性是没有用的,我们必须将ImageView布局结构中的所有父布局都设置上这个属性才可以,修改activity_fruit.xml中的代码,如下所示(已经修改,无须添加)
但是,即使我们将android: fitsSystemWindows属性都设置好了还是没有用的,因为还必须在程序的主题中将状态栏颜色指定成透明色才行。指定成透明色的方法很简单,在主题中将 android: statusBarColor属性的值指定成@android:color/transparent 就可以了。但问题在于,android:statusBarColor这个属性是从API 21,也就是Android 5.0系统开始才有的,之前的系统无法指定这个属性。那么,系统差异型的功能实现就要从这里开始了。
右击res目录—>New—>Directory,创建一个values-v21目录,然后右击values-v21目录一>New Values resource file,创建一个styles.xml文件。接着对这个文件进行编写,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="FruitActivityTheme" parent="AppTheme">
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>

这里我们定义了一个FruitActivityTheme主题,它是专门给FruitActivity使用的。FruitActivityTheme的parent主题是AppTheme,也就是说,它继承了 AppTheme中的所有特性。然后我们在 FruitActivityTheme中将状态栏的颜色指定成透明色,由于values-v21目录是只有Android 5.0及 以上的系统才会去读取的,因此这么声明是没有问题的。
但是Android 5.0之前的系统却无法识别FruitActivityTheme这个主题,因此我们还需要对 values/styles.xml文件进行修改,如下所示:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="FruitActivityTheme" parent="AppTheme"/>

</resources>

可以看到,这里也定义了一个FruitActivityTheme主题,并且parent主题也是AppTheme,但是它的内部是空的。因为Android 5.0之前的系统无法指定状态栏的颜色,因此这里什么都不用做就可以了。
最后,我们还需要让FruitActivity使用这个主题才可以,修改AndroidManifest.xml中的代码, 如下所示:

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

    <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">
        <activity
            android:name=".MainActivity"
            android:label="Fruits">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".FruitActivity"
            android:theme="@style/FruitActivityTheme"></activity>
    </application>

</manifest>

这里使用android:theme属性单独给FruitActivity指定了FruitActivityTheme这个主题,这样我们就大功告成了。
现在只要是在Android 5.0及以上的系统运行该程序,水果详情展示界面的效果就会进行相应的变化。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值