Android统一表单输入-Statement

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)
    }
}

具体使用

  1. 在指定布局中添加RecyclerView并且赋予androidx.recyclerview.widget.LinearLayoutManager线性布局
  2. 创建StatementListAdapter,并传递给RecyclerView
  3. 创建Statement列表,并传入对应的Statement对象
    val list = mutableListOf<BaseStatement>()
    list.add(
    	TextViewStatement(
    		title = "你好",
    		text = "Hello World",
    		key = "abc",
    		hint = "world"
    	)
    )
    
  4. 内容加载,更新监听,更新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需要提供相关存储权限"
)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

baiyunkai295

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值