第一行代码Android 阅读笔记 第十二章(仅自用)

Material Design实战

统一android平台的界面风格。

Toolbar

actionbar就是标题栏,但由于设计原因,被限定在activity的顶部,从而不能实现Material Design效果。

toolbar继承了actionbar的所有功能,且灵活性极高,可配合其他控件完成一些Material Design的效果

每个activity都会自带一个actionbar,这定义在androidmanifest中

<application
 android:allowBackup="true"
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"
 android:roundIcon="@mipmap/ic_launcher_round"
 android:supportsRtl="true"
 //使用了主题AppTheme
 android:theme="@style/AppTheme">
 ...
</application>

//style里面有以下定义
<resources>
 <!-- Base application theme. -->
 //出现actionbar就是因为指定了这个主题,而我们准备使用Toolbar来替代AcitonBar,
 //需指定一个不带ActionBar的主题,常有有Theme.AppCompat.NoActionBar 和  Theme.AppCompat.Light.NoActionBar可选,
 //前者为深色主题,主体为深色,陪衬色为浅色,后者则相反。
 <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>

在这里插入图片描述
类似默认定义好的colorPrimary等,我们也可以定义上图其他元素,而coloAccent用于表达强调。

<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">
 //此处定义了一个toolbar控件,由appcompat库提供
 //由于前面已经设计主题使用浅色主题,因此toolbar也为浅色主题,toolbar上的各种元素就会变为深色系
 //但这样就会导致字体变成黑色,很难看
 //为使toolbar单独使用深色主题,这里指定了主题为深色 android:theme
 //但这样就导致了toolbar弹出的菜单项也变成深色主题,又会十分难看
 //因此将弹出菜单指定为浅色主题 app:popupTheme
 <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>

此处使用了xmlns:app指定了一个新的命名空间,许多marerial属性是在新系统中新增的,老系统不存在。

//传入toolbar实例,使得既使用了toolbar,又使其外观和功能都和actionbar一致
class MainActivity : AppCompatActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 setSupportActionBar(toolbar)
 }
}

优化ToolBar

<application
 android:allowBackup="true"
 android:icon="@mipmap/ic_launcher"
 //设置标题栏的文字,默认为app名字
 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">
 ...
 </activity>
</application>

为toolbar添加按钮


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

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

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

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

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

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

class MainActivity : AppCompatActivity() {
 ...
  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
     menuInflater.inflate(R.menu.toolbar, menu)
     return true
 }
  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.backup -> Toast.makeText(this, "You clicked Backup",Toast.LENGTH_SHORT).show()
        R.id.delete -> Toast.makeText(this, "You clicked Delete",Toast.LENGTH_SHORT).show()
        R.id.settings -> Toast.makeText(this, "You clicked Settings",Toast.LENGTH_SHORT).show()
       }
       return true
    }
}

最终结果如下
此处就会出现了三种显示情况,

  • 返回设置的是always,总是显示在toolbar中,屏幕空间不足时才不显示,此处空间足够,因此正常显示;
  • delete设置的是ifRoom表示空间足够才显示在toolbar中,不够则显示在菜单当中;
  • setting设置的是never,无论如何都不会显示在toolbar,只会在菜单当中

ps: toolbar中的只显示图标,菜单则只显示文字
在这里插入图片描述

滑动菜单

DrawerLayout

隐藏一些菜单选项,并不放置在主屏幕上,而是通过滑动将菜单显示出来,通过DrawerLayout控件实现。

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

<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
   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>
   //第二个子控件,即拉出来的内容
 <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>

滑动效果出现了,但可能用户不知道可以滑动,我们可以添加个导航键,使得点击导航键展开滑动页面。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    //获取action的实例
    setSupportActionBar(toolbar)
    supportActionBar?.let {
       it.setDisplayHomeAsUpEnabled(true)//显示导航按钮
       it.setHomeAsUpIndicator(R.drawable.ic_menu)//设置导航按钮图标
     }
 }
 ...
  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
    //home按钮的id永远都是android.R.id.home,通过openDrawer展示菜单
       android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
       ...
   }
   return true
  }
}

NavigationView

使用NavigationView优化滑动菜单,在滑动菜单里面可以使用NavigationView定制任意布局。

添加依赖库,第一个为material库,第二为一个开源项目CircleImageView,用于实现图片圆形化。

implementation ‘com.google.android.material:material:1.1.0’
implementation ‘de.hdodenhof:circleimageview:3.0.1’

