美文网首页
android之Meterial Design

android之Meterial Design

作者: 0246eafe46bd | 来源:发表于2021-12-11 23:04 被阅读0次

Toolbar

去掉ActionBar,使用Toolbar

Toolbar继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件完成一些MaterialDesign的效果。新建的项目,默认都是会显示ActionBar的,在AndroidManifest.xml文件里的application标签的android:theme属性配置的,如下

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_config"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.AndroidStudy">
    ......

android:theme属性的值@style/Theme.AndroidStudy在文件res/values/styles.xml文件中配置,如下

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.AndroidStudy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ......
    </style>
</resources>

可以看到Theme.AndroidStudy 的 parent是Theme.MaterialComponents.DayNight.DarkActionBar,而如果想要使用Toolbar来替代ActionBar,就需要指定一个不带ActionBar的主题,例如:Theme.MaterialComponents.Light.NoActionBar

使用Toolbar来替代ActionBar,修改 activity_meterial.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".meterial_study.MeterialActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/purple_500"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</androidx.constraintlayout.widget.ConstraintLayout>

由于我们刚才在styles.xml中将程序的主题指定成了浅色主题,因此Toolbar现在也是浅色主题,那么Toolbar上面的各种元素就会自动使用深色系,从而和主体颜色区别开。那么能让Toolbar单独使用深色主题,这里我们使用了android:theme属性,将Toolbar的主题指定成了ThemeOverlay.AppCompat.Dark.ActionBar。但是这样指定之后又会出现新的问题,如果Toolbar中有菜单按钮,那么弹出的菜单项也会变成深色主题,于是这里又使用了app:popupTheme属性,单独将弹出的菜单项指定成了浅色主题
修改 MeterialActivity

class MeterialActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meterial)
        // 调用setSupportActionBar()方法并将Toolbar的实例传入
        setSupportActionBar(toolbar)
    }
}

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

在 AndroidManifest.xml 中的 activity 标签下的 android:label 属性设置,如果没有指定的话,会默认使用application中指定的label内容,也就是应用名称

<activity
    android:name=".meterial_study.MeterialActivity"
    android:exported="true"
    android:label="Toolbar Test">

使用action按钮丰富Toolbar

创建 res/menu/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">
    <!--android:id给这个菜单项指定一个唯一的标识符,
    android:title给这个菜单项指定一个名称
    android:icon用于指定按钮的图标-->
    <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 来指定按钮的显示位置,主要有以下几种值可选:

always表示永远显示在Toolbar中,如果屏幕空间不够则不显示;ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话就显示在菜单当中;never则表示永远显示在菜单当中。

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

修改MeterialActivity

class MeterialActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meterial)
        // 调用setSupportActionBar()方法并将Toolbar的实例传入
        setSupportActionBar(toolbar)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        // 加载了toolbar.xml这个菜单文件
        menuInflater.inflate(R.menu.toolbar, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // 处理各个按钮的点击事件
        when (item.itemId) {
            R.id.backup -> Toast.makeText(this, "backup clicked", Toast.LENGTH_SHORT).show()
            R.id.delete -> Toast.makeText(this, "delete clicked", Toast.LENGTH_SHORT).show()
            R.id.settings -> Toast.makeText(this, "settings clicked", Toast.LENGTH_SHORT).show()
        }
        return super.onOptionsItemSelected(item)
    }
}

效果


image-20211212142047474.png

滑动菜单

DrawerLayout

DrawerLayout简单用法

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

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

修改 activity_meterial.xml'

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

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <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/purple_500"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </FrameLayout>

    <!--DrawerLayout的第而个子控件,作为滑动菜单中显示的内容-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#ffffff"
        android:text="this is menu"
        android:textSize="30sp" />

</androidx.drawerlayout.widget.DrawerLayout>

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

效果

image-20211212143411437.png

为 DrawerLayout 添加导航按钮

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

class MeterialActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meterial)
        // 调用setSupportActionBar()方法并将Toolbar的实例传入
        setSupportActionBar(toolbar)
        // 添加DrawerLayout的导航按钮
        supportActionBar?.apply {
            setDisplayHomeAsUpEnabled(true)
            setHomeAsUpIndicator(R.drawable.ic_menu)
        }
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        // 加载了toolbar.xml这个菜单文件
        menuInflater.inflate(R.menu.toolbar, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // 处理各个按钮的点击事件
        when (item.itemId) {
            R.id.backup -> Toast.makeText(this, "backup clicked", Toast.LENGTH_SHORT).show()
            R.id.delete -> Toast.makeText(this, "delete clicked", Toast.LENGTH_SHORT).show()
            R.id.settings -> Toast.makeText(this, "settings clicked", Toast.LENGTH_SHORT).show()
            // 处理DrawerLayout导航按钮点击事件
            android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
        }
        return super.onOptionsItemSelected(item)
    }
}

