目录
Android统一表单输入-Statement
本文所有代码均使用Kotlin
在参与的toB项目中,很多时候要输入多个表头信息,最初也是采用了最笨的方式,通过线性布局一个一个添加不同的控件实现内容输入,一旦需求发生改动,就要重新修改xml文件来应对新需求。
这在实际工作中是很费时费力的,那么就研究出只要传入对应类型的参数就会显示相应的样式,并在更新时提示我发生了数据变化,也就是接下来要说的Statement模块,当然我也仅是提供一种思路并根据我的需要整理了部分内容,其他的内容还请自行编写。
功能演示
动图中包含了静态表单和动态表单两种,如点击‘是否提醒’开启和关闭所展示的内容是不一致的,也可以点击跳转到其他页面获取onActivityResult
中返回参数进行加载,同时更新内容会实时进行更新。
下面Logcat中截取了部分实时更新的表单json数据,可以直接用该数据进行网络请求,或将其实例化为指定对象并在保存时存储至数据库中。
原理
继承
这里用的是Kotlin,与Java类似,所以引用百度百科对于Java关于继承相关说明
继承是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。比如可以先定义一个类叫车,车有以下属性:车体大小,颜色,方向盘,轮胎,而又由车这个类派生出轿车和卡车两个类,为轿车添加一个小后备箱,而为卡车添加一个大货箱。
只需要创建一个基类,让各种表单类型继承该基类,并根据Helper加载出对应的
ViewHolder
即可实现扩建
具体实现
IStatement
公共的接口,方便后续
interface IStatement {
/**
* 将改内容转换为json字符串
*/
fun save(): String
/**
* 加载试图
*/
fun init(holder: Any, position: Int)
/**
* 更新数据
* @param data Any
*/
fun update(data: Any?)
/**
* 帮助按钮
*/
fun help(view: View)
/**
* 是否是必填信息
*/
fun important(view: View)
}
BaseStatement
/**
* 基础框架
* @author byk
* @date 2022年2月7日
*/
open class BaseStatement(
/**
* 是否必填
*/
var important: Boolean = false,
/**
* 说明文本
*/
var help: String = "",
/**
* 下方文本
*/
var hint: String = "",
/**
* 显示标题
*/
var title: String = "",
/**
* 输入内容
*/
var text: String = "",
/**
* json key
*/
var key: String = "",
/**
* 该列表key
*/
var KEY: String = "",
/**
* viewType
*/
open var type: Int = 0
): IStatement {
/**
* 生成表单json
* 不应依赖该项
*/
override fun save(): String {
return "\"${key}\":\"${text}\""
}
/**
* viewholder初始化
*/
override fun init(holder: Any, position: Int) {
}
/**
* 通知Helper进行数据更新
*/
override fun update(data: Any?) {
StatementViewHelper.dataMap[KEY]?.let {
it[key] = data
StatementViewHelper.listenMap[KEY]?.invoke(it)
}
}
/**
* 判断是否存在帮助信息
*/
override fun help(view: View) {
if (TextUtils.isEmpty(help)) {
view.visibility = View.GONE
}
view.setOnClickListener {
HelpDialog.instance.init(
title = title,
help = help
).show(view.context)
}
}
/**
* 判断是否是必填项
*/
override fun important(view: View) {
if (!important) {
view.visibility = View.GONE
} else {
view.visibility = View.VISIBLE
}
}
}
BaseStatementViewHolder
空的ViewHolder,主要是方便识别,其中没有任何代码
open class BaseStatementViewHolder(
item: View
): RecyclerView.ViewHolder(item) {
}
StatementListAdapter
继承ListAdapter,通过差分来实时更新数据
class StatementListAdapter: ListAdapter<BaseStatement, BaseStatementViewHolder>(DIFF) {
companion object {
val DIFF = object : DiffUtil.ItemCallback<BaseStatement>() {
override fun areItemsTheSame(oldItem: BaseStatement, newItem: BaseStatement): Boolean {
return oldItem.title == newItem.title
}
override fun areContentsTheSame(oldItem: BaseStatement, newItem: BaseStatement): Boolean {
return oldItem.key == newItem.key
}
}
}
override fun getItemViewType(position: Int): Int {
return getItem(position).type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseStatementViewHolder {
return StatementViewHelper.get(parent, viewType)
}
override fun onBindViewHolder(holder: BaseStatementViewHolder, position: Int) {
getItem(position).init(holder, position)
}
}
StatementViewHelper
这里存放了数据、动态更新回调、数据更新回调三个Map对象
dataMap
:数据,指定表单的json数据
updateMap
: 动态更新回调,如Switch开关状态改变动态修改表单时触发
listenMap
:数据更新回调,各个表单元素数据发生变化时触发
/**
* 找到对应页面
* @author byk
* @date 2022年2月7日
*/
class StatementViewHelper {
companion object {
val listenMap = mutableMapOf<String, (data: JSONObject) -> Unit>()
val updateMap = mutableMapOf<String, (data: Array<BaseStatement>, position: Int, old: Int) -> Unit>()
val dataMap = mutableMapOf<String, JSONObject>()
/**
* 获取对应的数据,将传入的list对象解析成json数据
* @param key String
* @param list List<BaseStatement>
*/
@JvmStatic
fun updateData(key: String, list: List<BaseStatement>) {
val data = StringBuilder()
data.append("{")
for (baseStatement in list) {
baseStatement.KEY = key
data.append(baseStatement.save()).append(",")
}
data.append("}")
dataMap[key] = JSONObject.parseObject(data.toString())
}
/**
* 在adapter中获取ViewHolder时使用
*/
@JvmStatic
fun get(parent: ViewGroup, viewType: Int): BaseStatementViewHolder {
return when(viewType) {
1 -> {
val binding = ItemStatementEdittextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = EditTextViewHolder(binding.root)
holder.binding = binding
holder
}
// ...这里可以根据BaseStatement中的type类型进行扩展
else -> {
val binding = ItemStatementBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = StatementViewHolder(binding.root)
holder.binding = binding
holder
}
}
}
/**
* 监听数据变化
*/
@JvmStatic
fun listen(key: String, listener: (data: JSONObject) -> Unit) {
listenMap[key] = listener
}
/**
* 监听列表变化
*/
@JvmStatic
fun update(key: String, listener: (data: Array<BaseStatement>, position: Int, old: Int) -> Unit) {
updateMap[key] = listener
}
}
}
基础布局
这是默认的静态文本的样式布局,可以根据程序需要自行添加新的xml布局
<?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:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title" />
<TextView
android:id="@+id/tv_item_s_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="TextView"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_item_s" />
<TextView
android:id="@+id/tv_item_s"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginEnd="16dp"
android:background="@drawable/shape_statement"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
以TextViewStatement静态文本样式为例
class TextViewStatement(
important: Boolean = false,
help: String = "",
hint: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* 输入内容
*/
text: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, hint, title, text, key) {
/**
* Helper根据这个type找到对应的ViewHolder
* 一个公用页面一个type
*/
override var type = 0
/**
* 在这里绘制视图
*/
override fun init(holder: Any, position: Int) {
holder as StatementViewHolder
holder.binding.tvItemSTitle.text = title
holder.binding.tvItemS.text = text
holder.binding.tvItemS.hint = hint
// 判断是否开启了帮助说明,如果开启则传入的view即为开启展示的功能按钮
help(holder.binding.ivItemSHelp)
// 判断是否为必填项,并没有做限制,仅在title左侧添加红色*
important(holder.binding.ivItemSImportant)
}
}
具体使用
- 在指定布局中添加
RecyclerView
并且赋予androidx.recyclerview.widget.LinearLayoutManager
线性布局 - 创建
StatementListAdapter
,并传递给RecyclerView
- 创建Statement列表,并传入对应的Statement对象
val list = mutableListOf<BaseStatement>() list.add( TextViewStatement( title = "你好", text = "Hello World", key = "abc", hint = "world" ) )
- 内容加载,更新监听,更新adapter
// test_list即本表单的标识,一个表单只有一个标识,如有多个请更换为其他标识,防止更新混乱 StatementViewHelper.updateData("test_list", list) StatementViewHelper.listen("test_list") { // 这里面就是最新的表单json数据 Log.d("test_list", it.toString()) } StatementViewHelper.update("test_list") { data, position, old -> // 当表单发生变化时触发,这个可以不修改,复制到任何地方,只需修改对应的key即可 for (i in 0 until old) { list.removeAt(position + 1) } list.addAll(position + 1, data.toList()) StatementViewHelper.updateData("test_list", list) adapter.submitList(list.toList()) } adapter.submitList(list.toList())
扩展
日期选择DateStatement
- DataStatement
/**
* 日期选择
* @author byk
* @date 2022/2/8
*/
class DateStatement(
/**
* 是否显示时间
*/
var time: Boolean = false,
/**
* 是否显示日期
*/
var date: Boolean = true,
/**
* 所选时间戳
*/
var timeInMillis: Long = System.currentTimeMillis(),
important: Boolean = false,
help: String = "",
/**
* 默认显示
*/
hint: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* 输入内容
*/
text: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, hint, title, text, key) {
companion object {
@SuppressLint("SimpleDateFormat")
private val dateFormat = SimpleDateFormat("yyyy-MM-dd")
@SuppressLint("SimpleDateFormat")
private val timeFormat = SimpleDateFormat("HH:mm")
@SuppressLint("SimpleDateFormat")
private val format = SimpleDateFormat("yyyy-MM-dd HH:mm")
}
override fun save(): String {
return "\"${key}\":\"${timeInMillis}\""
}
init {
// 如果time和日期均为false默认只显示时间
if (!time && !date) {
date = true
}
}
override var type = 0
override fun init(holder: Any, position: Int) {
holder as StatementViewHolder
holder.binding.tvItemSTitle.text = title
holder.binding.tvItemS.text = text
holder.binding.tvItemS.hint = hint
updateTime(holder.binding, timeInMillis)
holder.binding.tvItemS.setOnClickListener {
it as TextView
DateTimePicker.instance.init(
title = hint,
date = date,
time = time,
start = timeInMillis
) { timeMillis ->
updateTime(holder.binding, timeMillis)
}.show(holder.itemView.context)
}
help(holder.binding.ivItemSHelp)
important(holder.binding.ivItemSImportant)
}
private fun updateTime(binding: ItemStatementBinding, timeMillis: Long) {
timeInMillis = timeMillis
if (time && date) {
text = format.format(timeInMillis)
} else if (time) {
text = timeFormat.format(timeInMillis)
} else if (date) {
text = dateFormat.format(timeInMillis)
}
binding.tvItemS.text = text
update(timeInMillis)
}
}
- 使用
DateStatement(
time = true,
title = "时间",
key = "time",
hint = "请选择时间",
important = true
)
文本输入EditTextStatement
- EditTextStatement
/**
* 输入框
* @author byk
* @date 2022年2月7日
*/
class EditTextStatement(
important: Boolean = false,
help: String = "",
/**
* 隐藏文字
*/
hint: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* 输入内容
*/
text: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, hint, title, text, key) {
override var type = 1
override fun init(holder: Any, position: Int) {
holder as EditTextViewHolder
holder.binding.let {
it.etItemSE.hint = hint
it.etItemSE.setText(text)
it.tvItemSETitle.text = title
it.etItemSE.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (s == null) {
this@EditTextStatement.text = ""
} else {
this@EditTextStatement.text = s.toString()
}
update(s)
}
})
}
help(holder.binding.ivItemSHelp)
important(holder.binding.ivItemSImportant)
}
}
- Helper :type == 1
1 -> {
val binding = ItemStatementEdittextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = EditTextViewHolder(binding.root)
holder.binding = binding
holder
- xml
<?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:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_e_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_e_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_e_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_e_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_e_title" />
<TextView
android:id="@+id/tv_item_s_e_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="16sp"
android:layout_marginTop="3dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/et_item_s_e" />
<EditText
android:id="@+id/et_item_s_e"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginEnd="16dp"
android:textSize="16sp"
android:gravity="top"
android:background="@drawable/select_statement"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用
EditTextStatement(
hint = "请输入标题",
title = "标题",
key = "name",
important = true
)
单选框RadioStatement
- RadioStatement
/**
* 传入单选文本,自动生成单选框
*/
class RadioStatement(
/**
* 单选框文本
*/
var radio: Array<String> = arrayOf(),
/**
* 单选框文本标识
*/
var radioId: Array<String> = arrayOf(),
/**
* 选中下标
*/
var select: Int = 0,
important: Boolean = false,
help: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, "", title, "", key) {
override var type = 3
override fun save(): String {
return "\"${key}\":\"${radioId[select]}\""
}
init {
if (radio.size != radioId.size) {
throw RuntimeException("选项和标识数量应保持一致")
}
if (select > radio.size) {
select = radio.size - 1
}
}
override fun init(holder: Any, position: Int) {
holder as RadioViewHolder
holder.binding.tvItemSTitle.text = title
holder.binding.rgItemRadio.let {
if (radio.size <= 3) {
// 横向
it.orientation = RadioGroup.HORIZONTAL
} else {
it.orientation = RadioGroup.VERTICAL
}
for (index in radio.indices) {
val button = RadioButton(holder.itemView.context)
button.text = radio[index]
button.id = -1 * (index + 100)
button.hint = radioId[index]
button.isChecked = select == index
it.addView(button)
}
it.setOnCheckedChangeListener { _, checkedId ->
select = -(checkedId + 100)
update(radioId[select])
}
}
help(holder.binding.ivItemSHelp)
important(holder.binding.ivItemSImportant)
}
}
- Helper :type == 3
3 -> {
val binding = ItemStatementRadioBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = RadioViewHolder(binding.root)
holder.binding= binding
holder
}
- xml
<?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="wrap_content"
android:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title" />
<TextView
android:id="@+id/tv_item_s_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="16sp"
android:layout_marginTop="6dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/rg_item_radio" />
<RadioGroup
android:id="@+id/rg_item_radio"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginRight="16dp"
android:background="@drawable/shape_statement"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent" >
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用
RadioStatement(
radio = arrayOf( "不重复", "每周", "每月", "每年"),
radioId = arrayOf("0", "1", "2", "3"),
title = "重复方式",
key = "repeat"
)
开关(动态)SwitchStatement
- SwitchStatement
/**
* 开关模块
* @author byk
* @date 2022/2/8
*/
class SwitchStatement(
/**
* 开关开启文本
*/
var on: String = "",
/**
* 开关关闭文本
*/
var off: String = "",
/**
* 是否选中
*/
var check: Boolean = false,
/**
* 所包含的子集
*/
var children: Array<Array<BaseStatement>> = arrayOf(),
/**
* 子集对应标识
*/
var childrenType: Array<String> = arrayOf(),
important: Boolean = false,
help: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, "", title, "", key) {
override var type = 2
/**
* 历史选择大小
*/
private var old: Int = -1
override fun save(): String {
return "\"${key}\":\"${check}\""
}
override fun init(holder: Any, position: Int) {
holder as SwitchViewHolder
holder.binding.tvItemSTitle.text = title
holder.binding.tvItemSSType.text = if (check) on else off
holder.binding.scItemSS.let {
it.isChecked = check
it.setOnCheckedChangeListener { _, isChecked ->
check = isChecked
select(holder.binding, position)
}
select(holder.binding, position)
}
help(holder.binding.ivItemSHelp)
important(holder.binding.ivItemSImportant)
}
/**
* 当发生改变时触发
*/
private fun select(binding: ItemStatementSwitchBinding, position: Int) {
val select = if (check) on else off
binding.tvItemSSType.text = select
// 判断所选内容
for (i in childrenType.indices) {
if (childrenType[i] == select) {
val selectStatements = children[i]
StatementViewHelper.updateMap[KEY]?.invoke(selectStatements, position, old)
old = selectStatements.size
}
}
update(check)
}
}
- Helper :type == 2
2 -> {
val binding = ItemStatementSwitchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = SwitchViewHolder(binding.root)
holder.binding= binding
holder
}
- xml
<?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="wrap_content"
android:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/tv_item_s_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="TextView"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sc_item_s_s" />
<TextView
android:id="@+id/tv_item_s_s_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/sc_item_s_s"
app:layout_constraintStart_toEndOf="@+id/sc_item_s_s"
app:layout_constraintTop_toTopOf="@+id/sc_item_s_s" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/sc_item_s_s"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginVertical="8dp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用
SwitchStatement(
on = "开启提醒",
off = "关闭提醒",
title = "是否提醒",
key = "ring",
children = arrayOf(
arrayOf(
DateStatement(
time = true,
title = "提醒时间",
key = "ringTime",
hint = "请选择提醒时间",
help = "",
)
), arrayOf(
TextViewStatement(
title = "提醒",
text = "将在未来某个时间不进行相关提醒",
key = "ringTime2",
hint = "请选择提醒时间"
)
)
),
childrenType = arrayOf("开启提醒", "关闭提醒")
)
数字输入NumberStatement
- NumberStatement
/**
* 数字选择
* @author byk
* @date 2022年2月13日
*/
class NumberStatement(
/**
* 选择内容
*/
var select: Float = 0f,
/**
* 最小
*/
var min: Float = 0f,
/**
* 最大
*/
var max: Float = Float.MAX_VALUE,
/**
* 间隔
*/
var space: Float = 1f,
/**
* 整数还是小数
* 默认整数切换
*/
var integer: Boolean = true,
important: Boolean = false,
help: String = "",
hint: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* 输入内容
*/
text: String = "",
/**
* json key
*/
key: String = ""
): BaseStatement(important, help, hint, title, text, key) {
override var type = 4
@SuppressLint("ClickableViewAccessibility")
override fun init(holder: Any, position: Int) {
holder as NumberViewHolder
holder.binding.let { binding ->
updateNumber(binding)
binding.tvItemSTitle.text = title
// 减少按钮
binding.ivItemSNSub.setOnClickListener {
if (select - space < min) {
return@setOnClickListener
}
select -= space
updateNumber(binding)
binding.etItemSNNumber.isEnabled = false
binding.etItemSNNumber.isEnabled = true
binding.etItemSNNumber.clearFocus()
}
// 增加按钮
binding.ivItemSNAdd.setOnClickListener {
if (select + space > max) {
return@setOnClickListener
}
select += space
updateNumber(binding)
binding.etItemSNNumber.isEnabled = false
binding.etItemSNNumber.isEnabled = true
binding.etItemSNNumber.clearFocus()
}
binding.etItemSNNumber.addTextChangedListener(object : TextWatcher {
private var deleteLastChar = false
private var startWithDot = false
private var startWithDotAnd = false
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
s?.let {
if (s.toString().contains(".")) {
val length: Int =
s.length - s.toString()
.lastIndexOf(".")
// 小数位
deleteLastChar = length >= 4
// 小数点开头
startWithDot =
s.toString().startsWith(".")
// -.开头
startWithDotAnd =
s.toString().startsWith("-.")
}
}
}
override fun afterTextChanged(s: Editable?) {
if (!TextUtils.isEmpty(s)) {
var str = s.toString()
if (deleteLastChar) {
str = s.toString().substring(0, s.toString().length - 1)
}
if (startWithDot) {
str = "0."
}
if (startWithDotAnd) {
str = "-0."
}
Log.i("lskjdf", str)
Log.i("ldjflj", str.toFloat().toString())
val toFloat = str.toFloat()
if (select == toFloat) {
return
}
select = toFloat
if (select < min) {
select = min
}
if (select > max) {
select = max
}
updateNumber(binding)
}
}
})
help(binding.ivItemSHelp)
important(binding.ivItemSImportant)
}
}
/**
* 更新所选文本
*/
private fun updateNumber(binding: ItemStatementNumberBinding) {
var value = select.toString()
if (integer) {
value = value.split(".")[0]
}
binding.etItemSNNumber.setText(value)
if (binding.etItemSNNumber.hasFocus()) {
binding.etItemSNNumber.setSelection(value.length)
}
update(value)
}
}
- Helper :type == 4
4 -> {
val binding = ItemStatementNumberBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = NumberViewHolder(binding.root)
holder.binding= binding
holder
}
- xml
<?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:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title" />
<TextView
android:id="@+id/tv_item_s_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:layout_marginTop="3dp"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/et_item_s_n_number" />
<ImageView
android:id="@+id/iv_item_s_n_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_mark_remove_circle" />
<EditText
android:id="@+id/et_item_s_n_number"
android:layout_width="108dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:background="@drawable/select_statement"
android:ems="10"
android:gravity="center"
android:layout_marginLeft="16dp"
android:inputType="number|numberDecimal"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@+id/iv_item_s_n_sub"
app:layout_constraintStart_toEndOf="@+id/iv_item_s_n_sub"
app:layout_constraintTop_toTopOf="@+id/iv_item_s_n_sub" />
<ImageView
android:id="@+id/iv_item_s_n_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/et_item_s_n_number"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_mark_add_circle" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用
NumberStatement(
title = "重要程度",
key = "important",
min = 0f,
max = 100f,
integer = true,
space = 1f,
help = "该数值越大在列表中显示越靠前"
)
图片选择PhotoStatement
- PhotoStatement
/**
* 使用FragmentContainerView来加载fragment的方式启动activity并获取回调
*/
class PhotoStatement (
var manager: FragmentManager,
var photos: Array<String> = arrayOf(),
important: Boolean = false,
help: String = "",
hint: String = "",
/**
* 显示标题
*/
title: String = "",
/**
* 输入内容
*/
text: String = "",
/**
* json key
*/
key: String = "",
): BaseStatement(important, help, hint, title, text, key) {
override var type = 5
private val photosList = mutableListOf<Photo>()
init {
for (photo in photos) {
photosList.add(Photo(path = photo, edit = false, cache = ""))
}
photosList.add(Photo(uuid = "add", path = "", edit = false, cache = ""))
}
override fun save(): String {
return "\"${key}\":\"${formatArray(photos)}\""
}
override fun init(holder: Any, position: Int) {
holder as PhotoViewHolder
holder.binding.tvItemSTitle.text = title
help(holder.binding.ivItemSHelp)
important(holder.binding.ivItemSImportant)
// 加载图片选择列表
val adapter = PhotoSelectedListAdapter()
holder.binding.recyclerView.adapter = adapter
adapter.submitList(photosList.toList())
// 点击事件
adapter.onPhotoClickListener = object : PhotoSelectedListAdapter.OnPhotoClickListener {
// 图片点击进入预览,可使用自己项目中的图片预览页面
override fun click(position: Int, photo: Photo) {
if (photo.uuid == "add") {
// 如果是添加,则加载fragment
manager.beginTransaction().add(R.id.fcv_item_s_p, PhotoHelperFragment.instance).commit()
return
}
PhotoRepo.repo.selectPhoto.value = photosList.subList(0, photosList.size - 1)
val context = holder.itemView.context
context.startActivity(Intent(context, PhotoAlbumPreviewActivity::class.java))
}
// 删除该图片
override fun delete(position: Int, photo: Photo) {
photosList.removeAt(position)
val toMutableList = photos.toMutableList()
toMutableList.removeAt(position)
photos = toMutableList.toTypedArray()
update(formatArray(photos))
adapter.submitList(photosList.toList())
}
}
// 初始化fragment
PhotoHelperFragment.instance.init {
Log.d("图片选择", it)
if (it != "cancel") {
val new = photos.toMutableList()
for (any in JSONArray.parseArray(it)) {
any as JSONObject
val path = any.getString("path")
if (!new.contains(path)) {
new.add(path)
photosList.add(photosList.size - 1, Photo(path = path, edit = false, cache = ""))
}
}
photos = new.toTypedArray()
adapter.submitList(photosList.toList())
update(formatArray(photos))
}
// 获取回调后,移除该fragment
manager.beginTransaction().remove(PhotoHelperFragment.instance).commit()
}
}
fun formatArray(photos: Array<String>): String {
if (photos.isEmpty()) {
return ""
}
val string = StringBuilder()
for (photo1 in photos) {
string.append(photo1).append(";")
}
return string.substring(0, string.length - 1)
}
}
- Helper :type == 5
5 -> {
val binding = ItemStatementPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = PhotoViewHolder(binding.root)
holder.binding = binding
holder
}
- PhotoSelectedListAdapter
class PhotoSelectedListAdapter: ListAdapter<Photo, PhotoSelectedListAdapter.AlbumItemViewHolder>(DIFF) {
lateinit var onPhotoClickListener : OnPhotoClickListener
companion object {
val DIFF = object : DiffUtil.ItemCallback<Photo>() {
override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean {
return oldItem.uuid == newItem.uuid
}
override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean {
return oldItem.cache == newItem.cache && oldItem.edit == newItem.edit && oldItem.path == newItem.path
}
}
}
class AlbumItemViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
lateinit var binding: ItemStatementPhotoSelectedBinding
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumItemViewHolder {
val binding: ItemStatementPhotoSelectedBinding =
ItemStatementPhotoSelectedBinding.inflate(LayoutInflater.from(parent.context))
val holder = AlbumItemViewHolder(binding.root)
holder.binding = binding
return holder
}
override fun onBindViewHolder(holder: AlbumItemViewHolder, position: Int) {
val photo: Photo = getItem(position)
val binding = holder.binding
if (photo.uuid == "add") {
binding.ivItemStatementPSDelete.visibility = View.GONE
binding.sivItemStatementPS.visibility = View.GONE
binding.imageView.visibility = View.VISIBLE
} else {
binding.imageView.visibility = View.GONE
Glide.with(holder.itemView.context)
.load(photo.path)
.into(binding.sivItemStatementPS)
binding.ivItemStatementPSDelete.visibility = View.VISIBLE
}
binding.cvItemStatementPS.setOnClickListener {
onPhotoClickListener.click(position, photo)
}
binding.ivItemStatementPSDelete.setOnClickListener {
onPhotoClickListener.delete(holder.adapterPosition, photo)
}
}
interface OnPhotoClickListener {
fun click(position: Int, photo: Photo)
fun delete(position: Int, photo: Photo)
}
}
- PhotoHelperFragment
/**
* 图片帮助
* @author byk
* @date 2022年2月17日
*/
class PhotoHelperFragment : BaseFragment() {
companion object {
val instance = PhotoHelperFragment()
}
lateinit var callback : (photos: String) -> Unit
fun init(
callback: (photos: String) -> Unit = this.callback
) {
this.callback = callback
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// 这里使用的是空页面,fragment_photo_helper没有任何内容
return inflater.inflate(R.layout.fragment_photo_helper, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 跳转至自有的图片选择框架,并设置回调
val intent = Intent(context, PhotoAlbumMainActivity::class.java)
intent.putExtra("max", 9)
startActivityForResult(intent, 1)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == 0) {
callback.invoke("cancel")
return
}
if (resultCode != -1) {
return
}
if (requestCode != 1) {
return
}
// 解析回调内容并传回PhotoStatement
data?.let {
val photo = it.getStringExtra("result") as String
callback.invoke(photo)
}
}
}
- xml
PhotoSelectedListAdapter
<?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:theme="@style/Theme.Topcoolcha">
<androidx.cardview.widget.CardView
android:id="@+id/cv_item_statement_p_s"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardBackgroundColor="@color/shapeBackground"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<top.coolcha.photo.widget.SquareImageView
android:id="@+id/siv_item_statement_p_s"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop" />
<top.coolcha.photo.widget.SquareImageView
android:id="@+id/imageView"
android:layout_gravity="center"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:srcCompat="@drawable/ic_mark_add"
app:tint="@color/textHintColor" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/iv_item_statement_p_s_delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:layout_marginTop="2dp"
android:layout_marginRight="2dp"
app:layout_constraintEnd_toEndOf="@+id/cv_item_statement_p_s"
app:layout_constraintTop_toTopOf="@+id/cv_item_statement_p_s"
app:srcCompat="@drawable/ic_mark_remove_circle"
app:tint="@android:color/holo_red_dark" />
</androidx.constraintlayout.widget.ConstraintLayout>
PhotoStatement
<?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="wrap_content"
android:theme="@style/Theme.Topcoolcha">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<ImageView
android:id="@+id/iv_item_s_help"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_item_s_title"
app:layout_constraintStart_toEndOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title"
app:srcCompat="@drawable/ic_mark_help" />
<TextView
android:id="@+id/iv_item_s_important"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
android:textColor="#FF0000"
android:text="*"
app:layout_constraintEnd_toStartOf="@+id/tv_item_s_title"
app:layout_constraintTop_toTopOf="@+id/tv_item_s_title" />
<TextView
android:id="@+id/tv_item_s_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="TextView"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginEnd="8dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fcv_item_s_p"
android:layout_width="1dp"
android:layout_height="1dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用
PhotoStatement(
title = "图片列表",
key = "photos",
manager = supportFragmentManager,
help = "一次最多可以选择9张图片,可以多次进行选择\n需要提供相关存储权限"
)