" ]\n" +
“}”
)
}
}
核心代码
大部分代码都有中英文对应的注释,可能有些地方翻译地不太好,请各位见谅哈。
上面也提到了,该控件是以RecyclerView为基础,涉及到JsonItemView、JsonViewAdapter和JsonRecyclerView三个类。
JSONItemView
该类用于展示每一条数据对应的视图,用到的布局文件是item_json_view.xml,它继承LinearLayout,有四个关键的变量,代码如下所示:
// JSONItemView.kt
private lateinit var tvLeft: TextView
private lateinit var ivIcon: ImageView
private lateinit var tvRight: TextView
/**
-
Set the scaled pixel text size.
-
设置文本大小。
*/
var textSize = DEFAULT_TEXT_SIZE_SP
set(value) {
// 范围是[12.0F,30.0F]
field = when {
value < 12.0F -> 12.0F
value > 30.0F -> 30.0F
else -> value
}
// 设置左边文本的文字大小
tvLeft.textSize = field
// 设置展示展开和收缩图标的大小
val size = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
field,
resources.displayMetrics
).toInt()
ivIcon.layoutParams = (ivIcon.layoutParams as LinearLayout.LayoutParams).apply {
width = size
height = size
}
// 设置右边文本的文字大小
tvRight.textSize = field
} -
变量tvLeft是左边的TextView,用于展示key相关的文本。
-
变量ivIcon是中间的ImageView,用于展示展开或者收缩的图标
-
变量tvRight是右边的TextView,用于展示value相关的文本。
-
变量textSize是public的变量,可以通过该变量改变文本的大小,要注意的是,单位是sp,无论是赋多大或者多小值,文本的最小值是12sp,最大值是30sp。
JSONViewAdapter
该类用于处理不同类型的数据和点击相关的逻辑,它继承RecyclerView.Adapter,主要涉及到如下几个关键的方法:
onBindViewHolder方法
代码如下所示:
// JSONViewAdapter.kt
override fun onBindViewHolder(holder: JSONViewAdapter.JsonItemViewHolder, position: Int) {
with(holder.jsonItemView) {
textSize = this@JSONViewAdapter.textSize
setRightColor(textColor)
jsonObject?.let { bindJSONObjectData(position, it) }
jsonArray?.let { bindJSONArrayData(position, it) }
}
}
作用是将数据绑定到视图,如果数据类型是JSONObject,就调用bindJSONObjectData方法,如果数据类型是JSONArray,就调用bindJSONArrayData方法。
handleValue方法
代码如下所示:
// JSONViewAdapter.kt
/**
- Handle the styling of the right part of the json item view (i.e., the part that shows the value).
- 处理JsonItemView右边部分的样式(即展示值的部分)。
- @param value The value to be displayed in the json item view.(要在JsonItemView展示的value。)
- @param itemView The json item view to be processed.(要处理的JsonItemView对象。)
- @param appendComma Whether to append commas.(是否附加逗号。)
- @param hierarchy The number of view hierarchies.(View的层次结构数量。)
*/
private fun handleValue(
value: Any?,
itemView: JSONItemView,
appendComma: Boolean,
hierarchy: Int
) {
itemView.showRight(SpannableStringBuilder().apply {
when (value) {
is Number ->
// 处理值为Number类型的样式
handleNumberValue(itemView, value)
is Boolean ->
// 处理值为Boolean类型的样式
handleBooleanValue(itemView, value)
is String ->
// 处理值为String类型的样式
handleStringValue(itemView, value)
is JSONObject ->
// 处理值为JSONObject类型的样式
handleJSONObjectValue(itemView, value, appendComma, hierarchy)
is JSONArray ->
// 处理值为JSONArray类型的样式
handleJSONArrayValue(itemView, value, appendComma, hierarchy)
else ->
// 处理值为null的样式
handleNullValue(itemView)
}
if (appendComma) append(",")
})
}
onClick方法
如果数据类型是JSONObject或者JSONArray的话,可以通过点击来展开或者收缩视图,代码如下所示:
如果是第一次展开,就调用performFirstExpand方法,否则就调用performClick方法,代码如下所示:
// JSONViewAdapter.kt
override fun onClick(v: View?) {
// 如果itemView的子View数量是1,就证明这是第一次展开
(itemView.childCount == 1)
.yes { performFirstExpand() }
.otherwise { performClick() }
}
performFirstExpand方法
该方法用于第一次展开JSONObject或者JSONArray对应的itemView,代码如下所示:
// JSONViewAdapter.kt
/**
- The first time the view corresponding to a JSONObject or JSONArray is expanded.
- 第一次展开JSONObject或者JSONArray对应的itemView。
*/
private fun performFirstExpand() {
isExpanded = true
itemView.showIcon(false)
itemView.tag = itemView.getRightText()
itemView.showRight(if (isJsonObject) “{” else “[”)
// 展开该层级以下的视图
val array: JSONArray? =
if (isJsonObject) (value as JSONObject).names() else value as JSONArray
val length = array?.length() ?: 0
for (i in 0 until length) {
itemView.addViewNoInvalidate(JSONItemView(itemView.context).apply {
textSize = this@JSONViewAdapter.textSize
setRightColor(textColor)
val childValue = array?.opt(i)
isJsonObject
.yes {
handleJSONObject(
key = childValue as String,
value = (value as JSONObject)[childValue],
appendComma = i < length - 1,
hierarchy = hierarchy
)
}
.otherwise {
handleJSONArray(
value = childValue,
appendComma = i < length - 1,
hierarchy = hierarchy
)
}
})
}
// 展示该层级最后的一个视图
itemView.addViewNoInvalidate(JSONItemView(itemView.context).apply {
textSize = this@JSONViewAdapter.textSize
setRightColor(textColor)
showRight(
StringBuilder(getHierarchyStr(hierarchy - 1))
.append(if (isJsonObject) “}” else “]”)
.append(if (appendComma) “,” else “”)
)
})
// 重绘itemView
itemView.requestLayout()
itemView.invalidate()
}
performClick方法
该方法用于点击后展开或者收缩,代码如下所示:
/**
- Click to expand or collapse.
- 点击后展开或者收缩。
*/
private fun performClick() {
itemView.showIcon(isExpanded)
val rightText = itemView.getRightText()
itemView.showRight(itemView.tag as CharSequence)
itemView.tag = rightText
for (i in 1 until itemView.childCount) {
// 如果展开的话,就把子View都设成可见状态,否则就设为隐藏状态
itemView.getChildAt(i).visibility = if (isExpanded) View.GONE else View.VISIBLE
}
isExpanded = !isExpanded
}
如果数据类型是url的话,可以通过点击来打开浏览器查看,代码如下所示:
/**
- Click to expand or collapse.
- 点击后展开或者收缩。
*/
private fun performClick() {
itemView.showIcon(isExpanded)
val rightText = itemView.getRightText()
itemView.showRight(itemView.tag as CharSequence)
itemView.tag = rightText
for (i in 1 until itemView.childCount) {
// 如果展开的话,就把子View都设成可见状态,否则就设为隐藏状态
itemView.getChildAt(i).visibility = if (isExpanded) View.GONE else View.VISIBLE
}
isExpanded = !isExpanded
}
判断是否是url类型的正则表达式如下所示,注释已经写得很详细,这里就不再赘述:
private val urlPattern: Pattern = Pattern.compile(
// 验证是否是http://、https://、ftp://、rtsp://、mms://其中一个
"^((http|https|ftp|rtsp|mms)?😕
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
/)?" +
// 判断字符是否为FTP地址(ftp://user:password@)
// 判断字符是否为0到9、小写字母a到z、、!、~、*、’、(、)、.、&、=、+、$、%、-其中一个,匹配零次或者一次
"(([0-9a-z!~’().&=+$%-]+: )?" +
// 判断字符是否为0到9、小写字母a到z、_、!、~、、’、(、)、.、&、=、+、$、%、-其中一个,匹配一次或者多次
“[0-9a-z_!~’().&=+$%-]+" +
// @
“@)?” +
// 判断字符是否为IP地址,例子:192.168.255.255
// 判断字符是否匹配1+[0到9,匹配两次],例如:192
“((1\d{2}” +
// 或者
“|” +
// 判断字符是否匹配2+[0到4,匹配一次]+[0到9,匹配一次],例如:225
“2[0-4]\d” +
// 或者
“|” +
// 判断字符是否匹配25+[0到5,匹配一次],例如:255
“25[0-5]” +
// 或者
“|” +
// 判断字符是否匹配[1到9,匹配一次]+[0到9,匹配一次],例如:25
“[1-9]\d” +
// 或者
“|” +
// 判断字符是否匹配1到9,匹配一次,例如:5
“[1-9])” +
// 判断字符是否匹配.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d),匹配三次
“(\.(” +
// 判断字符是否匹配1+[0到9,匹配两次],例如:192
“1\d{2}” +
// 或者
“|” +
// 判断字符是否匹配2+[0到4,匹配一次]+[0到9,匹配一次],例如:225
“2[0-4]\d” +
// 或者
“|” +
// 判断字符是否匹配25+[0到5,匹配一次],例如:255
“25[0-5]” +
// 或者
“|” +
// 判断字符是否匹配[1到9]+[0到9],例如:25
“[1-9]\d” +
// 或者
“|” +
// 判断字符是否匹配0到9,匹配一次,例如:5
“\d))” +
// 匹配三次
“{3}” +
// 或者
“|” +
// 判断字符是否为域名(Domain Name)
// 三级域名或者以上,判断字符是否为0到9、小写字母a到z、_、!、~、、’、(、)、-其中一个,匹配零次或者多次,然后加上.,例如:www.
“([0-9a-z_!~’()-]+\.)” +
// 二级域名,长度不能超过63个字符,先判断第一个字符是否为0到9、小写字母a到z其中一个,匹配一次,然后判断第二个字符是否为0到9、小写字母a到z、-其中一个,最多匹配61次,这两个字符匹配零次或者一次,最后判断第三个字符是否为0到9、小写字母a到z其中一个,然后加上.
“([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]” +
// 顶级域名,判断字符是否为小写字母a到z其中一个,匹配最少两次、最多六次,例如:.com、.cn
“\.[a-z]{2,6})” +
// 端口号,判断字符是否匹配:+[0到9,匹配最少一次、最多四次],匹配零次或者一次
“(:[0-9]{1,4})?” +
// 判断字符是否为斜杠(/),匹配零次或者一次,如果没有文件名,就不需要斜杠
“((/?)|” +
// 判断字符是否为0到9、小写字母a到z、大写字母A到Z、、!、~、*、’、(、)、.、;、?、:、@、&、=、+、$、,、%、#、-其中一个,匹配一次或者多次
"(/[0-9a-zA-Z!~*’(){}.;?😡&=+$,%#-]+)+” +
// 判断字符是否为斜杠(/),匹配零次或者一次
“/?)$”
)
该正则表达式可视化图如下所示:
JSONRecyclerView
该类用于将要处理的数据以列表的方式展示到视图,注释写得比较清楚,这里就不再赘述了,代码如下所示:
// JSONRecyclerView.kt
package com.tanjiajun.jsonrecyclerview.view
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.tanjiajun.jsonrecyclerview.DEFAULT_TEXT_SIZE_SP
import com.tanjiajun.jsonrecyclerview.R
import com.tanjiajun.jsonrecyclerview.adapter.JSONViewAdapter
import org.json.JSONArray
import org.json.JSONObject
/**
- Created by TanJiaJun on 5/31/21.
*/
class JSONRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
private val adapter = JSONViewAdapter(context)
init {
layoutManager = LinearLayoutManager(context)
setAdapter(adapter)
}
/**
- 绑定JSON字符串数据。
- Bind the json string data.
- @param jsonString The json string to bind.(要绑定的JSON字符串。)
*/
fun bindData(jsonString: String) =
adapter.bindData(jsonString)
/**
- 绑定JSONObject数据。
- Bind the json object data.
- @param jsonObject The json object to bind.(要绑定的JSONObject。)
*/
fun bindData(jsonObject: JSONObject) =
adapter.bindData(jsonObject)
/**
- 绑定JSONArray数据。
- Bind the json array data.
- @param jsonArray The json array to bind.(要绑定的JSONArray。)
*/
fun bindData(jsonArray: JSONArray) =
adapter.bindData(jsonArray)
/**
- 设置JsonItemView的样式。
- Set the json item view styles.
- @param textSize The size of all text.(所有文本的大小。)
- @param textColor The normal text color.(普通文本的颜色)
- @param keyColor The color of the text of type key.(key类型文本的颜色。)
- @param stringColor The color of the text of type String.(字符串类型文本的颜色。)
- @param numberColor The color of the text of type Number.(Number类型文本的颜色。)
- @param booleanColor The color of text of type Boolean.(Boolean类型文本的颜色。)
- @param urlColor The color of url text.(url文本的颜色。)
- @param nullColor The color of null text.(null文本的颜色。)
*/
@JvmOverloads
fun setStyles(
textSize: Float = DEFAULT_TEXT_SIZE_SP,
@ColorInt textColor: Int = ContextCompat.getColor(context, R.color.default_text_color),
@ColorInt keyColor: Int = ContextCompat.getColor(context, R.color.default_key_color),
@ColorInt stringColor: Int = ContextCompat.getColor(context, R.color.default_string_color),
@ColorInt numberColor: Int = ContextCompat.getColor(context, R.color.default_number_color),
@ColorInt booleanColor: Int = ContextCompat.getColor(
context,
R.color.default_boolean_color
),
@ColorInt urlColor: Int = ContextCompat.getColor(context, R.color.default_url_color),
@ColorInt nullColor: Int = ContextCompat.getColor(context, R.color.default_null_color)
) {
with(adapter) {
this.textSize = when {
textSize < MIN_TEXT_SIZE -> MIN_TEXT_SIZE
textSize > MAX_TEXT_SIZE -> MAX_TEXT_SIZE
else -> textSize
}
this.textColor = textColor
this.keyColor = keyColor
this.stringColor = stringColor
this.numberColor = numberColor
this.booleanColor = booleanColor
this.urlColor = urlColor
this.nullColor = nullColor
// 刷新列表
notifyDataSetChanged()
}
}
private companion object {
const val MIN_TEXT_SIZE = 12.0F
const val MAX_TEXT_SIZE = 24.0F
}
tSize < MIN_TEXT_SIZE -> MIN_TEXT_SIZE
textSize > MAX_TEXT_SIZE -> MAX_TEXT_SIZE
else -> textSize
}
this.textColor = textColor
this.keyColor = keyColor
this.stringColor = stringColor
this.numberColor = numberColor
this.booleanColor = booleanColor
this.urlColor = urlColor
this.nullColor = nullColor
// 刷新列表
notifyDataSetChanged()
}
}
private companion object {
const val MIN_TEXT_SIZE = 12.0F
const val MAX_TEXT_SIZE = 24.0F
}