调用getSupportActionBar()方法得到了ActionBar的实例,虽然这个ActionBar的具体实现是由Toolbar来完成的,调用setDisplayHomeAsUpEnabled()方法让导航按钮显示出来,调用setHomeAsUpIndicator()方法来设置一个导航按钮图标。

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

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

NavigationView

使用NavigationView可以将滑动菜单页面的实现变得简单

添加依赖

// Material库
implementation "com.google.android.material:material:1.1.0"

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

menu 和 headerLayout

NavigationView需要准备两个东西:menu和headerLayout。menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在NavigationView中显示头部布局的

nav_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <!--checkableBehavior指定为single表示组中的所有菜单项只能单选-->
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_call"
            android:icon="@drawable/nav_call"
            android:title="Call" />
        <item
            android:id="@+id/nav_friends"
            android:icon="@drawable/nav_friends"
            android:title="Friends" />
        <item
            android:id="@+id/nav_location"
            android:icon="@drawable/nav_location"
            android:title="Location" />
        <item
            android:id="@+id/nav_mail"
            android:icon="@drawable/nav_mail"
            android:title="Friends" />
        <item
            android:id="@+id/nav_task"
            android:icon="@drawable/nav_task"
            android:title="Task" />
    </group>
</menu>

nav_header.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:padding="10dp"
    android:layout_height="180dp"
    android:background="@color/purple_500">

    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/icon_image"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@drawable/nav_icon"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:shapeAppearanceOverlay="@style/round_style" />

    <TextView
        android:id="@+id/mail_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="123456@gmail.com"
        android:textColor="#ffffff"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />

    <TextView
        android:id="@+id/user_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Tony"
        android:textColor="#ffffff"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@+id/mail_text"
        app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

圆角控件ShapeableImageView

上面使用 ShapeableImageView 为 icon_image 设置了圆角,也可以设置为其他形状,ShapeableImageView 也是Material 的一个控件,需要添加Material 依赖,前面已经添加过了

使用方法

  1. 添加依赖
  2. 在 values/style.xml 中创建style
  3. 布局文件中使用ShapeableImageView,并设置shapeAppearanceOverlay或shapeAppearance指向style,也可以使用strokeColo指定描边颜色,strokeWidth指定描边宽度, elevation和padding搭配做出阴影
在 values/style.xml 中创建style
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 圆角图片Style -->
    <style name="round_style">
        <item name="cornerSize">35dp</item>
        <!--也可以用下面的逐个设置圆角,但要注意不是cornerRadius属性-->
        <!--<item name="cornerSizeTopLeft">35dp</item>
        <item name="cornerSizeTopRight">35dp</item>
        <item name="cornerSizeBottomRight">35dp</item>
        <item name="cornerSizeBottomLeft">35dp</item>-->
    </style>
</resources>
布局文件中使用ShapeableImageView,并使用style
<com.google.android.material.imageview.ShapeableImageView
    android:id="@+id/icon_image"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:src="@drawable/nav_icon"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:shapeAppearanceOverlay="@style/round_style" />

使用 NavigationView

menu和headerLayout准备好了,可以使用NavigationView了,修改activity_main.xml中的代码

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

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <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/purple_500"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </FrameLayout>

    <!--DrawerLayout的第而个子控件,作为滑动菜单中显示的内容-->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:text="this is menu"
        android:textSize="30sp"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_menu" />

</androidx.drawerlayout.widget.DrawerLayout>

修改 MeterialActivity

......
// 将Call菜单项设置为默认选中
nav_view.setCheckedItem(R.id.nav_call)

nav_view.setNavigationItemSelectedListener { item ->
    // 处理NavigationView菜单项的点击事件
    when (item.itemId) {
        R.id.nav_call -> Toast.makeText(this, "call clicked", Toast.LENGTH_SHORT).show()
        R.id.nav_friends-> Toast.makeText(this, "friends clicked", Toast.LENGTH_SHORT).show()
        R.id.nav_location -> Toast.makeText(this, "location clicked", Toast.LENGTH_SHORT).show()
        R.id.nav_mail -> Toast.makeText(this, "mail clicked", Toast.LENGTH_SHORT).show()
        R.id.nav_task -> Toast.makeText(this, "task clicked", Toast.LENGTH_SHORT).show()
    }
    drawerLayout.closeDrawers()
    true
}
......

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

效果

image-20211212153929006.png

悬浮按钮 FloatingActionButton

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

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        ......
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/floating_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="20dp"
            android:elevation="8dp"
            android:src="@drawable/ic_done"
            app:backgroundTint="#ff0000"
            app:rippleColor="#ffffff"
            app:tint="#00ff00" />
    </FrameLayout>
