文章

Material Design实战

Material Design是由Google的设计工程师们基于传统优秀的设计原则,结合丰富的创意和科学技术所开发的一套全新的界面设计语言,包含了视觉、运动、互动效果等特性。为了做出表率,Google从Android 5.0系统开始,就将所有内置的应用都使用Material Design风格进行设计。

MaterialDesign实战_1.jpg
MaterialDesign实战_2.jpg

一、Toolbar

Toolbar由AndroidX库提供,对于它的另一个相关控件ActionBar由于设计的原因,被限定只能位于Activity的顶部,从而不能实现Material Design的效果,因此官方现在已经不再建议使用ActionBar了。ToolBar强大之处在于不仅继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件完成一些Material Design效果,下面就来学习一下。

因为我们现在准备使用ToolBar替换ActionBar,所以需要指定一个不带ActionBar的主题,通常有以下两种主题可选:

  1. 深色主题
    Theme.AppCompat.NoActionBar
  2. 浅色主题
    Theme.AppCompat.Light.NoActionBar

1.修改value->theme.xml中默认主题

<resources>
    <style 
        name="Theme.Test" 
        parent="Theme.AppCompat.Light.NoActionBar">
    </style>
</resources>

style中各个颜色的属性如下图所示:
MaterialDesign实战_3.jpg

2.使用ToolBar替代ActionBar

修改activity_main.xml代码:

<FrameLayout 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">
    
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>

因为之前使用了浅色主题,所以ToolBar上面的元素就会自动使用深色系,上面的字都会变成黑色的会很难看。所以为了让ToolBar单独使用深色主题,这里使用android:theme属性将ToolBar主题指定成了深色主题,但是这样又会产生新的问题,就是弹出的菜单项也会变成深色主题,这样就再次变得十分难看了,于是这里又使用app:popupTheme属性单独将弹出的菜单项指定成浅色主题。

接下来修改MainActivity代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(...) {
        ...
        // 将ToolBar实例传入
        setSupportActionBar(toolbar)
    }
}

3.修改标题栏显示文字内容

修改AndroidManifest.xml内容:

<application ...>
    <activity
        android:name=".MainActivity"
        <!-- 设置标题 -->
        android:label="Fruits">
</application>

4.给ToolBar添加action按钮

首先在res文件夹下新建menu文件夹,然后在menu文件夹下新建toolbar.xml文件并添加三个按钮:

<menu ...>
    <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>

app:showAsAction用来指定按钮的显示位置,主要有以下几种值可选:

  1. always
    永远显示在ToolBar中,如果屏幕空间不够则不显示。
  2. ifRoom
    屏幕空间足够的情况下显示在ToolBar中,不够的话就显示在菜单当中当中。
  3. never
    永远显示在菜单当中。

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

5.给Action按钮添加点击事件

修改MainActivity代码:

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        // 加载toolbar.xml菜单文件
        menuInflater.inflate(R.menu.toolbar, menu)
        return true
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // 处理各个按钮点击事件
         when(item.itemId) {
            R.id.backup -> ...
            R.id.delete -> ...
            R.id.settings -> ...
        }
        return true
    }
}

最终展示ToolBar效果图如下所示:
MaterialDesign实战_4.jpg

二、滑动菜单——DrawerLayout

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

DrawerLayout是一个布局,在布局中允许放入两个直接子控件:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。因此我们可以对activity_main.xml中的代码做如下修改:

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- FrameLayout中组件作为主屏幕中显示内容 -->
    <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="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            />
    </FrameLayout>

    <!-- 作为滑动菜单中显示的内容 -->
    <!-- android:layout_gravity必须指定,用于告诉DrawerLayout是在屏幕左边还是右边 -->
    <!-- start表示根据系统语言方向进行判断 -->
    <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>

呈现效果如下图所示:
MaterialDesign实战_5.jpg

1.在Toolbar最左侧加入菜单按钮

class MainActivity : AppCompatActivity() {
    override fun onCreate(...) {
        ...
        // 获得ActionBar实例
        supportActionBar?.let {
            // 让按钮显示出来
            it.setDisplayHomeAsUpEnabled(true)
            // 设置按钮图标
            it.setHomeAsUpIndicator(R.drawable.ic_menu)
        }
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            // 点击后调用openDrawer()方法将滑动菜单显示出来
            android.R.id.home -> 
                drawerLayout.openDrawer(GravityCompat.START)
        }
    }
}

最终呈现效果如下所示:
MaterialDesign实战_6.png

2.NavigationView

