第一行代码 Android
第5章 Fragment
5.2 fragment的使用方法
5.2.1 Fragment的简单用法(静态添加)
创建一个fragment
,继承Fragment
class LeftFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.left_fragment,container,false)
}
}
- 静态添加
在宿主Activity
的xml
代码中添加fragment
,关键属性name
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:name="com.example.firstline.FiveChapter.Fragment1.LeftFragment"
android:id="@+id/left_fragment"/>
5.2.2 动态添加fragment
//1.创建待添加fragment的实例
val fragment = LeftFragment()
//2.获取fragmentManager
val manager = supportFragmentManager
//3.开启事务
val transaction = manager.beginTransaction()
//实现类似返回栈的效果
transaction.addToBackStack(null)
//4.将fragment添加到宿主Activity中id为R.id.right_layout的布局中
transaction.replace(R.id.right_layout,fragment)
//5.提交事务
transaction.commit()
5.2.3 fragment实现返回栈
Activity中无论添加多少fragment只要点击返回键,回直接关闭所有fragment和宿主activity,像要fragment实现activity一层一层返回的效果,需要fragment增加到返回栈中。参考上一个示例
5.2.4 Fragment和Activity之间的交互
-
在
fragment
中获取activity
:val activity = activity as FragmentMain1Activity
-
在
activity
中获取fragment
:val fragment = manager.findFragmentById(R.id.right_layout)
获取id为R.id.right_layout的布局中存在的fragment
5.3 framgment的生命周期
5.4 动态加载布局的技巧
5.4.1 使用限定符
5.4.2使用最小宽度限定符
第11章 网络
11.6 Retrofit 网络库
11.6.1 Retrofit的基本用法
作用:
数据来源为同一个服务器地址
获取服务器各个接口的数据
- 添加依赖
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
- 数据的类对象
class RetrofitApp (val id: String, val name: String, val version: String )
- 创建服务器接口,将同一服务器提供的功能统一归纳在一个接口中
- 在
getAppData()
方法上添加注解,这里使用了@GET
注解,表示当调用getAppData()
方法时,会发送一条get
请求,并传入请求地址的相对路径 getAppData()
方法的返回值必须声明成Retrofit中内置的Call
类型,并通过泛型来指定服务器响应的数据转换成什么对象。
interface RetrofitAppService {
@GET("get_data.gson")
fun getAppData(): Call<List<RetrofitApp>>
}
- 网络调用
//获取Retrofit对象
val retrofit = Retrofit.Builder()
.baseUrl("服务器域名")//请求的根路径
.addConverterFactory(GsonConverterFactory.create())//解析数据时使用的转换库
.build()
//传入service接口对应的class类型,创建该接口的动态代理对象
val appService = retrofit.create(RetrofitAppService::class.java)
//getAppData方法会返回一个Call<List<RetrofitApp>>对象
appService.getAppData().enqueue(object: Callback<List<RetrofitApp>>{
override fun onFailure(call: Call<List<RetrofitApp>>, t: Throwable) {
t.printStackTrace()
}
override fun onResponse(call: Call<List<RetrofitApp>>, response:
Response<List<RetrofitApp>>) {
val list = response.body()
return list
}
}
}
})
11.6.2 处理复杂的接口地址类型
- 对于下面这个地址,代表页数,是一个可变的参数,需要访问这个地址试传入实参
GET http://example.com/<page>/get_data.json
/* 数据的接口地址中<page>需要传入参数
* GET中的{page}是一个占位符
* @Path("page")注解来声明yeshu这个参数是要传入占位符的参数
* */
@GET("{page}/get_data.json")
fun getData(@Path("page") yeshu: String):Call<ContactsContract.Data>
- 问号后面连接参数,参数直接用&符号分割的请求地址
GET http://example.com/get_data.json?u=<user>&t=<token>
通过@Query注解来声明
/*
* 问号后面连接参数,参数直接用&符号分割,这是标准的带参数的GET请求
* */
@GET("get_data.json")
fun getData(@Query("u") user: String,@Query("t") token: String):Call<Data>
协程完成Retrofit网络请求
- Retrofit已经库中KotlinExtensions.kt文件中定义好回调的代码,只需要三步,我们就可以完成网络请求。
//1.获取Retrofit对象
val retrofit = Retrofit.Builder()
.baseUrl("服务器域名")//请求的根路径
.addConverterFactory(GsonConverterFactory.create())//解析数据时使用的转换库
.build()
//3.在协程的作用域中调用
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
getAppData()
}
//2.获得网络请求返回结果
suspend fun getAppData(){
try {
val list = retrofit.create<RetrofitAppService>().getAppData().await()
//list就是Respose.body
}catch (e : Exception){
//捕获异常后操作
}
}
库中KotlinExtensions.kt文件源代码:
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name +
'.' +
method.name +
" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
在实际使用中完成Retrofit
- 需要get的网络地址,根据keywords获取该地区的行政区域,subdistrict是设置显示下级行政区级数1-4,key请求服务权限标识
https://restapi.amap.com/v3/config/district?keywords=<地名>&subdistrict=0&key=<key>
- 返回的数据格式:PlaceResponse.status==1表示请求成功,PlaceResponse.districts查询成功后返回的行政区域列表
data class PlaceResponse(
val count: String,
val districts: List<District>,
val info: String,
val infocode: String,
val status: String,
val suggestion: Suggestion
)
- 创建服务器接口,将同一服务器提供的功能统一归纳在一个接口中
interface PlaceService {
@GET("v3/config/district?subdistrict=0&key=${SunnyWeatherApplication.TOKEN_ADDRESS}")
fun searchPlaces(@Query("keywords")address: String): Call<PlaceResponse>
}
- 定义单例类,功能:
ServiceCreator .create(服务器的接口)
返回动态代理对象 - 动态代理对象:将某一个类或者接口投射给一个变量,这个变量就可以访问类或者接口的属性和方法
object ServiceCreator {
private const val BASE_URL = "https://restapi.amap.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>) = retrofit.create(serviceClass)
}
在本例中,动态代理对象的应用:
PlaceService
接口通过retrofit.create()
方法反射到placeService
变量。
placeService
变量就是PlaceService
接口动态代理对象。
然后通过变量placeService
调用PlaceService
接口的方法searchPlaces()
object SunnyWeatherNetWork{
//创建接口的动态代理对象
private val placeService = ServiceCreator.create(PlaceService::class.java)
//调用代理对象的方法
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
private suspend fun <T> Call<T>.await(): T{
return suspendCoroutine {
enqueue(object : Callback<T>{
override fun onFailure(call: Call<T>, t: Throwable) {
it.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null)
it.resume(body)
else
it.resumeWithException(RuntimeException("Response body is null"))
}
})
}
}
}
完成网络请求:
返回的结果就是PlaceResponse
类的实例
val placeResponse = SunnyWeatherNetWork.searchPlaces(query)
第12章 Material Design
12.2 Toolbar菜单栏
- 设置App的主题为Actionbar不可见
Theme.AppCompat.Light.NoActionBar
- 设置Toolbar,在Layout中加入如下代码:
<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属性,因为很多的Material中的属性在老系统中并不存在,为了很好的兼容老系统,增加了
xmlns:app="http://schemas.android.com/apk/res-auto"
的命名空间。 - 设置Toolbar的主题:
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
- 设置菜单项的主题:
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
- 在Activity中增加:
setSupportActionBar(toolbar)
- Toolbar中增加菜单
- 在res目录中新建menu文件夹,并创建toolbar.xml文件
- toolbar.xml文件中代码:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/backup"
android:icon="@drawable/ic_backup"
android:title="BackUp"
app:showAsAction="always"/>
<item
android:id="@+id/delete"
android:icon="@drawable/ic_delete"
android:title="Delete"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/setting"
android:icon="@drawable/ic_settings"
android:title="Setting"
app:showAsAction="always"/>
</menu>
app:showAsAction
属性值有三种情况:
always
表示永远显示在Toolbar中,空间不够则不显示;
ifRoom
表示屏幕空间足够的情况下显示在Toolbar中,不够的话在显示在菜单中;
never
则表示永远显示在菜单中。- 在Activity中添加代码:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar, menu)
return true
}
- 全部设置为
always
- 全部设置为
never
12.3 滑动菜单
12.3.1 DrawerLayout抽屉视图
- DrawerLyout是一个布局,
xml
的最外层布局更改为androidx.drawerlayout.widget.DrawerLayout
- DrawerLyout布局,通过
android:layout_gravity
属性来识别子布局是否为滑动菜单,在实际使用中建议:在DrawerLyout布局中创建两个子布局,第一个,没有加android:layout_gravity
属性的布局就是主屏幕的布局;第二个,加android:layout_gravity
属性的布局是滑动菜单。 - 滑动菜单的关键就是:
android:layout_gravity
属性,拥有这个属性的布局或者控件就是滑动菜单,属性的值是start
从左侧滑出,为end
从右侧滑出。 - 记得给DrawerLayout 加
id
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/drawerLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="右边的滑动菜单"
android:background="#FFF"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
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" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="左边的滑动菜单"
android:background="#FFF"
android:textSize="30sp" />
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
- 通过按键点击滑出滑动菜单。滑动菜单设置好以后,在屏幕中并没有办法看到滑动菜单这个功能,所以建议的做法是在Toolbar的左侧加一个导航按钮,在Activity中添加代码:
drawerLayout.openDrawer(GravityCompat.START)
START菜单打开
//在Actionbar上面的android.R.id.home按钮设置可见和图标
supportActionBar?.let {
//设置home键可见
it.setDisplayHomeAsUpEnabled(true)
//设置home键的图标
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
//在Actionbar上android.R.id.home按键的点击事件
//drawerLayout是滑动菜单布局的id
//GravityCompat.START属性值,表示展示android:layout_gravity="start"属性的布局
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
}
return true
}
12.3.2 NavigationView导航视图
- 添加依赖
implementation 'com.google.android.material:material:1.1.0'
implementation 'de.hdodenhof:circleimageview:3.0.1'
circleimageview实现图片圆形化的功能
2. App主题:parent="Theme.MaterialComponents.Light.NoActionBar"
3. NavigationView分为两部分:menu
和headerLayout
4. 在menu文件夹中新建nav_menu.xml
文件
5. group
表示一个组,android:checkableBehavior="single"
组内的菜单项都是单选
<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="Task"/>
</group>
</menu>
- 在layout文件夹中新建
nav_header.xml
文件 de.hdodenhof.circleimageview.CircleImageView
控件,是一个将图片圆形化的控件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:id="@+id/mailText"
android:textColor="#FFF"
android:textSize="14sp"
android:text="tony@mail.com"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/mailText"
android:textColor="#FFF"
android:textSize="14sp"
android:text="Tony Green"
android:id="@+id/userText"/>
</RelativeLayout>
activity_main.xml
代码修改- 加入导航视图控件
com.google.android.material.navigation.NavigationView
android:layout_gravity="start"
属性使得该控件为滑动菜单
app:menu="@menu/nav_menu"
属性,加载导航的目录
app:headerLayout="@layout/nav_header"
属性,加载头部
<!-- 左侧滑动菜单-->
<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"/>
11. 处理导航视图的菜单点击事件,在Activity中加入代码:
//导航菜单设置默认选择项
navView.setCheckedItem(R.id.navCall)
//导航菜单监听点击,点击后关闭滑动菜单,返回true表示事件已经处理
navView.setNavigationItemSelectedListener {
when (it.itemId){
R.id.navCall -> Toast.makeText(this,"call",Toast.LENGTH_SHORT).show()
}
drawerLayout.closeDrawers()
true
}
- 记得在点击后加上:
drawerLayout.closeDrawers()
方法关闭滑动菜单
12.4悬浮按钮和可交互提示
12.4.1 FloatingActionButton悬浮操作按钮
- 在页面中添加控件,
app:elevation
属性:按钮的高度值;高度越高,投影的范围越大,投影效果越淡。 - 由于悬浮按钮需要指定位置,所以外层布局需要相对布局或者帧布局一类可以定位的布局
<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"/>
- 在Activity中添加点击事件:
fab.setOnClickListener {
Toast.makeText(this,"悬浮按钮",Toast.LENGTH_SHORT).show()
}
12.4.2 Snackbar
- Snackbar在Toast的基础上增加了动作,可以用来确认用户操作
fab.setOnClickListener {
Snackbar.make(it,"确认删除吗?",Snackbar.LENGTH_SHORT)
.setAction("Undo"){
Toast.makeText(this,"已经删除",Toast.LENGTH_SHORT).show()
}
.show()
}
12.4.3 CoodinatorLayout 协调布局
- 当Snackbar弹出时,挡住了悬浮按钮
- 更改悬浮按钮所在布局为
androidx.coordinatorlayout.widget.CoordinatorLayout
CoodinatorLayout
就是一个加强版的FrameLayout
,没有什么方便的定位方式- 修改布局后,注意布局内各控件的位置属性是否需要修改
<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" />
<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>
12.5 卡片式布局
12.5.1 MaterialCardView 卡片式布局
MaterialCardView
布局适合与Adapter配合应用于图片展示,主要作用就是对每个item进行了美化
app:cardCornerRadius
属性:每个卡片的圆角的弧度
app:elevation
属性:按钮的高度值;高度越高,投影的范围越大,投影效果越淡。MaterialCardView
布局就是一个FrameLayout
布局,没有什么方便的定位方式,所以内部需要再嵌套一个布局,方便放置控件
<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"
app:elevation="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
.
.
.
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
- 在示例中还引入Glide库:
implementation 'com.github.bumptech.glide:glide:4.9.0'
Glide库是一个图片加载库,可以加载本地图片、网络图片、GIF图片甚至是本地视频 - 使用示例:
Glide.with(context).load("R.drawable.id").into(ImageView)
首先调用Glide.with()
方法并传入一个Context
、Activity
、Fragment
参数,然后调用load()
方法加载图片,可以是url
地址,也可以是本地路径或者是资源id,最后调用into()
方法将图片设置到具体某一个ImageView
控件中就可以了
12.5.2 AppBarLayout
- 作用:效果如上图,可以看到图片遮蔽了导航栏,使用AppBarLayout解决这个问题
- 用
com.google.android.material.appbar.AppBarLayout
布局包裹Toolbar
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
.
.
/>
</com.google.android.material.appbar.AppBarLayout>
- 这下反过来了,导航栏遮蔽了图片,需要给
RecyclerView
增加属性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"/>
这次看起来完美了
AppbarLayout
还给带来了其他的滚动效果,在Toolbar
增加属性app:layout_scrollFlags="scroll|enterAlways|snap"
,就能实现上拉隐藏导航栏,下拉显示导航栏的效果
scroll
:当RecyclerView
向上滚动的时候,Toolbar会跟着一起向上滚动实现隐藏
enterAlways
:当RecyclerView
向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示
snap
:当Toolbar还没有完全隐藏的时候,根据当前滚动的距离,自动选择是隐藏还是显示
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
.
.
app:layout_scrollFlags="scroll|enterAlways|snap"/>
12.6下拉刷新
- 添加依赖
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
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
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>
- 在Activity中,增加下拉刷新的功能代码
//设置刷新进度条颜色
swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
//设置下拉监听
swipeRefresh.setOnRefreshListener {
//开始刷新显示进度条
swipeRefresh.isRefreshing = true
//刷新Adapter
.
.
//刷新完成关闭进度条
swipeRefresh.isRefreshing = false
}
12.7 可折叠式标题栏
12.7.1 CollapsingToolbarLayout 折叠标题栏
- 使用
CollapsingToolbarLayout
布局是不能独立存在的,必须是AppBarLayout
的子布局,而AppBarLayout
必须是CoordinatorLayout
的子布局 app:contentScrim="@color/colorPrimary"
属性:折叠之后的背景色app:layout_scrollFlags="scroll | exitUntilCollapsed"
属性:
scorll
:CollapsingToolbarLayout
布局会随着滚动而一起滚动
exitUntilCollapsed
:CollapsingToolbarLayout
布局随着滚动完成折叠之后留在界面上
<!---->
<!--删除了不重要的代码-->
<!--CoordinatorLayout协调布局:加强版的FrameLayout,没有什么定位方式-->
<!--整个页面分为两个部分,可折叠的标题栏和可滚动的内容-->
<androidx.coordinatorlayout.widget.CoordinatorLayout>
<!--AppBarLayout,内部包裹的是整个页面的头部-->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar">
<!--1.CollapsingToolbarLayout必须嵌套在上面两个布局内-->
<!--2.CollapsingToolbarLayout包含两个控件,imageView和Toolbar-->
<!--3.当CollapsingToolbarLayout没有折叠的时候,页面是内部两个控件共同作用显示的效果,
在折叠之后就只有Toolbar的效果了-->
<!--4.折叠之后只有Toolbar,而app:contentScrim属性就是折叠之后Toolbar背景色-->
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!--ImageView控件设置app:layout_collapseMode="parallax"属性
表示在CollapsingToolbarLayout折叠的过程中产生一定的视觉效果,纯粹为了好看-->
<ImageView
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<!--app:layout_collapseMode="pin"属性,保证在折叠完成以后,toolbar不变,
如果属性值设为parallax,则标题栏中的返回按钮会随着折叠而消失-->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<!--页面的下半部分:可滚动的内容-->
<!--1.NestedScrollView是滚动ScrollView的升级版,增加了嵌套响应滚动事件的功能。-->
<!--如使用ScrollView,则不会触发滚动事件,折叠栏标题不会折叠-->
<!--2.因为整个页面的布局为FrameLayout没有什么可定位的方式,
app:layout_behavior属性保证了该布局在appbar布局下面-->
<!--3.NestedScrollView布局下只能由一个子布局,增加了LinearLayout的子布局-->
<androidx.core.widget.NestedScrollView
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout>
<!--MaterialCardView卡片式布局,单纯为了美观,让布局由一个卡片的造型,四个角为圆弧-->
<com.google.android.material.card.MaterialCardView
app:cardCornerRadius="4dp">
<TextView
android:id="@+id/fruitContentText"/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!--FloatingActionButton悬浮按钮-->
<!--1.app:layout_anchor="@id/appBar"属性,称为锚,决定这个悬浮按钮显示在哪个布局中-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
12.7.2 充分利用系统状态栏空间
- 在style文件中增加状态栏透明主题:
<style name="transparentTheme" parent="AppTheme">
<!-- Customize your theme here. -->
<!-- 属性:状态栏,颜色:透明 -->
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
- 设置AndroidManifest应用该透明主题
<activity
android:name=".****Activity"
android:theme="@style/transparentTheme">
</activity>
- 在布局中添加
android:fitsSystemWindows="true"
属性,可以使得有此属性的布局可以显示在系统状态栏中。
第13章 Jetpack
13.2 ViewModel
个人理解:实现原理很简单,就是创建了一个类来保存Activity中的数据,假如只创建一个普通的类保存数据,那么这个类会随着Activity的生命周期而创建和销毁,而使用ViewModelProvider来获取ViewModel的实例,实例就不会随着Activity的生命周期销毁或者新建。其实这个功能使用单例类也可以实现。
- 作用: Activity的任务太重了,既要负责逻辑处理,控制UI,还要处理网络请求。
ViewModel可以帮助Activiyt分担一部分工作,专门用户存放与用户界面相关的数据
- 实际应用:Activity在横竖屏切换时需要重新onCteate,数据就会丢失,将界面相关的变量存入ViewModel就可以完全避免这些问题
- 创建继承ViewModel的类
- 将类通过ViewModelProvider创建ViewModel的实例,将Activity中的数据保存在实例中
//添加依赖
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//继承ViewModel
class ViewModelClass(): ViewModel() {
var counter = 0
}
//Activity
lateinit var viewModelClass: ViewModelClass
viewModelClass = ViewModelProvider(this).get(ViewModelClass::class.java)
plus_bt.setOnClickListener {
viewModelClass.counter ++
}
}
- 使用工厂类来创建ViewModel
作用:可以传递参数
class ViewModelClass(countReserved: Int): ViewModel() {
var counter = countReserved
}
class ViewModelFactory(private val countReserved: Int): ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ViewModelClass(countReserved) as T
}
}
//Activity
lateinit var viewModelClass: ViewModelClass
viewModelClass = ViewModelProvider(this,
ViewModelFactory(countReserved)).get(ViewModelClass::class.java)
13.3 Lifecycle生命周期
作用:感知Activity的生命周期,当Activity的生命周期由变化时执行相应的方法
- 创建实现了
LifecycleObserver
的类(生命周期观察者) - 在
Activity
和是Fragment
是继承了LifecycleOwner
(生命周期拥有者),直接使用lifecycle.addObserver(myObserver)
在拥有者中增加观察者就完成了 - 观察者类
MyObserev
中的lifecycle
属性,会主动提供拥有者的生命周期。
观察者类中的
Lifecycle
属性:
这个属性不是必须的,可有可无。
没有此属性,只有Activity中的生命周期变化了,观察者才会执行相应方法,是被动接收。
有此属性,观察者时刻都可以通过该属性的lifecycle.currentState
来获得被观察者的状态,是主动获取。
class MyObserver(val lifecycle: Lifecycle): LifecycleObserver {
fun getState(): Lifecycle.State {
return lifecycle.currentState
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart(){
Log.d("yuelei", "MyObserver_activityStart: ON_START")
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun activityPause(){
Log.d("yuelei", "MyObserver_activityPause: ON_PAUSE")
}
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun activityAny(){
Log.d("yuelei", "MyObserver_activityAny: ON_ANY")
}
}
//Activity
val myObserver = MyObserver(lifecycle)
lifecycle.addObserver(myObserver)
13.4 LiveData实时数据
- 概念: LiveData是一种响应式编程组件,它可以包含任何数据的类型,并在数据发生变化的时候通知给观察者,通常与ViewModel结合在一起使用
- 创建ViewModel类及其工厂类(工厂类单纯为了传递参数)
- ViewModel类中的某一属性定义为
MutableLiveData<T>()
可变的实时数据类型,并且指定其泛型。 - 对实时数据的读写方法,get、set、put(非主线程赋值使用)
class LiveDataViewModel(num: Int): ViewModel() {
val counter = MutableLiveData<Int>()
init {
counter.value = num
}
fun plusOne(){
counter.value = (counter.value?:0) + 1
}
fun clear(){
counter.value = 0
}
}
class LDViewModelFactroy(val num: Int): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return LiveDataViewModel(num) as T
}
}
- 在Activity中设置该属性的观察者observe,当数据又变动时,会触发观察者执行代码
observe方法
第一参数是生命周期的拥有者(lifecycleOwner),在Activity中就是Activity本身
第二个参数是Observer接口,当LiveData数据发生变化时,会触发接口中的操作。
lateinit var viewModel: LiveDataViewModel
viewModel = ViewModelProvider(this,
LDViewModelFactroy(0)).get(LiveDataViewModel::class.java)
plus_bt.setOnClickListener {
viewModel.plusOne()
}
clear_bt.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this,
Observer<Int> { textView.text = it.toString() })
- 官方建议写法:
- 被观察的数据设置为私有的
- 定义一个新的变量为
LiveData
类型,get()
方法返回被观察的实例
class LiveDataViewModel(num: Int) : ViewModel() {
val counter: LiveData<Int>
get() = _counter
private val _counter = MutableLiveData<Int>()
init {
_counter.value = num
}
fun plusOne() {
_counter.value = (_counter.value ?: 0) + 1
}
fun clear() {
_counter.value = 0
}
}
13.4.2 map和swithMap
总结:用哪一个映射方法,取决于第二个参数的来源,如果是ViewModel中定义的就用map,如果是外部来源就用swithmap
- map映射
作用:通过对MutableLiveData数据的修改触发map转换函数,转换后的数据就是提供给观察者的数据
Transformations.map
方法有两个参数:
- MutableLiveData数据
- 转换后的数据,给观察者提供观察者想要的数据。
- map方法中第二个参数的it,就是第一个参数的value
个人理解:
触发数据----------------中间人map----------------观察者
中间人:通过触发数据被触发时,给观察者提供观察者需要的数据
- 触发数据:中间人的开关。(定义实时数据为私有的并且是可变的:
private val 实时数据= MutableLiveData<实时数据的类型>()
)- 中间人:触发数据发生变化时,map会监听到变化,并执行转换函数中的逻辑,再将转换后的数据通知给观察者【
val 中间人: LiveData<(给观察者的数据)的数据类型> = Transformations.map( 触发数据,{ 给观察者的数据 } )
】- 观察者:
userViewModel.中间人.observe(this, Observer{ 中间人提供给观察者的数据 -> 观察者根据中间人提供的数据而做出的动作})
data class User(var firstName: String,var lastName:String,var age: Int)
class UserViewModel:ViewModel() {
private val userLiveData = MutableLiveData<User>()
val userName: LiveData<String> = Transformations.map(userLiveData){
"${it.firstName} ${it.lastName}"
}
fun changeName(){
userLiveData.value = User("xiao" ,"mei")
}
}
//Activity
lateinit var userViewModel: UserViewModel
userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
userViewModel.userName.observe(this, Observer{
Log.d("ViewModel", "LiveDataActivity_onCreate1: $it")
})
- swithMap 转换映射
基本与Map
相同
区别:switchMap
的第二个参数是ViewModel
类之外定义的LiveData
当中间人转换后的数据的LiveData
是外部提供的,就需要用swithMap
,
不懂的地方,返回给观察着的是一个LiveData
类型,但是观察者是怎么把LiveData
变成User
类型的
object Repostory{
fun getUser(userId: String): LiveData<User>{
val liveData = MutableLiveData<User>()
liveData.value = User(userId,userId,0)
return liveData
}
}
class UserSwithMap:ViewModel(){
private val userIdLiveData = MutableLiveData<String>()
val user = Transformations.switchMap(userIdLiveData) {
userId -> Repostory.getUser(userId)}
fun getUserId(userId: String){
userIdLiveData.value = userId
}
}
lateinit var userSwithMap: UserSwithMap
userSwithMap = ViewModelProvider(this).get(UserSwithMap::class.java)
userSwithMap.user.observe(this, Observer {
livedata_tv.text = it.firstName
})
switchMap_bt.setOnClickListener {
var userId = (0..100).random().toString()
userSwithMap.getUserId(userId)
}
- LiveData与lifecycle的关系:
- 当Activity处于不可见状态的时候,如果LiveData中的数据发生了变化,是不会通知观察者的。只有当Activity重新恢复可见状态时,才会将数据通知观察者。
- 如果在不可见状态发生了很多次数据变化,LiveData会将最新的那份数据通知观察者,之前的数据都会被丢掉。
在实际使用中的LiveData
liveData()函数:
- 可指定线程
- 返回一个LiveData对象,
- 在它的代码块中可以执行挂起函数
在此例中的
Result.success
、Result.failure
应用的非常巧妙,首先是返回值,在示例中除了Result.failure(RuntimeException)是返回一个异常外,另外两个Result的返回值是相同的类型,Result.failure<List>(e)是指定了返回值的类型,Result.success(places)则places本身就是List类型。
liveData(Dispatchers.IO){//即可以返回一个LiveData对象,有在此代码中可以执行挂起函数
val result =
//这三个Result使用的非常巧妙
try{
if (success)Result.success(T)
else Result.failure(RuntimeException("异常原因"))
}catch(e :Exception){
Result.failure<T>(e)
}
emit(result)//与liveData配合使用
}
object Repository {
fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
val result = try {
//执行Retrofit的网络请求
val placeResponse = SunnyWeatherNetWork.searchPlaces(query)
//请求返回的数据中status为1则表示请求成功
if (placeResponse.status == "1"){
val places = placeResponse.districts
//使用Result.success这种方式返回
Result.success(places)
}else{
Result.failure(RuntimeException("response status is ${placeResponse.status}"))
}
}catch (e :Exception){
Result.failure<List<District>>(e)
}
//liveData无法返回数据,使用emit与其配合,强制返回数据
emit(result)
}
}
class PlaceViewModel:ViewModel() {
//通过seachPlace方法,导致searchLiveData的value值发生变化
private val searchLiveData = MutableLiveData<String>()
//当searchLiveData的value值发生变化后,执行代码块中的代码
//代码块中的it就是searchLiveData.value
val placeLiveData = Transformations.switchMap(searchLiveData){
Repository.searchPlaces(it)
}
fun seachPlace(query: String){
searchLiveData.value = query
}
//用于保存Activity界面的数据,当屏幕反转的时候不会导致数据丢失
val placeList = ArrayList<District>()
}
13.5 Room数据库存储
- 添加依赖:
apply plugin: 'kotlin-kapt'
dependencies {
implementation "androidx.room:room-runtime:2.1.0"
kapt "androidx.room:room-compiler:2.1.0"
}
- Room的3个组成部分:
- Entity。用于定义封装的实体类,每个实体类对应一张表,表中的列与实体类的字段对应。
- Dao。数据访问对象,对数据库的各种操作都是由Dao操作的。
- Database。定义数据库,包括数据库的版本号、包含的实体类、Dao。
Room操作数据库相对比较简单,从Dao的操作可以看出,Room就是面向对象的数据库,它将每一条数据都看做是Entity类的实例对象。
//Room的3大组成部分,各种注释定义数据库
@Entity
data class UserData(val name: String,val age: Int,val gender:String) {
@PrimaryKey(autoGenerate = true)
var id :Long = 0
}
@Dao
interface UserDao{
@Insert
fun insertUser(user: UserData):Long
@Update
fun updataUser(newUser: UserData)
@Query("select * from UserData")
fun loadAllUser(): List<UserData>
@Query("select * from UserData where age> :age")
fun loadUserOldrThan(age: Int): List<UserData>
@Delete
fun deleteUserData(user: UserData)
@Query("delete from UserData where gender=:gender")
fun deleteUserByGender(gender: Boolean)
}
@Database(version = 1,entities = [UserData::class])
abstract class AppDatabase : RoomDatabase(){
abstract fun UserDao(): UserDao
companion object{
private var instance :AppDatabase? = null
@Synchronized
fun getDatabase(comtext: Context):AppDatabase{
instance?.let {
return it
}
return Room.databaseBuilder(comtext.applicationContext,
AppDatabase::class.java,"app_database.db")
.build().apply{
instance = this
}
}
}
}
- 所以数据库操作不能在主线程执行
//Activity
//创建数据库并拿到Dao就可以对数据库各种操作了
val userDao = AppDatabase.getDatabase(this).UserDao()
13.6 WorkManager定时任务
- 作用:后台权限逐步收紧,很多任务无法执行,产生了定时任务工具WorkManager
- 注意:WorkManager定时任务不一定会准时执行。系统为了减少cpu的唤醒次数,有效延长电池的的使用时间,会把触发时间临近的几个任务放在一起执行。
- 添加依赖
implementation("androidx.work:work-runtime:2.2.0")
- WorkManager用法:
- 执行任务
- 定义一个后台任务,在dowork中实现具体的任务逻辑
- 配置该后台任务的,约束信息和运行条件,例如:执行一次、循环执行、间隔时间等
- 将后台任务传入
WorkManager
的equeue()
方法中,系统会在合适的时间运行
1. 定义一个后台任务,在dowork中实现具体的任务逻辑
class SimpleWorker(context: Context,params: WorkerParameters): Worker(context,params) {
//三种返回结果
override fun doWork(): Result {
Log.d("yuelei", "SimpleWorker_doWork")
return Result.success() //成功
// return Result.failure() //失败
// return Result.retry() //失败,结合setBackoffCriteria使用,重新执行
}
}
2. 配置该后台任务的,约束信息和运行条件,例如:执行一次、循环执行、间隔时间等
//只执行一次OneTimeWorkRequest
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.build()
//周期性运行PeriodicWorkRequest,间隔不少于15分钟
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java,
15, TimeUnit.MINUTES)
.build()
//延迟运行
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5,TimeUnit.MINUTES)
.build()
3. 将后台任务传入`WorkManager`的`equeue()`方法中,系统会在合适的时间运行
WorkManager.getInstance(this).enqueue(request)
- 取消任务
通过标签或者id取消任务
//通过addTag方法给任务设置标签
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.addTag("simple")
.build()
//通过运行对象id取消任务
WorkManager.getInstance(this).cancelWorkById(request.id)
//取消所有持有此标签的任务
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
- 任务返回Result.retry()的结果时
返回Result.retry(),主要是setBackoffCriteria的参数设置
setBackoffCriteria
- 第二个和第三个参数用于指定在多长时间后第一次重试,时间不得少于10秒
- 第一个参数用于指定任务重试失败后,到下次执行的间隔时间,可选值有两种:
BackoffPolicy.LINEAR,间隔时间线性增长
BackoffPolicy.EXPONENTIAL,间隔时间指数级增长
/*
* 后台任务doWork()返回Result.retry()时,结合setBackoffCriteria使用
* */
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 ,TimeUnit.SECONDS)
.build()
WorkManager.getInstance(this).enqueue(request)
- 任务返回Result.success()和Result.failure()的结果时
可以通过id观察任务的执行结果:getWorkInfoByIdLiveData
也可以通过Tag观察任务的执行结果:getWorkInfoByTagLiveData
//doWork返回Result.failure()和Result.success()时有什么作用
//通过LiveData的observe观察返回的结果
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.addTage("simple")
.build()
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this, Observer {
when(it.state){
WorkInfo.State.SUCCEEDED -> Log.d(
"",
"ThirteenWorkManagerActivity_onCreate:SUCCEEDED "
)
WorkInfo.State.FAILED -> Log.d(
"yuelei",
"ThirteenWorkManagerActivity_onCreate: FAILED"
)
}
})
- 链式反应
需要多个任务配合来完成工作
//链式反应
val first = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val third = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
WorkManager.getInstance(this)
.beginWith(first)
.then(second)
.then(third)
.enqueue()
第14章进阶,高级技巧
14.1 全局获取Context的技巧
- 创建一个
MyApplication
类,继承Application
class MyApplication: Application() {
companion object{
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
- 修改
AndroidManifest.xml
代码
<application
.
.
android:name=".FourteenChapter.MyApplication">
- 在项目的任何地方都可以通过
MyApplication.context
得到context
14.2 使用Intent传递对象
作用:传递自定义的类对象等
14.2.1 Serializable序列化方式
- 类继承Serializable
- 将类对象通过Intent发送
- 接收对象,反序列化,向下转型
class Person(val name:String,val age: Int): Serializable
//发送
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person",Person("tom",10))
startActivity(intent)
//接收
val serializableExtra = intent.getSerializableExtra("person") as Person
14.2.2 Parcelable可包装方式
- 类中的数据必须封装在主构造函数中
- 接收对象
@Parcelize
class Person(val name:String,val age: Int): Parcelable
//发送
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person",Person("tom",10))
startActivity(intent)
//接收
val person = intent.getParcelableExtra<Person>("person") as Person
14.3 定制自己的日志工具
object LogUtli {
private const val VERBOSE = 1
private const val DEBUG = 2
private const val INFO = 3
private const val WARN = 4
private const val ERROR = 5
//自定义打印级别
private const val level = VERBOSE
fun v(tag: String, msg: String){
if (level <= VERBOSE){
Log.v(tag,msg)
}
}
fun d(tag: String, msg: String){
if (level <= DEBUG){
Log.d(tag,msg)
}
}
fun i(tag: String, msg: String){
if (level <= INFO){
Log.i(tag,msg)
}
}
fun w(tag: String, msg: String){
if (level <= VERBOSE){
Log.v(tag,msg)
}
}
fun e(tag: String, msg: String){
if (level <= VERBOSE){
Log.e(tag,msg)
}
}
}