......

</androidx.drawerlayout.widget.DrawerLayout>

app:elevation:给FloatingActionButton指定一个高度值。高度值越大,投影范围也越大,但是投影效果越淡;高度值越小,投影范围也越小,但是投影效果越浓

app:backgroundTint:设置背景色,如下面效果图的红色
app:rippleColor:设置点击时候的上面的蒙版颜色
app:tint:设置图标颜色,如下面效果图的绿色,要注意不是android:tint

同样使用setOnClickListener()方法来设置按钮的点击事件

效果图

image-20211212160453183.png

Snackbar

Material库提供的更加先进的提示工具,允许在提示中加入一个可交互按钮,下面代码在Toolbar上的删除图标点击时,显示Snackbar提示

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    // 处理各个按钮的点击事件
    when (item.itemId) {
        R.id.backup -> Toast.makeText(this, "backup clicked", Toast.LENGTH_SHORT).show()
        R.id.delete -> {
            Snackbar.make(floating_button, "删除数据", Snackbar.LENGTH_SHORT)
                .setAction("取消") {
                    // 在这里取消删除数据
                    Log.d("TAG", "onCreate: ")
                }
                .show()
        }
        R.id.settings -> Toast.makeText(this, "settings clicked", Toast.LENGTH_SHORT).show()
        // 处理DrawerLayout导航按钮点击事件
        android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
    }
    return super.onOptionsItemSelected(item)
}

Snackbar.make()方法的第一个参数需要传入一个View,指定了Snackbar是基于哪个View触发的;第二个参数就是Snackbar中显示的内容;第三个参数是Snackbar显示的时长

效果图

image-20211212174132277.png

CoordinatorLayout

前面的Snackbar将我们的悬浮按钮给遮挡住了,借助CoordinatorLayout可以解决

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

修改activity_meterial.xml

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

    <!--使用CoordinatorLayout,以免Snackbar遮住FloatingActionButton-->
    <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/purple_500"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/floating_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="20dp"
            android:elevation="8dp"
            android:src="@drawable/ic_done"
            app:backgroundTint="#ff0000"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:rippleColor="#ffffff"
            app:tint="#00ff00" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    ......

</androidx.drawerlayout.widget.DrawerLayout>

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

效果图

image-20211212174856505.png

卡片式布局 MaterialCardView

MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉

效果图

image-20211212184603785.png

下面就实现上面的效果

添加依赖

implementation "com.github.bumptech.glide:glide:4.9.0"

添加Glide库的依赖。Glide是一个超级强大的开源图片加载库,它不仅可以用于加载本地图片,还可以加载网络图片、GIF图片甚至是本地视频

代码实现

修改activity_meterial.xml

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

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        ......
        <!--添加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>

Fruit类

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

新建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"
    android:layout_margin="5dp">

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

        <ImageView
            android:id="@+id/fruit_image"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="fitXY" />

        <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>
</com.google.android.material.card.MaterialCardView>

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

新建FruitAdapter类

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

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

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

    override fun getItemCount(): Int {
        return fruitList.size
    }
}

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

如果图片像素非常高,不进行压缩就直接展示的话,很容易引起内存溢出。而使用Glide就完全不需要担心这回事,Glide在内部做了许多非常复杂的逻辑操作,其中就包括了图片压缩
修改MeterialActivity中的代码

class MeterialActivity : AppCompatActivity() {

    private val fruits = mutableListOf(Fruit("Apple", R.drawable.apple),
        Fruit("Orange", R.drawable.orange),
        Fruit("Watermelon", R.drawable.watermelon),
        Fruit("Pineapple", R.drawable.pineapple),
        Fruit("Strawberry", R.drawable.strawberry),
        Fruit("Banana", R.drawable.banana)
    )
    private val fruitList = ArrayList<Fruit>()
    private fun initFruits() {
        fruitList.clear()
        repeat(50) {
            val random = (0 until fruits.size).random()
            fruitList.add(fruits[random])
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meterial)

        // 调用setSupportActionBar()方法并将Toolbar的实例传入
        setSupportActionBar(toolbar)
        // 添加DrawerLayout的导航按钮
        supportActionBar?.apply {
            setDisplayHomeAsUpEnabled(true)
            setHomeAsUpIndicator(R.drawable.ic_menu)
        }
        dealNavView()

        initFruits()
        recyclerView.layoutManager = GridLayoutManager(this, 2)
        recyclerView.adapter = FruitAdapter(this, fruitList)
    }
    ......
}

AppBarLayout

上面的效果中,Toolbar被RecyclerView给挡住了,可以使用AppBarLayout解决

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

