一、效果示意
下图GIF展示了两个比较简单的子item type:TextView文本 和 Button按钮
二、依赖
使用到四个依赖,分别是:
RecyclerView、multitype、gson及apache的网络库(用于将HTTP内容字符的字节数组转换为字符串)
// (1)RecyclerView的依赖
api 'androidx.recyclerview:recyclerview:1.0.0'
// (2)multitype的依赖
api 'com.drakeet.multitype:multitype:4.3.0'
// (3)解析json数据源的依赖
api 'com.google.code.gson:gson:2.8.5'
// (4)apache http网络库:使用EncodingUtils的utf-8转换的工具(将HTTP内容字符的字节数组转换为字符串)
implementation 'org.apache.httpcomponents:httpcore:4.4.4'
三、实现过程
1、解析json数据
(1)json数据源,放入assets文件夹下
如下json格式,将rv_data.json放入assets文件夹下,其中title就是展示标题的字段,show_type是用于区分type类型的字段,这里就展示了两个字段,用0和1进行简单区分。
{
"data": [
{
"title": "item1.......",
"show_type":0
},
{
"title": "item2.......",
"show_type":1
},
{
"title": "item3.......",
"show_type":0
},
...
{
"title": "item17.......",
"show_type":0
}
]
}
(2)json数据源对应的RvDataBean
对应json数据源的DataBean如下所示:
TYPE_0、TYPE_1用于区分RecyclerView的Item类型、对应json数据源的show_type
@SerializedName("data") 是json数据最外层的data
@SerializedName("title")、 @SerializedName("show_type")同上
open class RvDataBean {
companion object {
const val TYPE_0 = 0 // 标题类型0
const val TYPE_1 = 1 // 标题类型1
}
@SerializedName("data")
private var mData: ArrayList<DataBean> = ArrayList<DataBean>()
open fun getData(): ArrayList<DataBean> {
return mData
}
open fun setData(data: ArrayList<DataBean>) {
mData = data
}
class DataBean {
@SerializedName("title")
var title: String? = ""
/**
* 显示样式 0 、1
*/
@SerializedName("show_type")
var showType = 0
}
}
(3)解析json数据,将json数据映射到RvDataBean对象上
两个步骤:读取json文件,转换为字符串;将字符串,转换为Bean对象
1)读取json文件,转换为字符串
public static String getJsonStrFromAssets(Context context, String fileName) {
String cacheJson = null;
InputStream in = null;
try {
in = context.getResources().getAssets().open(fileName);
int length = in.available();
byte[] buffer = new byte[length];
int result = in.read(buffer);
if (result == 0) {
Log.e(TAG, "读取失败");
return null;
}
cacheJson = EncodingUtils.getString(buffer, "UTF-8");
} catch (Exception e) {
Log.e(TAG, "ex", e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
Log.e(TAG, "ex", e);
}
}
}
return cacheJson;
}
2)将字符串,转换为Bean对象
public static <T> T convertJsonStrToBean(String json, final Class<T> clazz) {
T dataBean = null;
try {
if (TextUtils.isEmpty(json)) {
return null;
}
dataBean = new Gson().fromJson(json, clazz);
} catch (Exception ex) {
Log.e(TAG, "convertJsonToBean error = ", ex);
}
return dataBean;
}
有了前面的两个步骤,就可以调用获得RvDataBean对象了,如下
private suspend fun getData() : ArrayList<RvDataBean.DataBean> {
return withContext(Dispatchers.IO) {
val dataBean = arrayListOf<RvDataBean.DataBean>()
val localData: String = StrUtils.getJsonFromAssets(AppApplication.application, "rv_data.json")
dataBean.addAll(StrUtils.convertJsonToBean(localData, RvDataBean::class.java).getData())
dataBean
}
}
这里的AppApplication如下,便于更快捷获取context上下文;
class AppApplication : Application() {
companion object {
var application : AppApplication? = null
}
override fun onCreate() {
super.onCreate()
application = this
}
}
<application
android:name="com.example.nested_scroll_demo.AppApplication"
...
2、ViewModel
通过ViewModel来解析json数据源后,并通知RecyclerView去刷新界面。
(1)定义一个jsonList对象,当其内容变化时,即通知监听它的地方。
class RvViewModel : ViewModel() {
var jsonList : MutableLiveData<ArrayList<RvDataBean.DataBean>?> = MutableLiveData()
//从json里面读取消息
@Nullable
fun getDataFromJson() {
MainScope().launch {
if (jsonList.value == null) {
jsonList.value = getData()
}
}
}
private suspend fun getData() : ArrayList<RvDataBean.DataBean> {
return withContext(Dispatchers.IO) {
val dataBean = arrayListOf<RvDataBean.DataBean>()
val localData: String = StrUtils.getJsonFromAssets(AppApplication.application, "rv_data.json")
dataBean.addAll(StrUtils.convertJsonToBean(localData, RvDataBean::class.java).getData())
dataBean
}
}
}
(2)在Activity引入ViewModel,并监听jsonList
在MainActivity的onCreate中引入ViewModel,并主动解析json数据,当数据解析完成后,通过initObserve的jsonList变化通知Adapter去刷新界面,这里的adapter就是MulitAdapter,下面会讲解到。
private var rvViewModel : RvViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvViewModel = ViewModelProvider(this)[RvViewModel::class.java]
initObserve()
rvViewModel?.getDataFromJson()
}
@SuppressLint("NotifyDataSetChanged")
private fun initObserve() {
// 列表数据内容刷新
rvViewModel?.jsonList?.observe(this) {
//live data两个特点:(1)黏性的可以接收到注册前的数据 ;(2)数据生命周期时间长; 因此这里要判空
if (originData.isEmpty() && it != null) {
originData.addAll(it)
adapter.items = originData
adapter.notifyDataSetChanged()
}
}
}
3、子item的Delegate实现
(1)第一个子item:TextDelegate
TextDelegate 继承 ItemViewDelegate, ItemViewDelegate两个构造参数:数据源DataBean以及视图模板ViewHolder
因此,做三件事:根据item布局来创建的ViewHolder模板、真正创建ViewHolder、数据绑定到ViewHolder上
class TextDelegate : ItemViewDelegate<RvDataBean.DataBean, TextDelegate.ViewHolder>() {
private lateinit var mContext: Context
// 数据绑定到ViewHolder上
override fun onBindViewHolder(holder: ViewHolder, item: RvDataBean.DataBean) {
holder.titleTextView.text = item.title
}
// 真正创建ViewHolder的方法
override fun onCreateViewHolder(context: Context, parent: ViewGroup): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.rv_item, parent, false)
mContext = parent.context
return ViewHolder(view)
}
// 根据item布局来创建的ViewHolder模板
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView : TextView = itemView.findViewById(R.id.rv_item_text)
}
}
(2)第二个子item:ButtonDelegate
和第一个TextDelegate类似,这里不再展示。
4、MultiTypeAdapter和RecyclerView
(1)MainActivity的RecyclerView布局文件
布局特别简单,就是单纯的引入了一个RecyclerView,如下所示:
<?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"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
(2)MultiTypeAdapter与RecyclerView的初始化及绑定
将所有的Item都注册到MultiTypeAdapter中,并将所有的Item与数据源进行一一映射,如下initRecyclerView中的代码所示,然后将adapter与RecyclerView绑定一起即可。
private val adapter = MultiTypeAdapter()
private var recyclerView : RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvViewModel = ViewModelProvider(this)[RvViewModel::class.java]
initRecyclerView()
initObserve()
rvViewModel?.getDataFromJson()
}
private fun initRecyclerView() {
// init rv
recyclerView = findViewById(R.id.rv)
recyclerView?.layoutManager = LinearLayoutManager(this)
adapter.register(RvDataBean.DataBean::class.java).to(
TextDelegate(),
ButtonDelegate()
).withKotlinClassLinker { _, dataBean ->
when (dataBean.showType) {
RvDataBean.TYPE_0 -> TextDelegate::class
RvDataBean.TYPE_1 -> ButtonDelegate::class
else -> TextDelegate::class
}
}
recyclerView?.adapter = adapter
}
四、其他
1、RecycelerView触边界后如何去掉波浪
在RecyclerView的布局中添加:android:overScrollMode="never",或者在代码层面调用recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER,至于为什么滑动动顶部或者底部有波浪现象,留给下篇文章去探析下其中的原理吧。
2、Fragment页面如何加载ViewModel
Fragment页面和Activity页面加载ViewModel还是有点区别的,在Fragment中使用下面两个步骤即可:
(1)先构建ViewModelFactory
class RvViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
try {
for (constructor in modelClass.constructors) {
if (arrayOf(Context::class.java).contentEquals(constructor.parameterTypes)) {
return (constructor as Constructor<T>).newInstance(context)
}
}
return modelClass.newInstance()
} catch (e: InstantiationException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: IllegalAccessException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
}
}
}
(2)然后调用下面方式即可创建viewModel
viewModel = ViewModelProvider(this, RvViewModelFactory(activity!!)).get(RvewModel::class.java)
3、在support包下如何使用MultiTypeAdapter进行register
使用support包下的RecyclerView 和 使用AndroidX下面的RecyclerView使用 MultiTypeAdapter是不同的,具体可以参考下面的写法,用于在support包下使用RecyclerView和MultiTypeAdapter。
尽量不要使用support包下的RecyclerView,因为MultiType已经不再针对适配support更新啦。
private fun initRecyclerView() {
recyclerView = findViewById(R.id.rv)
recyclerView?.layoutManager = LinearLayoutManager(this)
adapter.register(RvDataBean.DataBean::class).to(
TextDelegate(),
ButtonDelegate()
).withKClassLinker { _, dataBean ->
when (dataBean.showType) {
RvDataBean.TYPE_0 -> TextDelegate::class
RvDataBean.TYPE_1 -> ButtonDelegate::class
else -> TextDelegate::class
}
}
recyclerView?.adapter = adapter
}