除此之外还需要将主题改为无标题栏,避免崩溃。

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

Menu

<menu xmlns:android="http://schemas.android.com/apk/res/android">
 <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>

Header

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="180dp"
   android:padding="10dp"
   android:background="@color/colorPrimary">
   //CircleImageView是一个用于将图片圆形化的控件
 <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:textSize="14sp" />
 <TextView
   android:id="@+id/userText"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_above="@id/mailText"
   android:text="Tony Green"
   android:textColor="#FFF"
   android:textSize="14sp" />
</RelativeLayout>
<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
   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>
 <com.google.android.material.navigation.NavigationView
   android:id="@+id/navView"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_gravity="start"
   //通过app:menu和app:headerLayout属性将我们刚才准备好的menu和headerLayout设置了进去
   app:menu="@menu/nav_menu"
   app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>

activity中设计点击时间

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

悬浮按钮和可交互提示

立面设计是Material Design中非常重要的设计思想,即应用程序的界面不仅仅是一个平面,而应该是有立体效果。

FloatingActionButton

FloatingActionButton是material库中提供的一个控件,默认使用colorAccent作为按钮颜色。

<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
  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" />
 <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" />
 </FrameLayout>
 ...
</androidx.drawerlayout.widget.DrawerLayout>

悬浮按钮还有阴影效果,即前面提到的立体效果,值越大,越高,阴影越淡,反之越浓。

app:elevation=“8dp”

Snackbar

material库提供比Toast更先进的提示工具–Snackbar
两者差异在于:Toast是告诉用户现在发生了什么事情,但用户只能被动接收这个事情。而Snackbar进行了拓展,允许在提示中加入一个可交互按钮,当用户点击按钮时,可执行一些额外的逻辑操作。

如当执行删除操作时,通过Snackbar增加一个undo按钮,提供了弥补措施。

fab.setOnClickListener {
//view为当前界面的任意一个view,snackbar会使用view自动查找最外层布局,
//用于展示提示信息,参数2为内容,参数3为显示时长,与Toast类似。
  view ->Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
     .setAction("Undo") {
          Toast.makeText(this, "Data restored", Toast.LENGTH_SHORT).show()
   }
   .show()
 }

在这里插入图片描述
此处的snackbar挡住了悬浮按钮。

CoordinatorLayout

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

CoordinatorLayout可以监听其所有子控件的各种事件,并自动帮助我们做出最合理的响应。如上面例子,刚才弹出snackbar提示遮挡住了悬浮按钮,当使用CoordinatorLayout监听到snackbar的弹出事件,它会自动将悬浮按钮向上偏移,使其不被遮挡。

修改framelayout为CoordinatorLayout即可
在这里插入图片描述
前面提到了CoordinatorLayout可以监听其所有子控件的各种事件,但snackbar并不是CoordinatorLayout的子控件,之所以能被监听到,是因为我们在snackbar的make方法中传入的第一个参数是view,即悬浮按钮本身,这个参数指定了snackbar基于悬浮按钮触发,使得CoordinatorLayout可以监听到snackbar的弹出和隐藏事件。

如果将view改为DrawerLayout,就会出现遮挡问题。

卡片式布局

此时主页面仍比较空,我们可以添加图片在其中,而为了让水果图片也能material化,使得页面中的元素看起来就像在卡片中医药,拥有圆角和投影。

MaterialCardView

MaterialCardView用于实现卡片式布局效果的重要控件,由material库提供,实际上MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果。
卡片效果如下:
在这里插入图片描述
引入导航

implementation ‘androidx.recyclerview:recyclerview:1.0.0’
implementation ‘com.github.bumptech.glide:glide:4.9.0’

布局中加入

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

创建实体类

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

创建recyclerview item,此处使用MaterialCardView作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中。由于MCV是一个FrameLayout,因此它没有什么方便的定位方式,因此在这里再嵌套LinearLayout

<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    app:cardCornerRadius="4dp">
  <LinearLayout
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">
  <ImageView
   android:id="@+id/fruitImage"
   android:layout_width="match_parent"
   android:layout_height="100dp"
   //指定缩放模式,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>

接下来为RecycleView准备适配器,

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
        Glide.with(context).load(fruit.imageId).into(holder.fruitImage)
    }
    override fun getItemCount() = fruitList.size
}

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

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

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("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(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])
   }
 }
 ...
}