我们可以在滑动菜单中定制任意的布局,不过Google给我们提供了一种更好的方法——使用NavigationView。它是Material库中提供的一个控件,它不仅是严格按照Material Design的要求来设计的,而且可以将滑动菜单页面的实现变得非常简单。

使用NavigationView可以轻松实现下图的滑动菜单效果:
MaterialDesign实战_7.jpg

首先,既然这个控件是Material提供的,就需要将这个库引入项目中才行:

dependencies {
    // Material库
    implementation 'com.google.android.material:material:1.1.0'
    // 开源项目,CircleImageView,轻松实现图片圆形化功能
    implementation 'de.hdodenhof:circleimageview:3.0.1'
}

2.1.准备menu

menu是用来在NavigationView中显示具体的菜单项的。

在res中新建menu文件夹,然后右键menu文件夹->New->Menu resource file,创建一个nav_menu.xml文件:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- checkableBehavior="single"表示所有选项只能单选 -->
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/navCall"
            android:icon="@drawable/nav_call"
            android:title="Call" />
        <item
            android:id="@+id/navFriends"
            android:icon="@drawable/nav_friends"
            android:title="Friends" />
        <item
            android:id="@+id/navLocation"
            android:icon="@drawable/nav_location"
            android:title="Location" />
        <item
            android:id="@+id/navMail"
            android:icon="@drawable/nav_mail"
            android:title="Mail" />
        <item
            android:id="@+id/navTask"
            android:icon="@drawable/nav_Task"
            android:title="Tasks" />
    </group>
</menu>

2.2.准备headerLayout

headerLayout是用来在NavigationView中显示头部布局的。

这是一个可以随意定制的布局,我们就在headerLayout中放置头像、用户名、邮箱地址这3项。

在layout文件夹下新建nav_header.xml文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    <!-- 180dp是NavigationView比较合适的高度 -->
    android:layout_height="180dp"
    android:padding="10dp"
    android:background="@color/colorPrimary">
    
    <!-- 显示的图片是圆形 -->
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/iconImage"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@drawable/nav_icon"
        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:textSiz="14sp" />
    <TextView
        android:id="@+id/userText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:above="@id/mailText"
        android:text="Tony Green"
        android:textColor="#FFF"
        android:textSizr="14sp" />
</RelativeLayout>

2.3.修改activity_main.xml

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    ...
    
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        <!-- 设置刚才准备的menu -->
        app:menu="@menu/nav_menu"
        <!-- 设置刚才准备的headerLayout -->
        app:headerLayout="@layout/nav_header" />
        
</androidx.drawerlayout.widget.DrawerLayout>

2.4.设置菜单项点击事件

class MainActivity : AppCompatActivity() {
    override fun onCreate(...) {
        ...
        // 将Call项设为默认选中
        navView.setCheckedItem(R.id.navCall)
        // 设置菜单项选中事件监听器
        navView.setNavigationItemSelectedListener {
            // 将滑动菜单关闭
            drawerLayout.closeDrawers()
            // true表示此事件已被处理
            true
        }
    }
}

三、悬浮按钮和可交互提示

1.悬浮按钮——FloatingActionButton

FloatingActionButton是Material库中提供的一个控件,这个控件可以帮助我们比较轻松实现悬浮按钮的效果,它默认使用colorAccent作为按钮的颜色,我们还可以通过一个图标来表示这个按钮的作用是什么。

修改activity_main.xml:

<androidx.drawerlayout.widget.DrawerLayout ... >
    <FrameLayout ...>
        ...
        <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_done"
            <!-- 指定悬浮高度/阴影大小 -->
            app:elevation="8dp" />
    </FrameLayout>
    ...
</androidx.drawerlayout.widget.DrawerLayout>

按钮的点击事件和普通按钮一致:

fab.setOnClickListener {
    // 处理点击事件
}

2.可交互提示——Snackbar

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

修改MainActivity代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(...) {
        ...
        fab.setOnClickListener { view ->
            // 传入任意布局view即可
            Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
                .setAction("Undo") {
                    // 处理Snackbar上面的按钮点击事件
                }
                .show()
        }
    }
}

这里还会存在一个问题,Snackbar显示的时候可能会把FloatingActionButton遮挡住,想要解决这个问题,就要引入下一个知识点了。

四、提供Material的布局——CoordinatorLayout

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

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

CoordinatorLayout的使用也非常简单,只需要将原来的FrameLayout替换一下就可以了,现在修改activity_main.xml:

<androidx.drawerlayout.widget.DrawerLayout ...>
    
    <!-- 替换原来的FrameLayout -->
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <androidx.appcompat.widget.Toolbar ...>
        <com.google.android.material.floatingactionbutton.FloatingActionButton ...>
   
     </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
