12.5--卡片式布局

虽然现在 MaterialTest 中已经应用了非常多的 Material Design 效果,不过你会发现,界面上最主要的一块区域还处于空白状态。这块区域通常都是用来放置应用的主体内容的,我准备使用些精美的水果图片来填充这部分区域。

那么为了要让水果图片也能 Material 化,本节中我们将会学习如何实现卡片式布局的效果。卡片式布局也是 Material Design 中提出的一个新的概念,它可以让页面中的元素看起来就像在卡片中一样,并且还能拥有圆角和投影,下面我们就开始具体学习ー下。

 

12.5.1 MaterialCardView

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

我们先来看一下MaterialCardView 的基本用法吧,其实非常简单,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="4dp"
    android:elevation="5dp"
    >
    <TextView
        android:id="@+id/infoText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />


</com.google.android.material.card.MaterialCardView>

这里定义了一个 Cardview 布局,我们可以通过 app: cardCornerRadius 属性指定卡片圆角的弧度,数值越大,圆角的弧度也越大。另外还可以通过 app:elevation 属性指定卡片的高度高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓,这一点和 FloatingActionButton 是一致的。

然后我们在 MaterialCardView 布局中放置了一个 TextView,那么这个 TextView 就会显示在一张卡片当中了,MaterialCardView 的用法就是这么简单。

但是我们显然不可能在如此宽阔的一块空白区域内只放置一张卡片,为了能够充分利用屏幕的空间,这里我准备综合运用一下第4章中学到的知识,使用 RecyclerView 来填充 MaterialTest 项目的主界面部分。还记得之前实现过的水果列表效果吗?这次我们将升级一下,实现一个高配版的水果列表效果。

既然是要实现水果列表,那么首先肯定需要准备许多张水果图片,这里我从网上挑选了一些精美的水果图片,将它们复制到了项目当中。

然后由于我们还需要用到 Recycler View,因此必须在 app/build.gradle 文件中声明库的依赖:

dependencies {
    ...
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'com.github.bumptech.glide:glide:4.9.0'

}

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

接下来开始具体的代码实现,修改 activity_main.xml 中的代码,如下所示:

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

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

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    ...



</androidx.drawerlayout.widget.DrawerLayout>

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

接着定义一个实体类 Fruit,代码如下所示:

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

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

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

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

</com.google.android.material.card.MaterialCardView>

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

内容倒也没有什么特殊的地方,就是定义了一个 ImageView 用于显示水果的图片,又定义了 TextView 用于显示水果的名称,并让 TextView 在水平方向上居中显示。注意在 ImageView 中我们使用了一个 scaleType 属性,这个属性可以指定图片的缩放模式。由于各张水果图片的长宽比例可能都不一致,为了让所有的图片都能填充满整个 ImageView,这里使用了 centerCrop 模式,它可以让图片保持原有比例填充满 ImageView,并将超出屏幕的部分裁剪掉

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

class FruitAdapter(val context:Context,val fruitList:List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
    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): FruitAdapter.ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.fruit_item,parent,false)
        return ViewHolder(view)
    }

    override fun getItemCount(): Int = fruitList.size

    override fun onBindViewHolder(holder: FruitAdapter.ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitName.text = fruit.name
        Glide.with(context).load(fruit.imageId).into(holder.fruitImage)
    }
}

上述代码相信你一定很熟悉,和我们在第 3 章中编写的 FruitAdapter 几乎一模一样。唯一需要注意的是,在 onBindViewHo lder() 方法中我们使用了 Glide 来加载水果图片。

那么这里就顺便来看一下 Glide 的用法吧,其实并没有太多好讲的,因为 Glide 的用法实在是太简单了。首先调用 Glide.with() 方法并传入一个 Context、Actlvity 或 Fragment 参数然后调用 load()方法去加载图片,可以是一个 URL 地址,也可以是一个本地路径,或者是一个资源id,最后调用 into()方法将图片设置到具体某一个 ImageView 中就可以了。

那么我们为什么要使用 Glide 而不是传统的设置图片方式呢?因为这次我从网上找的这些水果图片像素都非常高,如果不进行压缩就直接展示的话,很容易就会引起内存溢出。而使用 Glide 就完全不需要担心这回事,因为 Glide 在内部做了许多非常复杂的逻辑操作,其中就包括了图片压缩,我们只需要安心按照 Glide 的标准用法去加载图片就可以了。

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