这样设置还有一个问题是,遮挡了toolbar,因为CoordinatorLayout本身是一个特别的framelayout,在不进行定位时,都会默认放置在左上角。
解决方法除了传统的偏移,还有更合适的方法。
使用AppBarLayout,这其实是一个垂直方向上的LinearLayout,内部做了很多滚动事件的封装,并应用了一些Material design的思想。

解决方法:
通过appbarlayout包含toolbar,
在recycleviewlayout中补充app:layout_behavior=“@string/appbar_scrolling_view_behavior”

<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">
 <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" />
 ...
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
 ...
</androidx.drawerlayout.widget.DrawerLayout>

前面提到了appbarlayout有material design的思想,上面的没有太多体现。

当recyclerview滚动时将滚动事件通知给appbarlayout,只是我们没有进行处理。

当appbarlayout接收到滚动事件的时候,它内部的子控件其实是可以去指定如何响应事件

当下滑时,一般都会隐藏toolbar,可将此句添加在appbarlayout的子控件中

app:layout_scrollFlags=“scroll|enterAlways|snap”

scroll鄙视当RV向上滚动,toolbar跟着向上滚动并隐藏。
enterAlways表示当RV向下滚动时,toolbar会跟着一起向下滚动并重新显示。
snap表示当toolbar还没完全隐藏或显示时,会根据当前滚动的距离,自动选择是隐藏还是显示。

下拉刷新

google让android的下拉刷新有统一风格,在material design中制定了一个官方的设计规范,提供了现成的控件。

SwipeRefreshLayout就是实现下拉刷新功能的核心类,把要想实现下拉刷新功能的控件放置在SwipeRefreshlayout中,可让这个控件支持下拉刷新。

添加依赖

implementation?“androidx.swiperefreshlayout:swiperefreshlayout:1.0.0”

将recyclerlayout嵌入SwipeRefreshlayout中

<?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"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <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/design_default_color_primary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme = "@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways|snap">
         </androidx.appcompat.widget.Toolbar>
        </com.google.android.material.appbar.AppBarLayout>
          //添加嵌套
        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/swipeRefresh"
            android:layout_width = "match_parent"
            android:layout_height = "match_parent"
            //此时变更了子控件,需要及时修改
            app:layout_behavior = "@string/appbar_scrolling_view_behavior">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behavior" />
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/floating_action_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:layout_gravity="bottom|end"
            android:src="@mipmap/done"
            android:contentDescription="done">
        </com.google.android.material.floatingactionbutton.FloatingActionButton>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header">
    </com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>

设置刷新逻辑

class MainActivity : AppCompatActivity() {
 ...
 override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
 ...
   swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
   swipeRefresh.setOnRefreshListener {
       refreshFruits(adapter)
  }
 }
   private fun refreshFruits(adapter: FruitAdapter) {
     thread {
         Thread.sleep(2000)
         runOnUiThread {
             initFruits()
             adapter.notifyDataSetChanged()
             swipeRefresh.isRefreshing = false
          }
       }
    }
 ...
}

可折叠式标题栏

Collapsing ToolbarLayout

Collapsing ToolbarLayout是一个作用域toolbar基础上的布局,由material库提供,Collapsing ToolbarLayout使得toolbar效果更丰富,不仅仅是展示一个标题栏,且能实现非常华丽的效果。

但Collapsing ToolbarLayout不能独立存在,仅能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局,因此使三层布局。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            **//要实现更高级的toolbar效果,需要将这个主题的指定提到上一层来。
            //app:contentScrim用于指定CoollapsingToolbarLayout在趋于折叠状态以及折叠后的背景色
            //其实CoollapsingToolbarLayout在折叠后就是一个普通的toolbar。
            //app:layout_scrollFlags属性也同样提到外面来,
            //其中scroll表示CTL会随着水果内容详情的滚动一起滚动,
            //exitUntilCollapsed表示当CTL随着滚动完成折叠后就保留在界面上,不再移出屏幕。**
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:contentScrim="@color/design_default_color_primary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            //在CTL里面定义了一个imageview和toolbar,
            //即表示这个高级的标题栏是由普通标题栏加上图片组合而成的。
            //app:layout_collapseMode指定当前控件在CTL折叠过程中的折叠模式,
            //其中toolbar指定为pin,表示在折叠的过程中位置始终保持不变,
            //imageview指定成parallax,表示在折叠过程中产生一定的错位偏移。
            <ImageView
                android:id="@+id/fruitImageView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax" />
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    //由于CoordinatorLayout本身可以响应滚动事件,
    //其内部就需要NestedScrollView或RecyclerView这样的布局,
    //另外还通过app:layout_behavior属性指定了一个布局行为,
    //这和之前在RecyclerView中的用法是一模一样的,
     //水果内容详情的最外层布局使用了一个NestedScrollView,
     //注意它和AppBarLayout是平级的,NestedScrollView比起ScrollView,
     //除了允许使用滚动的方式来查看屏幕以外的数据,还增加了嵌套响应滚动事件的功能。
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        //不管是ScrollView还是NestedScrollView,
        //其内部只允许存在一个直接子布局,
        //因此当我们想向其中放入很多东西,通常先嵌套一个LinearLayout,
        //然后在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>
    //FloatingActionButton中使用app:layout_anchor属性指定了一个锚点,
    //我们将锚点设置为AppBarLayout,这样悬浮按钮就会出现在水果标题栏的区域内,
    //接着又使用app:layout_anchorGravity属性将悬浮按钮定位在标题栏区域的右下角。
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@mipmap/other"
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Adapter中也要进行对应修改,需要完成点击事件。
用户点击图片进入详情页,adapter需要为每个item设置监听器,当接收到点击事件后,跳转到详情页,并传入数据:水果名字和图片。

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)
        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

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