AppBarLayout实际上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装

AppBarLayout使用方法

  1. 将Toolbar嵌套到AppBarLayout中
  2. 给RecyclerView指定一个布局行为
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".meterial_study.MeterialActivity">

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <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/purple_500"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                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" />

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

</androidx.drawerlayout.widget.DrawerLayout>

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

事实上,当RecyclerView滚动的时候就已经将滚动事件通知给AppBarLayout了,当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就能实现,scroll表示当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示;snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示

下拉刷新SwipeRefreshLayout

把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新

添加依赖

implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"

代码

修改activity_meterial.xml

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

    <!--DrawerLayout的第一个子控件,用于作为主屏幕中显示的内容-->
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
......
        <androidx.swiperefreshlayout.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">

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

修改MeterialActivity

// 设置下拉刷新进度条颜色
swipe_refresh.setColorSchemeResources(R.color.purple_200)
// 下拉刷新监听
swipe_refresh.setOnRefreshListener {
    Thread.sleep(2000)
    runOnUiThread {
        initFruits()
        fruitAdapter.notifyDataSetChanged()
        // 加载完成后,一定要将isRefreshing设置为false,否则刷新进度条一直转
        swipe_refresh.isRefreshing = false
    }
}

效果图

image-20211212213622107.png

可折叠式标题栏CollapsingToolbarLayout

现在的标题栏是使用Toolbar来编写的,不过它看上去和传统的ActionBar没什么两样,只不过可以响应RecyclerView的滚动事件来进行隐藏和显示。可以借助CollapsingToolbarLayout实现一个可折叠式标题栏的效果

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

创建水果详情展示界面

创建一个 FruitActivity 作为水果的详情展示界面

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".meterial_study.FruitActivity">

    <!--标题栏部分-->
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="250dp">

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

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

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <!--内容部分-->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            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_margin="15dp"
                android:layout_marginTop="35dp"
                app:cardCornerRadius="4dp">

                <TextView
                    android:id="@+id/fruit_content"
                    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>

    <!--悬浮球-->
    <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"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:tint="#ffffff" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色,app:layout_scrollFlags的scroll表示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动,exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕

app:layout_collapseMode用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好

水果内容详情的最外层布局使用了一个NestedScrollView,注意它和AppBarLayout是平级的,ScrollView和NestedScrollView相似,它们的内部都只允许存在一个直接子布局,而NestedScrollView在此基础之上还增加了嵌套响应滚动事件的功能。由于CoordinatorLayout本身已经可以响应滚动事件了,因此我们在它的内部就需要使用NestedScrollView或RecyclerView这样的布局

加入一个悬浮按钮,将锚点设置为AppBarLayout,这样悬浮按钮就会出现在水果标题栏的区域内,接着又使用app:layout_anchorGravity属性将悬浮按钮定位在标题栏区域的右下角

FruitActivity

class FruitActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "FruitActivity"
        const val FRUIT_NAME = "fruit_name"
        const val FRUIT_IMAGE_ID = "fruit_image_id"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fruit)
        // 获取传过来的水果名和图片id
        val fruitName = intent.getStringExtra(FRUIT_NAME)
        val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
        //设置toolbar
        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        collapsing_toolbar.title = fruitName
        //设置图片和内容
        Glide.with(this).load(fruitImageId).into(fruit_image)
        fruit_content.text = fruitName.repeat(500)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            android.R.id.home->{
                finish()
            }
        }
        return super.onOptionsItemSelected(item)
    }
}

修改FruitAdapter

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view =
        LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
    val holder = ViewHolder((view))
    holder.itemView.setOnClickListener {
        val fruit = fruitList[holder.bindingAdapterPosition]
        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
}

效果图

上滑前的界面


image-20211212223240006.png

上滑后的界面


image-20211212223349302.png

系统状态栏空间

借助android:fitsSystemWindows这个属性让背景图能够和系统状态栏融合,将控件的android:fitsSystemWindows属性指定成true,就表示该控件会出现在系统状态栏里,上面的代码只给ImageView设置这个属性是没有用的,我们必须将ImageView布局结构中的所有父布局都设置上这个属性才可以

同时还必须在程序的主题中将状态栏颜色指定成透明色才行。指定成透明色的方法很简单,在主题中将android:statusBarColor属性的值指定成@android:color/transparent就可以了

<style name="MyTheme" parent="Theme.AndroidStudy">
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

注意,需要重新写一个 theme,继承自默认的 theme,否则没有效果。然后到AndroidManifest.xml文件中应用

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_config"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/MyTheme">

效果图

image-20211212224223017.png

相关文章

网友评论

      本文标题:android之Meterial Design

      本文链接:https://www.haomeiwen.com/subject/jrmdfrtx.html