class MainActivity : AppCompatActivity() {
    val fruits = mutableListOf(Fruit("Apple",R.drawable.apple),
        Fruit("Banana",R.drawable.banana),
        Fruit("Orange",R.drawable.orange),
        Fruit("Watermelon",R.drawable.watermelon),
        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(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        initFruits()
        val layoutManager = GridLayoutManager(this,2)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(this,fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits(){
        fruitList.clear()
        repeat(50){
            val index = (0 until fruits.size).random()
            fruitList.add(fruits[index])
        }
    }
    ...
}

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

之后的用法就是 RecyclerView 的标准用法了,不过这里使用了 GridLayoutManager 这种布局方式。在第 4 章中我们已经学过了 LinearLayoutManager 和 StaggeredGridLayoutManager,现在终于将所有的布局方式都补齐了。GridLayoutManager 的用法也没有什么特别之处,它的构造函数接收两个参数,第一个是 Context,第二个是列数,这里我们希望每一行中会有两列数据。

现在重新运行一下程序,效果如图所示:

可以看到,精美的水果图片成功展示出来了。每个水果都是在一张单独的卡片当中的,并且还拥有圆角和投影,是不是非常美观?另外,由于我们是使用随机的方式来获取水果数据的,因此界面上会有一些重复的水果出现,这属于正常现象。

当你陶醉于当前精美的界面的时候,你是不是忽略了一个细节?哎呀,我们的 Toolbar 怎不见了!仔细观察一下原来是被 RecyclerView 给挡住了。这个问题又该怎么解决呢?这就需要借助到另外一个工具了——AppBarLayout。

 

12.5.2 AppBarLayout

首先我们来分析一下为什么 RecyclerView 会把 Toolbar 给遮挡住吧。其实并不难理解,由于 RecyclerView 和 Toolbar 都是放置在 CoordinatorLayout 中的,而前面已经说过,CoordinatorLayout 就是一个加强版的 FrameLayout,那么 FrameLayout 中的所有控件在不进行明确定位的情况下默认都会摆放在布局的左上角,从而也就产生了遮挡的现象。其实这已经不是你第一次遇到这种情况了,我们在 4.3.3 小节学习 FrameLayout  的时候就早已见识过了控件与控件之间遮挡的效果。

既然已经找到了问题的原因,那么该如何解决呢?传统情况下,使用偏移是唯一的解决办法,即让 RecyclerView 向下偏移一个 Toolbar 的高度,从而保证不会遮挡到 Toolbar。不过我们使用的并不是普通的 FrameLayout,而是 CoordinatorLayout,因此自然会有一些更加巧妙的解决办法。

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

那么我们怎样使用 AppBarLayoutオ能解决前面的覆盖问题呢?其实只需要两步就可以了第一步将 Toolbar 嵌套到 AppBarLayout 中,第二步给 RecyclerView 指定一个布局行为。修改 activity_main.xml 中的代码,如下所示:

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

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/colorPrimary"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                />
        </com.google.android.material.appbar.AppBarLayout>
        
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            />
        <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"
            />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    ...


</androidx.drawerlayout.widget.DrawerLayout>

可以看到,布局文件并没有什么太大的变化。我们首先定义了一个 AppBarLayout,并将 Toolbar 放置在了 AppBarLayout 里面,然后在 RecyclerView 中使用 app:layout_behavior 属性指定了一个布局行为。其中 appbar_scrolling_view_behavior 这个字符串也是由 Material库提供的

现在重新运行一下程序,你就会发现一切都正常了,如图所示。

虽说使用 AppBarLayout 已经成功解决了 RecyclerView 遮挡 Toolbar 的问题,但是刚才有提到过,说 AppBarLayout 中应用了一些 Material Design 的设计理念,好像从上面的例子完全体现不出来呀。事实上,当 RecyclerView 滚动的时候就已经将滚动事件都通知给 AppBarLayout 了,只是我们还没进行处理而已。那么下面就让我们来进一步优化,看看 AppBarLayout 到底能实现什么样的 Material Design 效果。

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

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

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/colorPrimary"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                />
        </com.google.android.material.appbar.AppBarLayout>

       ...

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
   ...



</androidx.drawerlayout.widget.DrawerLayout>

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

我们要改动的就只有这一行代码而已,现在重新运行一下程序,并向上滚动 RecyclerView 效果如图所示。

可以看到,随着我们向上滚动 RecyclerView, Toolbar 竟然消失了,而向下滚动 RecyclerView, Toolbar 又会重新出现。这其实也是 Material Design 中的一项重要设计思想,因为当用户在向上滚动 RecyclerView 的时候,其注意力肯定是在 RecyclerView 的内容上面的,这个时候如果 Toolbar 还占据着屏幕空间,就会在一定程度上影响用户的阅读体验,而将 Toolbar 隐藏则可以让阅读体验达到最佳状态。当用户需要操作 Toolbar 上的功能时,只需要轻微向下滚动,Toolbar 就会重新出现。这种设计方式,既保证了用户的最佳阅读效果,又不影响任何功能上的操作,Material Design 考虑得就是这么细致人微。

当然了,像这种功能,如果是使用 Actionbar 的话,那就完全不可能实现了,To0bar 的出现为我们提供了更多的可能。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值