</androidx.drawerlayout.widget.DrawerLayout>

由于CoordinatorLayout是一个加强版的FrameLayout,因此这种替换不会有任何的副作用。现在再次触发Snackbar显示,悬浮按钮就会向上偏移了Snackbar的同等高度,从而确保不会被完全遮挡,当Snackbar消失的时候,悬浮按钮会自动向下偏移到原来的位置。

五、卡片式布局

这节中我们学习如何实现卡片式布局的效果。卡片式布局也是Materials Design中提出的一个新概念,它可以让页面中的元素看起来就像在卡片中一样,并且还能拥有圆角和投影,类似下图:
MaterialDesign实战_8.jpg

1.MaterialCardView基本用法

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

先来看一下MaterialCardView的基本用法:

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    <!-- 指定卡片圆角弧度 -->
    app:cardCornerRadius="4dp"
    <!-- 指定卡片投影高度/阴影大小 -->
    app:elevation="5dp">
    
    <!-- 在MaterialCardView中放置一个TextView -->
    <!-- 这个TextView就会显示在一张卡片中 -->
    <TextView
        android:id="@+id/infoText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
        
</com.google.android.material.card.MaterialCardView>

2.利用RecyclerView实现图片中效果

首先要载入RecyclerView依赖:

dependencies {
    implementation "androidx.recyclerview:recyclerview:1.1.0"
    // Glide开源库,用于加载图片到ImageView中等
    // 可以加载网络图片、GIF、甚至是本地视频
    implementation "com.github.bumptech:glide:glide:4.9.0"
}

2.1.修改activity_main.xml代码

<androidx.drawerlayout.widget.DrawerLayout ...>
    <androidx.coordinatorlayout.widget.CoordinatorLayout ...>
        ...
        <!-- 在CoordinatorLayout中添加RecyclerView -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        ...
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    ...
</androidx.drawerlayout.widget.DrawerLayout>

2.2.定义实体类Fruit

class Fruit(val name: String, val imageId: Int)

2.3.给RecyclerView指定自定义布局

在layout目录下新建fruit_item.xml:

<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="wrap_content"
            android:layout_height="100dp"
            <!-- 指定图片缩放模式 -->
            <!-- centerCrop让图片保持原有比例填充满ImageView -->
            <!-- 超出的部分裁切掉 -->
            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>

2.4.给RecyclerView准备适配器

class FruitAdapter(val context: Context, val fruitList: List<Fruit>) : 
        RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
            
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitName.text = fruit.name
        // with()传入Context、Activity或者Fragment参数
        // 调用load()加载图片,可以是URL、本地路径或是资源id
        // 最后调用into()将图片设置到具体某一个ImageView中
        Glide.with(context).load(fruit.imageId).into(holder.fruitImage)
    }

    override fun getItemCount() = fruitList.size
}

2.5.修改MainActivity代码

class MainActivity : AppCompatActivity() {
    private val fruits = mutableListOf(Fruit("Apple", R.drawable.apple), Fruit("Banana", R.drawable.banana),
        Fruit("Orange", R.drawable.orange), Fruit("Watermelon", R.drawable.watermelon),
        Fruit("Pear", R.drawable.pear), Fruit("Grape", R.drawable.grape),
        Fruit("Pineapple", R.drawable.pineapple), Fruit("Strawberry", R.drawable.strawberry),
        Fruit("Cherry", R.drawable.cherry), Fruit("Mango", R.drawable.mango)
    )
    val fruitList = ArrayList<Fruit>()
    
    override fun onCreate(...) {
        ...
        initFruits()
        // 网格布局,传入Context和列数
        val layoutManager = GridLayoutManager(this, 2)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(this, fruitList)
        recyclerView.adapter = adapter
    }
    
    // 随机向fruitList中添加50个Fruit实例
    private fun initFruits() {
        fruitList.clear()
        repeat(50) {
            val index = (0 until fruits.size).random()
            fruitList.add(fruits[index])
        }
    }
    ...
}

现在运行软件就可以看到类似于一开始的图片中的卡片布局的效果,但是有一点不一样的是Toolbar被RecyclerView给挡住了,如果想要解决这个问题,就需要引入下面一个知识点——AppBarLayout。

六、AppBarLayout

首先来分析一下为什么RecyclerView会把Toolbar给挡住呢,由于RecyclerView和Toolbar都是放在CoordinatorLayout中的,前面已近说过,CoordinatorLayout就是一个加强版的FrameLayout,FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而产生遮挡现象。

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

那么我们要怎样使用AppBarLayout来解决前面的遮挡问题呢,只需两步即可,第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局文件,修改activity_main.xml代码:

<androidx.drawerlayout.widget.DrawerLayout ...>
    <androidx.coordinatorlayout.widget.CoordinatorLayout ...>
        <!-- 将Toolbar嵌套在AppBarLayout中 -->
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
                <androidx.appcompat.widget.Toolbar ...>
        </com.google.android.material.appbar.AppBarLayout>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            <!-- 指定RecyclerView布局行为 -->
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
        ...
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    ...
</androidx.drawerlayout.widget.DrawerLayout>

现在再次运行程序,可以发现一切都正常了!下面来进一步优化,实现一些Material Design效果。

当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就可以实现,修改activity_main.xml中的代码:

...
<androidx.appcompat.widget.Toolbar
    ...
    <!-- scroll表示RecyclerView向上滚动时Toolbar一起向上滚动并隐藏 -->
    <!-- enterAlways表示向下滚动时Toolbar会一起向下滚动并重新显示 -->
    <!-- snap表示Toolbar还没完全隐藏或者显示时,根据当前滚动距离—— -->
    <!-- ——自动选择隐藏还是显示 -->
    app:layout_scrollFlags="scroll|enterAlways|snap" />
...

现在重新运行程序并向上滚动RecyclerView可以看到非常美观的Material Design效果!

七、下拉刷新——SwipeRefreshLayout

SwipeRefreshLayout是用于实现下拉刷新功能的核心类,它是由Androidx库提供,我们只需要把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。在上面的项目中,应该支持下拉刷新的自然就是RecyclerView了。

1.修改activity_main.xml代码

...
<!-- 使用SwipeRefreshLayout包住RecyclerView -->
<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 .../>
    
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

2.实现刷新逻辑
修改MainActivity代码:

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(...) {
        ...
        // 设置下拉刷新进度条颜色
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
        // 设置下拉刷新监听器
        swipeRefresh.setOnRefreshListener {
            // 刷新RecyclerView内容
            refreshFruits(adapter)
        }
    }
    
    private fun refreshFruits(adapter: FruitAdapter) {
        thread {
            // 为了不让下拉刷新的进度条一下就消失,这里阻塞线程2秒
            Thread.sleep(2000)
            runOnUiThread {
                // 随机给RecyclerView设置50个Fruit实例
                initFruits()
                // 通知RecyclerView的adapter数据发生变化
                adapter.notifyDataSetChanged()
                // 传入false表示刷新事件结束,并隐藏进度条
                swipeRefresh.isRefreshing = false
            }
        }
    }
}

八、可折叠式标题栏

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

我们将会使用CollapsingToolbarLayout实现如下图所示效果:
MaterialDesign实战_9.jpg

1.CollapsingToolbarLayout

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

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

1.1.创建新Activity并编写activity_main.xml

右键包名 -> New -> Activity -> Empty Activity新建一个FruitActivity,然后编写activity_fruit.xml中的代码:

<!-- CoordinatorLayout作为最外层布局 -->
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    <!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
    android:fitsSystemWindows="true"
    tools:context=".FruitActivity">

    <!-- CoordinatorLayout中嵌套AppBarLayout -->
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        <!-- 高度250dp视觉效果较好,不固定 -->
        android:layout_height="250dp"
        <!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
        android:fitsSystemWindows="true">

        <!-- 在AppBarLayout中嵌套CollapsingToolbarLayout -->
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            <!-- 和之前的ToolBar相同的主题,原因相同 -->
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            <!-- 用于指定CollapsingToolbarLayout在趋于折叠状态—— -->
            <!-- ——以及折叠之后的背景色,折叠后其实就是普通的Toolbar -->
            app:contentScrim="@color/purple_500"
            <!-- scroll表示CollapsingToolbarLayout随着水果—— -->
            <!-- ——内容详情的滚动一起滚动 -->
            <!-- exitUntilCollapsed表示随着滚动完成折叠之后就—— -->
            <!-- ——保留在界面上,不再移出屏幕 -->
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            <!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
            android:fitsSystemWindows="true">

            <!-- 定义标题栏具体内容ImageView和Toolbar -->
            <!-- 意味着这个高级版的标题栏是由普通标题栏和图片组合而成 -->
            <ImageView
                android:id="@+id/fruitImageView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                <!-- 指定当前控件在CollapsingToolbarLayout—— -->
                <!-- ——的折叠过程中的折叠模式 -->
                <!-- parallax表示在折叠过程中产生一定错位偏移 -->
                app:layout_collapseMode="parallax"
                <!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
                android:fitsSystemWindows="true"
                />
            <androidx.appcompat.widget.Toolbar
                android:id="@id/toolBar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                <!-- pin表示折叠过程中始终位置不变 -->
                app:layout_collapseMode="pin" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>
    
    <!-- ***标题栏布局编写完成*** -->
    </com.google.android.material.appbar.AppBarLayout>


    <!-- ***下面编写水果内容详情部分*** -->
    <!-- NestedScrollView在ScrollView基础上—— -->
    <!-- ——增加嵌套响应滚动事件功能 -->
    <!-- 由于CollapsingToolbarLayout本身可以响应滚动事件 -->
    <!-- 所以它的内部就需要使用NestedScrollView或RecyclerView这样的布局 -->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        <!-- 指定布局行为 -->
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        
        <!-- 不管是NestedScrollView还是ScrollView -->
        <!-- 它们的内部只允许一个直接子布局 -->
        <!-- 所以通畅先嵌套一个LinearLayout -->
        <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">
                
                <!-- 显示水果详情内容 -->
                <TextView
                    android:id="@+id/fruitContentText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="10dp"
                    />
            </com.google.android.material.card.MaterialCardView>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

    <!-- 悬浮按钮 -->
    <!-- 它与AppBarLayout和NestedScrollView是平级的 -->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_comment"
        <!-- 指定锚点 appBar -->
        <!-- 这样悬浮按钮就会出现在标题栏(appBar)区域内 -->
        app:layout_anchor="@id/appBar"
        <!-- 指定悬浮按钮在标题栏区域右下角 -->
        app:layout_anchorGravity="bottom|end"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