编写详情页的activity

class FruitActivity : AppCompatActivity() {
    companion object {
        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)
        //接收传入的数据
        val fruitName = intent.getStringExtra(FRUIT_NAME) ?: ""
        val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
        //将toolbar实例化
        setSupportActionBar(toolbar)
        //当向上滑动时显示toolbar
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        collapsingToolbar.title = fruitName
        //进行UI更新
        Glide.with(this).load(fruitImageId).into(fruitImageView)
        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)
}

最终折叠化效果如下。
在这里插入图片描述
在这里插入图片描述

水果背景图和系统状态栏有点不搭,如果我们将背景图和状态栏融合在一起,能提升视觉体验。

在android5.0前,我们无法对状态栏的背景或颜色进行操作,5.0以及之后的系统就支持该功能。

想要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows来实现,在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout这种嵌套结构的布局中,将控件的android:fitSystemWindows指定为true,表示该控件出现在系统状态栏。
对应到程序,那就是水果标题栏中的imageview以及对应的所有父布局应该设置这个属性
除此之外还需要在主题文件中设置状态栏颜色为透明色

@android:color/transparent

在这里插入图片描述

编写好用的工具方法

求最大值,使用kotlin内置的max函数,但这只支持两个参数,如果想比较三个得写成max(a,max(b,c)),实在是麻烦,因此可以这么写

//vararg表示接收任意个同类型参数
fun max(vararg nums: Int): Int {
 var maxNum = Int.MIN_VALUE
 for (num in nums) {
    maxNum = kotlin.math.max(maxNum, num)
 }
 return maxNum
}

但这样写还有个问题,只能接收int类型数据,当然可以通过重写各种参数类型版本的max,但这十分繁琐,因此可以这么写:
java中任何类型的数字都可比较,因此需要实现comparable接口,这个规则在kotlin中也同样成立,我们可通过泛型将max修改成接收任意多个实现comparable的参数。

//指定泛型上界为Comparable<T>,表明T为Comparable<T>的子类型,即参数类型为实现了Comparable的子类型
fun <T : Comparable<T>> max(vararg nums: T): T {
 if (nums.isEmpty()) throw RuntimeException("Params can not be empty.")
     var maxNum = nums[0]
     for (num in nums) {
       if (num > maxNum) {
           maxNum = num
       }
    }
 return maxNum
}

Toast的简化写法

//重写两个int和string版本的,并将时长设置为默认参数,作为拓展函数写就很简便
fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
 Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
 Toast.makeText(context, this, duration).show()
}
"a".showToast(this)

Snackbar

//因为snackbar原本传入参数里面有view,会自动找寻最外层布局,用于显示snackbar,因此写成view的拓展函数
//通过高阶函数补充action的用法
fun View.showSnackbar(text: String, actionText: String? = null,
   duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
   val snackbar = Snackbar.make(this, text, duration)
   if (actionText != null && block != null) {
      snackbar.setAction(actionText) {
          block()
     }
   }
  snackbar.show()
}

fun View.showSnackbar(resId: Int, actionResId: Int? = null,
   duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
   val snackbar = Snackbar.make(this, resId, duration)
   if (actionResId != null && block != null) {
       snackbar.setAction(actionResId) {
           block()
       }
   }
 snackbar.show()
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值