随着Kotlin的推广,一些国内公司的安卓项目开发,已经从Java彻底切成Kotlin了。虽然Kotlin在各种编程语言中的排名比较靠后(据TIOBE发布了 19 年 8 月份的编程语言排行榜,Kotlin居然排名45位),可是做为安卓开发者,掌握该语言,却已经是大势所趋了。html
Kotlin的基础用法,总体仍是比较简单的,网上已经有不少文章了,你们熟悉下便可。android
这次案例,之因此选择分页列表,主要是由于该功能通用性强,涵盖的技术点也较多,对开发者熟悉Kotlin帮助性较大。数据库
案例的主要需求以下( 参考主流电商APP实现 ):
一、列表支持手势滑动分页查询(滑动到底部时,自动查询下一页,直到没有更多数据)
二、可切换列表样式和网格样式
三、切换样式后,数据位置保持不变(如当前在第100条位置,切换样式后,位置不变)
四、footview根据查询状态,显示不一样内容:编程
a、数据加载中... (正在查询数据时显示) b、没有更多数据了 (查询成功,可是已没有可返回的数据了) c、出错了,点击重试!!(查询时出现异常,多是网络,也多是其余缘由)
五、当查询出错时,再次点击footview,可从新发起请求(例如:网络异常了)
六、当切换网格样式时,footview应独占一行安全
虽然是简单案例,我们开发时,也应先进行简单的设计,让各模块、各种都各司其职、逻辑解耦,这样你们学起来会更简单一些。
此处,不画类图了,直接根据项目结构,简单介绍一下吧:服务器
一、pagedata 是指数据模块,包含:网络
DataInfo 实体类,定义商品字段属性 DataSearch 数据访问类,模拟子线程异步查询分页数据,可将数据结果经过lambda回调回去
二、pageMangage 是指分页管理模块,将分页的所有逻辑托管给该模块处理。为了简化分页逻辑的实现,根据功能单一性进行了简单拆分:多线程
PagesManager 分页管理类,用于统筹列表数据查询、展现、样式切换 PagesLayoutManager 分页布局管理类,用于列表样式和网格样式的管理、数据位置记录 PagesDataManager 分页数据管理类,用于分页列表数据、footview数据的封装
三、adapter 是指适配器模块,主要用于定义各种适配器app
PagesAdapter 分页适配器类,用于建立、展现 itemview、footview,处理footview回调事件
四、utils 是指工具模块,用于定义一些经常使用工具异步
AppUtils 工具类,用于判断网络链接状况
在文章的最后,会将Demo源码下载地址分享给你们,以供参考。
kotlin类中定义了属性,则已包含了默认的get、set
package com.qxc.kotlinpages.pagedata /** * 实体类 * * @author 齐行超 * @date 19.11.30 */ class DataInfo { /** * 标题 */ var title: String = "" /** * 描述 */ var desc: String = "" /** * 图片 */ var imageUrl: String = "" /** * 价格 */ var price: String = "" /** * 连接 */ var link: String = "" }
package com.qxc.kotlinpages.pagedata import com.qxc.kotlinpages.utils.AppUtils /** * 数据查询类:模拟网络请求,从服务器数据库获取数据 * * @author 齐行超 * @date 19.11.30 */ class DataSearch { //服务器数据库中的数据总数量(模拟) private val totalNum = 25 //声明回调函数(Lambda表达式参数:errorCode错误码,dataInfos数据,无返回值) private lateinit var listener: (errorCode: Int, dataInfos: ArrayList<DataInfo>) -> Unit /** * 设置数据请求监听器 * * @param plistener 数据请求监听器的回调类对象 */ fun setDataRequestListener(plistener: (errorCode: Int, dataInfos: ArrayList<DataInfo>) -> Unit) { this.listener = plistener } /** * 查询方法(模拟从数据库查询) * positionNum: 数据起始位置 * pageNum: 查询数量(每页查询数量) */ fun search(positionNum: Int, pageNum: Int) { //模拟网络异步请求(子线程中,进行异步请求) Thread() { //模拟网络查询耗时 Thread.sleep(1000) //得到数据查询结束位置 var end: Int = if (positionNum + pageNum > totalNum) totalNum else positionNum + pageNum //建立集合 var datas = ArrayList<DataInfo>() //判断网络状态 if (!AppUtils.instance.isConnectNetWork()) { //回调异常结果 this.listener(1,datas) //回调异常结果 //this.listener.invoke(-1, datas) return@Thread } //组织数据(模拟获取到数据) for (index in positionNum..end - 1) { var data = DataInfo() data.title = "Android Kotlin ${index + 1}" data.desc = "Kotlin 是一个用于现代多平台应用的静态编程语言,由 JetBrains ..." data.price = (100 * (index + 1)).toString() data.imageUrl = "" data.link = "跳转至Kotlin柜台 -> JetBrains" datas.add(data) } //回调结果 this.listener.invoke(0, datas) }.start() } }
DataSearch类有两个重点知识:
1.2.一、子线程异步查询的实现
a、参考常规分页网络请求API,数据查询方法应包含参数:起始位置、每页数量 b、安卓中的网络查询,须要在子线程中操做,当前案例直接使用Thread{}.start()实现子线程 线程实现的写法与Java中不太同样,为何这么写呢?我们具体展开说明一下: -----------------------------------Thread{}.start()------------------------------------- 一般状况下,Java中实现一个线程,可这么写: new Thread(new Runnable() { @Override public void run() { } }).start(); 若是彻底按照Java的写法,将其转为Kotlin,应该这么写: Thread(object: Runnable{ override fun run() { } }).start() 可是在本案例中倒是:Thread{}.start(),并未看到Runnable对象和run方法,实际上是被Lambda表达式替换了: >> Runnable接口有且仅有1个抽象函数run(),符合“函数式接口”定义(即:一个接口仅有一个抽象方法) >> 这样的接口可使用Lambda表达式来简化代码的编写 : 使用Lambda表示Runnable接口实现,因run()无参数、无返回值,对应的Lambda实现结构应该是: { -> } 当Lambda表达式无任何参数时,能够省略箭头符号: { } 咱们将Lambda表达式替换Runnable接口实现,Kotlin代码以下所示: Thread({ }) .start() 若是Lambda表达式是函数是最后一个实参,则能够放在小括号后面: Thread() { } .start() 若是Lambda是函数的惟一实参的时候,小括号能够直接省略,也就变成了我们案例的效果了: Thread{ } .start() -----------------------------------Thread{}.start()------------------------------------- 以上是线程Lambda表达式的简化过程!!! 使用Lambda表达式,使得咱们能够在 “Thread{ }” 的大括号里直接写子线程实现,代码变简单了 更多Lambda表达式介绍,请参考文章:https://www.cnblogs.com/Jetictors/p/8647888.html
1.2.二、数据回调监听
此案例经过Lambda表达式实现了数据回调监听(与iOS的block相似): a、声明Lambda表达式,用于定义回调方法结构(参数、返回值),如: var listener: (errorCode: Int, dataInfos: ArrayList<DataInfo>) -> Unit Lambda表达式可理解为一种特殊类型,即:抽象方法类型 b、由调用方传递过来Lambda表达式的实参对象(即:调用方已实现的Lambda表达式所表示的抽象方法) setDataRequestListener(plistener:....) c、当执行完数据查询时,将结果经过调用Lambda表达式实参对象回传回去,如: this.listener(1,datas) this.listener.invoke(0, datas) 这两种调用方式都是能够的,invoke是指执行自身 固然,咱们也能够在Kotlin中使用接口回调的那种方式,与Java同样,只是代码会繁琐一些而已!!!
为了简化分页逻辑,让你们更好理解,此处将分页数据、分页布局拆分出来,使其逻辑解耦,也便于代码的管理维护。
主要内容,包括:
一、配置分页数据的查询位置、每页数量,每次查询数据后从新计算下次查询位置 二、分页数据接口查询 三、分页状态文本处理(数据查询中、没有更多数据了、查询异常...)
package com.qxc.kotlinpages.pagemanage import android.os.Handler import android.os.Looper import android.util.Log import com.qxc.kotlinpages.pagedata.DataInfo import com.qxc.kotlinpages.pagedata.DataSearch /** * 分页数据管理类: * 一、分页数据的查询位置、每页数量 * 二、分页数据接口查询 * 三、分页状态文本处理 * * @author 齐行超 * @date 19.11.30 */ class PagesDataManager { var startIndex = 0 //分页起始位置 val pageNum = 10 //每页数量 val TYPE_PAGE_MORE = "数据加载中..." //分页加载类型:更多数据 val TYPE_PAGE_LAST = "没有更多数据了" //分页加载类型:没有数据了 val TYPE_PAGE_ERROR = "出错了,点击重试!!" //分页加载类型:出错了,当这种状态时可点击重试 //定义数据回调监听 //参数:dataInfos 当前页数据集合, footType 分页状态文本 lateinit var listener: ((dataInfos: ArrayList<DataInfo>, footType: String) -> Unit) /** * 设置回调 */ fun setDataListener(pListener: (dataInfos: ArrayList<DataInfo>, footType: String) -> Unit) { this.listener = pListener } /** * 查询数据 */ fun searchPagesData() { //建立数据查询对象(模拟数据查询) var dataSearch = DataSearch() //设置数据回调监听 dataSearch.setDataRequestListener { errorCode, datas -> //切回主线程 Handler(Looper.getMainLooper()).post { if (errorCode == 0) { //累计当前位置 startIndex += datas.size //判断后面是否还有数据 var footType = if (pageNum == datas.size) TYPE_PAGE_MORE else TYPE_PAGE_LAST //回调结果 listener.invoke(datas, footType) } else { //回调错误结果 listener.invoke(datas, TYPE_PAGE_ERROR) } } } //查询数据 dataSearch.search(startIndex, pageNum) } /** * 重置查询 */ fun reset() { startIndex = 0; } }
主要内容,包括:
一、建立列表布局、网格布局(只建立一次便可) 二、记录数据位置(用于切换列表布局、网格布局时,保持位置不变)
package com.qxc.kotlinpages.pagemanage import android.content.Context import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager /** * 分页布局管理类: * 一、建立列表布局、网格布局 * 二、记录数据位置(用于切换列表布局、网格布局时,保持位置不变) * * @author 齐行超 * @date 19.11.30 */ class PagesLayoutManager( pcontext: Context ) { val STYLE_LIST = 1 //列表样式(常量标识) val STYLE_GRID = 2 //网格样式(常量标识) var llManager: LinearLayoutManager //列表布局管理器对象 var glManager: GridLayoutManager //网格布局管理器对象 var position: Int = 0 //数据位置(当切换样式时,需记录列表数据的位置,用以保持数据位置不变) var context = pcontext //上下文对象 init { llManager = LinearLayoutManager(context) glManager = GridLayoutManager(context, 2) } /** * 得到布局管理器对象 */ fun getLayoutManager(pagesStyle: Int): LinearLayoutManager { //记录当前数据位置 recordDataPosition(pagesStyle) //根据样式返回布局管理器对象 if (pagesStyle == STYLE_LIST) { return llManager } return glManager } /** * 得到数据位置 */ fun getDataPosition(): Int { return position } /** * 记录数据位置 */ private fun recordDataPosition(pagesStyle: Int) { //pagesStyle表示目标样式,此处须要记录的是原样式时的数据位置 if (pagesStyle == STYLE_LIST) { position = glManager?.findFirstVisibleItemPosition() } else if (pagesStyle == STYLE_GRID) { position = llManager?.findFirstVisibleItemPosition() } } }
主要内容,包含:
一、建立、刷新适配器 二、查询、绑定分页数据 三、切换分页布局(列表布局、网格布局) 四、当切换至网格布局时,设置footview独占一行(即便网格布局每行显示多个item,footview也独占一行)
主要技术点,包括:
一、设置grid footview独占一行 二、RecyclerView控件的使用(数据绑定,刷新,样式切换等)
package com.qxc.kotlinpages.pagemanage import android.content.Context import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.qxc.kotlinpages.adapter.PagesAdapter import com.qxc.kotlinpages.pagedata.DataInfo /** * 分页管理类: * 一、建立适配器 * 二、查询、绑定分页数据 * 三、切换分页布局 * 四、当切换至网格布局时,设置footview独占一行 * * @author 齐行超 * @date 19.11.30 */ class PagesManager(pContext: Context, pRecyclerView: RecyclerView) { private var context = pContext //上下文对象 private var recyclerView = pRecyclerView //列表控件对象 private var adapter:PagesAdapter? = null //适配器对象 private var pagesLayoutManager = PagesLayoutManager(context) //分页布局管理对象 private var pagesDataManager = PagesDataManager() //分页数据管理对象 private var datas = ArrayList<DataInfo>() //数据集合 /** * 设置分页样式(列表、网格) * * @param isGrid 是否网格样式 */ fun setPagesStyle(isGrid: Boolean): PagesManager { //经过样式得到对应的布局类型 var style = if (isGrid) pagesLayoutManager.STYLE_GRID else pagesLayoutManager.STYLE_LIST var layoutManager = pagesLayoutManager.getLayoutManager(style) //得到当前数据位置(切换样式后,滑动到记录的数据位置) var position = pagesLayoutManager.getDataPosition() //建立适配器对象 adapter = PagesAdapter(context, datas, pagesDataManager.TYPE_PAGE_MORE) //通知适配器,itemview当前使用哪一种分页布局样式(列表、网格) adapter?.setItemStyle(style) //列表控件设置适配器 recyclerView.adapter = adapter //列表控件设置布局管理器 recyclerView.layoutManager = layoutManager //列表控件滑动到指定位置 recyclerView.scrollToPosition(position) //当layoutManager是网格布局时,设置footview独占一行 if(layoutManager is GridLayoutManager){ setSpanSizeLookup(layoutManager) } //设置监听器 setListener() return this } /** * 设置监听器: * * 一、当滑动到列表底部时,查询下一页数据 * 二、当点击了footview的"出错了,点击重试!!"时,从新查询数据 */ fun setListener() { //一、当滑动到列表底部时,查询下一页数据 adapter?.setOnFootViewAttachedToWindowListener { //查询数据 searchData() } //二、当点击了footview的"出错了,点击重试!!"时,从新查询数据 adapter?.setOnFootViewClickListener { if (it.equals(pagesDataManager.TYPE_PAGE_ERROR)) { //点击查询,更改footview状态 adapter?.footMessage = pagesDataManager.TYPE_PAGE_MORE adapter?.notifyDataSetChanged() //"出错了,点击重试!! searchData() } } } /** * 设置grid footview独占一行 */ fun setSpanSizeLookup(layoutManager: GridLayoutManager) { layoutManager.setSpanSizeLookup(object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return if (adapter?.TYPE_FOOTVIEW == adapter?.getItemViewType(position)) layoutManager.getSpanCount() else 1 } }) } /** * 得到查询结果,刷新列表 */ fun searchData() { pagesDataManager.setDataListener { pdatas, footMessage -> if (pdatas != null) { datas.addAll(pdatas) adapter?.footMessage = footMessage adapter?.notifyDataSetChanged() } } pagesDataManager.searchPagesData() } }
主要内容,包括:
一、建立、绑定itemview(列表item、网格item)、footview 二、判断是否滑动到列表底部(更简单的方式实现列表滑动到底部的监听) 三、footview点击事件回调(若是是footview显示为“出错了,点击重试”,须要获取点击事件,从新查询数据) 四、滑动到列表底部事件回调(当列表滑动到底部时,则须要查询下一页数据了)
主要技术点,包括:
一、多item项的应用 二、滑动到列表底部的判断(比“监听RecyclerView的Scroll坐标”这种常规作法要简化不少,且精准)
package com.qxc.kotlinpages.adapter import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.qxc.kotlinpages.R import com.qxc.kotlinpages.pagedata.DataInfo /** * 分页适配器类 * 一、建立、绑定itemview(列表item、网格item)、footview * 二、判断是否滑动到列表底部 * 三、footview点击事件回调 * 四、滑动到列表底部事件回调 * * @author 齐行超 * @date 19.11.30 */ class PagesAdapter( pContext: Context, pDataInfos: ArrayList<DataInfo>, pFootMessage : String ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private var context = pContext //上下文对象,经过构造传函数递过来 private var datas = pDataInfos //数据集合,经过构造函数传递过来 var footMessage = pFootMessage //footview文本信息,可经过构造函数传递过来,也可再次修改 val TYPE_FOOTVIEW: Int = 1 //item类型:footview val TYPE_ITEMVIEW: Int = 2 //item类型:itemview var typeItem = TYPE_ITEMVIEW val STYLE_LIST_ITEM = 1 //样式类型:列表 val STYLE_GRID_ITEM = 2 //样式类型:网格 var styleItem = STYLE_LIST_ITEM /** * 重写建立ViewHolder的函数 */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder { //若是是itemview if (typeItem == TYPE_ITEMVIEW) { //判断样式类型(列表布局、网格布局) var layoutId = if (styleItem == STYLE_LIST_ITEM) R.layout.item_page_list else R.layout.item_page_grid var view = LayoutInflater.from(context).inflate(layoutId, parent, false) return ItemViewHolder(view) } //若是是footview else { var view = LayoutInflater.from(context) .inflate(R.layout.item_page_foot, parent, false) return FootViewHolder(view) } } /** * 重写得到项数量的函数 */ override fun getItemCount(): Int { //因列表中增长了footview(显示分页状态信息),因此item总数量 = 数据数量 + 1 return datas.size + 1 } /** * 重写绑定ViewHolder的函数 */ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is ItemViewHolder) { if (datas.size <= position) { return } var data = datas.get(position) holder.tv_title.text = data.title holder.tv_desc.text = data.desc holder.tv_price.text = data.price holder.tv_link.text = data.link } else if (holder is FootViewHolder) { holder.tv_msg.text = footMessage //当点击footview时,将该事件回调出去 holder.tv_msg.setOnClickListener { footViewClickListener.invoke(footMessage) } } } /** * 从新得到项类型的函数(项类型包括:itemview、footview) */ override fun getItemViewType(position: Int): Int { //设置在数据最底部显示footview typeItem = if (position == datas.size) TYPE_FOOTVIEW else TYPE_ITEMVIEW return typeItem } /** * 当footview第二次出如今列表时,回调该事件 * (此处用于模拟用户上滑手势,当滑到底部时,从新请求数据) */ var footviewPosition = 0 override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { super.onViewAttachedToWindow(holder) if (footviewPosition == holder.adapterPosition) { return } if (holder is FootViewHolder) { footviewPosition = holder.adapterPosition //回调查询事件 footViewAttachedToWindowListener.invoke() } } /** * ItemViewHolder */ class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var tv_title = itemView.findViewById<TextView>(R.id.tv_title) var tv_desc = itemView.findViewById<TextView>(R.id.tv_desc) var tv_price = itemView.findViewById<TextView>(R.id.tv_price) var tv_link = itemView.findViewById<TextView>(R.id.tv_link) } /** * FootViewHolder */ class FootViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var tv_msg = itemView.findViewById<TextView>(R.id.tv_msg) } /** * 设置Item样式(列表、网格) */ fun setItemStyle(pstyle: Int) { styleItem = pstyle } //定义footview附加到Window上时的回调 lateinit var footViewAttachedToWindowListener: () -> Unit fun setOnFootViewAttachedToWindowListener(pListener: () -> Unit) { this.footViewAttachedToWindowListener = pListener } //定义footview点击时的回调 lateinit var footViewClickListener:(String)->Unit fun setOnFootViewClickListener(pListner:(String)->Unit){ this.footViewClickListener = pListner } }
此案例中主要用于判断网络链接状况。
该类的主要技术点:Kotlin的共生对象、线程安全单例,详见源码:
package com.qxc.kotlinpages.utils import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build /** * 工具类 * * @author 齐行超 * @date 19.11.30 */ class AppUtils { //使用共生对象,表示静态static companion object{ /** * 线程安全的单例(懒汉式单例) */ val instance : AppUtils by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { AppUtils() } private lateinit var context:Context /** * 注册 * * @param pContext 上下文 */ fun register(pContext: Context){ context = pContext } } /** * 判断是否链接了网络 */ fun isConnectNetWork():Boolean{ var result = false val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { cm?.run { this.getNetworkCapabilities(cm.activeNetwork)?.run { result = when { this.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true this.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true this.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true else -> false } } } } else { cm?.run { cm.activeNetworkInfo?.run { if (type == ConnectivityManager.TYPE_WIFI) { result = true } else if (type == ConnectivityManager.TYPE_MOBILE) { result = true } } } } return result } }
package com.qxc.kotlinpages import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import com.qxc.kotlinpages.pagemanage.PagesManager import com.qxc.kotlinpages.utils.AppUtils import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { var isGrid = false var pagesManager: PagesManager? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) AppUtils.register(this) initEvent() initData() } fun initEvent() { //切换列表样式按钮的点击事件 iv_style.setOnClickListener { //切换图标(列表与网格) var id: Int = if (isGrid) R.mipmap.product_search_list_style_grid else R.mipmap.product_search_list_style_list iv_style.setImageResource(id) //记录当前图标类型 isGrid = !isGrid //更改样式(列表与网格) pagesManager!!.setPagesStyle(isGrid) } } fun initData() { //初始化PagesManager,默认查询列表 pagesManager = PagesManager(this, rv_data) pagesManager!!.setPagesStyle(isGrid).searchData() } }
注意:页面中引用了 kotlinx.android.synthetic.main.activity_main.* 》》这表示无需再写findViewById()了,直接使用xml中控件id便可
MainActivity的布局页面,使用了约束布局,层级嵌套少,且更简单一些:
<?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=".MainActivity"> <View android:id="@+id/v_top" android:layout_width="match_parent" android:layout_height="50dp" android:background="#FD4D4D" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="分页demo" android:textColor="#ffffff" android:textSize="18sp" app:layout_constraintBottom_toBottomOf="@id/v_top" app:layout_constraintLeft_toLeftOf="@id/v_top" app:layout_constraintRight_toRightOf="@id/v_top" app:layout_constraintTop_toTopOf="@id/v_top" /> <ImageView android:id="@+id/iv_style" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginRight="5dp" android:scaleType="center" android:src="@mipmap/product_search_list_style_grid" app:layout_constraintBottom_toBottomOf="@id/v_top" app:layout_constraintRight_toRightOf="@id/v_top" app:layout_constraintTop_toTopOf="@id/v_top" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_data" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/v_top" /> </androidx.constraintlayout.widget.ConstraintLayout>
<?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:layout_height="150dp" android:layout_marginLeft="10dp" android:layout_marginTop="10dp" android:layout_marginRight="10dp" android:background="#eeeeee"> <ImageView android:id="@+id/iv_image" android:layout_width="80dp" android:layout_height="110dp" android:layout_marginLeft="10dp" android:scaleType="fitXY" android:src="@mipmap/kotlin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:text="Android Kotlin" android:textColor="#333333" android:textSize="18sp" app:layout_constraintLeft_toRightOf="@id/iv_image" app:layout_constraintTop_toTopOf="@id/iv_image" /> <TextView android:id="@+id/tv_desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:lines="2" android:text="Kotlin 是一个用于现代多平台应用的静态编程语言,由 JetBrains 开发..." android:textColor="#888888" android:textSize="12sp" app:layout_constraintLeft_toRightOf="@id/iv_image" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_title" /> <TextView android:id="@+id/tv_price_symbol" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginTop="20dp" android:text="¥" android:textColor="#FD4D4D" android:textSize="10dp" app:layout_constraintLeft_toRightOf="@id/iv_image" app:layout_constraintTop_toBottomOf="@id/tv_desc" /> <TextView android:id="@+id/tv_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="128.00" android:textColor="#FD4D4D" android:textSize="22sp" app:layout_constraintBaseline_toBaselineOf="@id/tv_price_symbol" app:layout_constraintLeft_toRightOf="@id/tv_price_symbol" /> <TextView android:id="@+id/tv_link" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:text="跳转至Kotlin柜台 -> JetBrains" android:textColor="#aaaaaa" android:textSize="10sp" app:layout_constraintBottom_toBottomOf="@id/iv_image" app:layout_constraintLeft_toRightOf="@id/iv_image" /> </androidx.constraintlayout.widget.ConstraintLayout>
<?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:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginTop="10dp" android:layout_marginRight="10dp" android:paddingBottom="10dp" android:background="#eeeeee"> <ImageView android:id="@+id/iv_image" android:layout_width="80dp" android:layout_height="110dp" android:layout_marginTop="10dp" android:scaleType="fitXY" android:src="@mipmap/kotlin" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginTop="20dp" android:text="Android Kotlin" android:textColor="#333333" android:textSize="18sp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/iv_image" /> <TextView android:id="@+id/tv_desc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:lines="2" android:text="Kotlin 是一个用于现代多平台应用的静态编程语言,由 JetBrains 开发..." android:textColor="#888888" android:textSize="12sp" app:layout_constraintLeft_toLeftOf="@id/tv_title" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_title" /> <TextView android:id="@+id/tv_price_symbol" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="¥" android:textColor="#FD4D4D" android:textSize="10dp" app:layout_constraintLeft_toLeftOf="@id/tv_title" app:layout_constraintTop_toBottomOf="@id/tv_desc" /> <TextView android:id="@+id/tv_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="128.00" android:textColor="#FD4D4D" android:textSize="22sp" app:layout_constraintBaseline_toBaselineOf="@id/tv_price_symbol" app:layout_constraintLeft_toRightOf="@id/tv_price_symbol" /> <TextView android:id="@+id/tv_link" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="跳转至Kotlin柜台 -> JetBrains" android:textColor="#aaaaaa" android:textSize="10sp" app:layout_constraintTop_toBottomOf="@id/tv_price" app:layout_constraintLeft_toLeftOf="@id/tv_title" /> </androidx.constraintlayout.widget.ConstraintLayout>
比较简单,仅有一个文本控件:
<?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:layout_height="50dp" android:layout_margin="10dp" android:background="#eeeeee"> <TextView android:id="@+id/tv_msg" android:layout_width="0dp" android:layout_height="0dp" android:gravity="center" android:text="加载中..." android:textColor="#777777" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
一、切换RecyclerView展现样式(列表样式、网格样式),保持数据位置不变
二、网格样式时,footview独占一行
三、直接在adapter中判断是否滑动到了底部,比常规作法(监听滑动坐标)更简单一些
四、分页状态管控(数据加载中、没有更多数据了、出错了点击重试)
一、多线程实现(Lambda表达式的应用)
二、异步回调(Lambda表达式的应用、高阶函数)
三、共生对象
四、线程安全单例
五、其余略(都比较基础了,你们熟悉下便可)
此篇文章主要是为了讲解常规分页的实现,因此只是作了一些基础的拆分解耦,若是想在项目中使用,建议仍是抽象一下,扩展性会更好一些(如:footview接口化扩展、数据查询接口化扩展等)。
若是有疑问,也欢迎留言咨询O(∩_∩)O~
Demo下载地址: https://pan.baidu.com/s/1gH0Zcd0QXdm4mRNMqJgS8Q