1.2.修改Activity代码,编写逻辑功能

修改FruitActivity中代码:

class FruitActivity : AppCompatActivity() {
    companion object {
        const val FRUIT_NAME = "fruit_name"
        const val FRUIT_IMAGE_ID = "fruit_image_id"
    }
    
    override fun onCreate(...) {
        super.onCreate(...)
        setContentView(R.layout.activity_fruit)
        // 获取传入的水果名和图片id
        val fruitName = intent.getStringExtra(FRUIT_NAME) ?: ""
        val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
        // 将toolbar替换ActionBar
        setSupportActionBar(toolbar)
        // 显示返回按钮
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        // 设置页面标题
        collapsingToolbar.title = fruitName
        // 将图片加载到ImageView
        Glide.with(this).load(fruitImageId).into(fruitImageView)
        // 设置水果内容详情,水果名重复500遍
        fruitContentText.text = generateFruitContent(fruitName)
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> {
                // 点击返回按钮关闭当前页面
                finish()
                return true
            }
        }
        return super.onOptionsItemSelected(item)
    }
    
    private fun generateFruitContent(fruitName: String) = 
            fruitName.repeat(500)
}

1.3.设置RecyclerView点击事件

修改FruitAdapter代码:

class FruitAdapter(val context: Context, val fruitList: List<Fruit>) :
        RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
    ...
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) :
            ViewHolder {
        val view = LayoutInflater.from(context).iflate(R.layout.fruit_item,
                parent, false)
        val holder = ViewHolder(view)
        holder.itemView.setOnClickListener {
            val position = holder.adapterPosition
            val fruit = fruitList[position]
            val intent = Intent(context, FruitActivity::class.java).apply {
                putExtra(FruitActivity.FRUIT_NAME, fruit.name)
                putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.imageId)
            }
            context.startActivity(intent)
        }
        return holder  
    }
    ...
}

现在重新运行程序,并点击卡片布局中的水果卡片,即可进入FruitActivity,此时上下滑动页面,可以看到很Material的UI体验!

1.4.充分利用状态栏空间

虽说现在水果详情展示界面效果已经非常华丽了,但是可以发现水果的背景图片和系统的状态栏总有一些不搭的感觉,如果能将背景图和状态栏融合到一起,这个视觉体验绝对提升好几个档次。

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

1.4.1.设置主题透明色

然后还需要在程序的主题中将状态栏颜色指定成透明色才行。在主题中将android:statusBarColor属性的值指定成**@android:color/transparent**就可以了。

打开res/values/theme.xml文件,对主题内容进行修改:

<resources>
    ...
    <style name="FruitActivityTheme" parent="Theme.MaterialTest">
        <item name="android:statusBarColor">
            @android:color/transparent
        </item>
    </style>
</resources>

1.4.2.应用主题

修改AndroidManifest.xml代码:

<manifest ...>
    <application ...>
        <activity
            android:name=".FruitActivity"
            android:theme="@style/FruitActivityTheme">
        </activity>
    </application>
</manifest>

License:  CC BY 4.0