美文网首页
Android 动态表单的实现思路

Android 动态表单的实现思路

作者: jianboo | 来源:发表于2024-08-28 13:57 被阅读0次

    一、动态表单介绍

    一些Android项目中需要用到大量的表单提交,比如各种OA系统等,需要处理流程,主表显示,回报表填写,各种字段需要处理,需要进行动态的配置下发,对表单域进行解析处理,灵活配置,可以提高代码和项目的复用性。下面说说我的思路。

    二、表单界面

    列表 表单界面

    表单组件有灰色背景的分组标签和具体的表单域组成。

    表单域由标签文字和具体需要填写和显示的数据组成。

    三、代码目录结构

    目录结构
    • 项目整体采用MVVM框架,使用databinding,主要使用kotlin和部分Java代码
    • 依赖注入使用了Koin
    • 数据库选择了Room
    • 网络请求使用了retrofit
    • 推送使用了极光推送
    • 使用了阿里的路由,处理各种页面之间的跳转
    • 部分事件处理使用了eventbus
    • 图片加载使用了Gilde
    • 缓存使用腾讯的mmkv开源框架,可以跨进程,性能优异。
    • 地图和位置轨迹等使用了百度地图开放平台

    四、动态表单部分

    表单控件类型主要包括文本、单选、多选、下拉、手写签名、照片视频、位置、文件上传、分组控件等。主要思想就是面向对象的特点,封装、继承、多态和抽象。

    1、FormWidget

    主要处理一些通用的方法,比如取值,标签名称,view是否只读等

    package com.sfy.gas.base
    
    import android.widget.TextView
    import com.sfy.gas.db.entities.form.FormTableDataFileDTO
    import com.sfy.gas.db.entities.form.FormWidgetEntity
    import com.sfy.gas.utils.FormAction
    
    /**
     *    @Author : Cook
     *    @Date   : 2022/2/8
     *    @Desc   :
     */
    open class FormWidget(open val formItem: FormWidgetEntity?, val formAction: FormAction) {
    
        protected val TAG: String = this.javaClass.simpleName
    
        /**
         * 当前控件是否只读
         */
        private var enable: Boolean = true
    
        /**
         * 获取当前控件的值
         */
        open fun getValue(): String = ""
    
        /**
         * 设置控件的值 限文本
         */
        open fun setValue(value: String) {}
    
        /**
         * 设置是否只读
         */
        open fun setEnable(enable: Boolean) {
            this.enable = enable
        }
    
        /**
         * 获取控件只读状态
         */
        open fun getEnable(): Boolean = enable
    
        var fieldName: String? = ""
    
        /**
         * 获取标签名称
         */
        open fun getLabelName(): String = "${formItem?.ITEMNAME}:"
    
        /**
         * 设置标签名称
         */
        fun setLabelName(tv: TextView) {
            tv.text = getLabelName()
        }
    
    }
    

    2、BaseFormWidget

    处理view加载,一些通用方法等

    package com.sfy.gas.base
    
    import android.util.Log
    import android.view.LayoutInflater
    import android.view.View
    import android.widget.TextView
    import androidx.annotation.LayoutRes
    import androidx.appcompat.app.AppCompatActivity
    import androidx.databinding.DataBindingUtil
    import androidx.databinding.ViewDataBinding
    import com.alibaba.android.arouter.launcher.ARouter
    import com.blankj.utilcode.util.ThreadUtils
    import com.kongzue.dialogx.dialogs.MessageDialog
    import com.kongzue.dialogx.dialogs.TipDialog
    import com.kongzue.dialogx.dialogs.WaitDialog
    import com.sfy.gas.R
    import com.sfy.gas.db.entities.form.FormTableDataFileDTO
    import com.sfy.gas.db.entities.form.FormWidgetEntity
    import com.sfy.gas.module.common.image.ImagePreviewActivity
    import com.sfy.gas.utils.ARouterPath
    import com.sfy.gas.utils.Constants
    import com.sfy.gas.utils.FormAction
    import com.sfy.gas.utils.ext.setLeftDrawable
    
    /**
     *    @Author : Cook
     *    @Date   : 2024/1/18
     *    @Desc   :
     */
    abstract class BaseFormWidget<T : ViewDataBinding>(
        protected val context: AppCompatActivity,
        formItem: FormWidgetEntity?, formAction: FormAction
    ) : FormWidget(formItem, formAction) {
        var binding: T
    
        fun <T : ViewDataBinding> binding(
            @LayoutRes layoutId: Int
        ): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, null, false)
    
        init {
            binding = binding(getLayoutID())
            initWidget()
        }
    
        /**
         * 初始化控件
         */
        private fun initWidget() {
    
            getLabelTextView()?.text = getLabelName()
    
            // setEnable(formItem?.ISREADONLY == 0)
    
            if (formItem?.ISREQUIRED == 1) {
                getLabelTextView()?.setLeftDrawable(R.drawable.icon_must)
            }
        }
    
        /**
         * 获取当前view
         */
        fun getView(): View {
            return binding.root
        }
    
        /**
         * 当前控件是否可见
         */
        fun isVisible(): Boolean {
            return binding.root.visibility == View.VISIBLE
        }
    
        /**
         * 获取标签的view
         */
        open fun getLabelTextView(): TextView? = null
    
        /**
         * 获取布局layoutID
         */
        abstract fun getLayoutID(): Int
    
        /**
         * 跳转 activity
         */
        fun startActivity(title: String? = "--", routePath: String) {
            ARouter.getInstance()
                .build(routePath)
                .withString(Constants.ACTIVITY_TITLE, title)
                .navigation()
        }
    
        /**
         * 弹窗
         */
        fun showDialog(tips: String, action: () -> Unit) {
    
            val builder = MessageDialog.build()
    
            builder
                .setTitle("提示")
                .setMessage(tips)
                .setCancelButton("取消") { _, _ ->
                    return@setCancelButton false
                }
                .setOkButton("确定") { dialog, _ ->
                    dialog.dismiss()
                    action()
                    return@setOkButton true
                }
                .show()
        }
    
        /**
         * 加载
         */
        fun showLoading(msg: String = "加载中...") {
            ThreadUtils.runOnUiThread {
                WaitDialog.show(msg)
            }
    
        }
    
        /**
         * 隐藏dialog
         */
        fun dismissDialog() {
            ThreadUtils.runOnUiThread {
                WaitDialog.dismiss()
            }
        }
    
        /**
         * 显示toast
         */
        fun showTips(msg: String) {
            ThreadUtils.runOnUiThread {
                TipDialog.show(msg, WaitDialog.TYPE.ERROR)
            }
        }
    }
    

    3、FormAction

    定义钩子,从宿主activity获取需要的action

    package com.sfy.gas.utils
    
    import com.sfy.gas.db.entities.form.FormTableDataFileDTO
    import com.sfy.gas.db.entities.form.FormWidgetEntity
    import java.text.FieldPosition
    
    /**
     *    @Author : Cook
     *    @Date   : 2022/3/4
     *    @Desc   :
     */
    interface FormAction {
        /**
         *获取图片列表
         */
        fun getImageList(
            filedName: String,
            formTableID: String,
            result: (data: List<FormTableDataFileDTO>) -> Unit
        )
    
        /**
         * 根据 filedName 获取字段值
         */
        fun getWidgetValue(filedName: String): String
    
        /**
         * 获取此条表单的数据ID
         */
        fun getUID(): String
    
        /**
         * 选取文件
         */
        fun startChooseFile(formItem: FormWidgetEntity)
    
        /**
         * 删除文件
         */
        fun deleteFile(position: Int)
    
    }
    

    4、FormWidgetEntity

    表单域实体类,定义一些参数,类型等

    package com.sfy.gas.db.entities.form
    
    import java.util.*
    
    /**
     *    @Author : Cook
     *    @Date   : 2024/2/28
     *    @Desc   :
     */
    class FormWidgetEntity {
        var ID = 0
        var FORMTABLEID = 0
        var IORDER = 0
        var LISTORDER = 0
        var VIEWORDER = 0
        var ITEMNAME: String = ""
        /**
         * 字段名
         */
        var FIELDNAME: String = ""
        var ITEMTYPE: String = ""
        var ITEMVALUE: String? = null
        var DEFAULTVALUE: String = ""
        var ISREQUIRED = 0
        var ISDELETED: String? = null
        var ISVIRTUAL = 0
        var ISREADONLY = 0
        var ISSYSTEM: Int = 0
    }
    

    5、实现栗子

    EditTextFormWidget,输入文本框

    package com.sfy.gas.view.widget
    
    import android.text.InputType
    import androidx.appcompat.app.AppCompatActivity
    import com.sfy.gas.R
    import com.sfy.gas.base.BaseFormWidget
    import com.sfy.gas.databinding.FormTextBinding
    import com.sfy.gas.db.entities.form.FormReportItemDTO
    import com.sfy.gas.db.entities.form.FormWidgetEntity
    import com.sfy.gas.utils.FormAction
    import com.sfy.gas.utils.UIType
    
    /**
     *    @Author : Cook
     *    @Date   : 2024/1/18
     *    @Desc   :
     */
    class EditTextFormWidget(
        context: AppCompatActivity,
        formItem: FormWidgetEntity,
        formAction: FormAction
    ) :
        BaseFormWidget<FormTextBinding>(
            context,
            formItem, formAction
        ) {
    
        init {
            initEditType()
        }
    
        /**
         * 获取当前值
         */
        override fun getValue(): String {
            return binding.formInput.text.toString()
        }
    
        /**
         * 返回布局layout ID
         */
        override fun getLayoutID() = R.layout.form_text
    
        /**
         * 设置值
         */
        override fun setValue(value: String) {
            binding.formInput.setText(value)
        }
    
        /**
         * 设置是否只读
         */
        override fun setEnable(enable: Boolean) {
            binding.formInput.isEnabled = enable
        }
    
        /**
         * 返回标签view
         */
        override fun getLabelTextView() = binding.label
    
        /**
         * 初始化输入框类型 解析formItem字段
         */
        private fun initEditType() {
            formItem?.let {
                when (UIType.valueOf(it.ITEMTYPE)) {
                    UIType.Numberic -> {
                        binding.formInput.inputType = InputType.TYPE_CLASS_NUMBER
                    }
    
                    else -> {}
                }
            }
    
        }
    
    }
    

    6、GroupFormWidget

    分组标签,分组显示使用。

    package com.sfy.gas.view.widget
    
    import androidx.appcompat.app.AppCompatActivity
    import com.sfy.gas.R
    import com.sfy.gas.base.BaseFormWidget
    import com.sfy.gas.databinding.FormGroupBinding
    import com.sfy.gas.utils.FormAction
    
    /**
     *    @Author : Cook
     *    @Date   : 2024/2/8
     *    @Desc   :
     */
    class GroupFormWidget(context: AppCompatActivity, formAction: FormAction) :
        BaseFormWidget<FormGroupBinding>(context, null, formAction) {
    
        override fun getLayoutID() = R.layout.form_group
    
        override fun setValue(value: String) {
    
            binding.text.text = value.ifEmpty { "基本信息" }
        }
    
    }
    

    7、DynamicFormHelper

    工具类,根据数据类型返回对应的Wdiget

    package com.sfy.gas.utils
    
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    import com.sfy.gas.base.BaseFormWidget
    import com.sfy.gas.db.entities.form.FormWidgetEntity
    import com.sfy.gas.view.widget.*
    
    /**
     *    @Author : Cook
     *    @Date   : 2024/2/8
     *    @Desc   :
     */
    object DynamicFormHelper {
    
        fun getWidget(
            context: AppCompatActivity,
            formReportItem: FormWidgetEntity, action: FormAction
        ): BaseFormWidget<*> {
            Log.i("getWidget", "itemType=${formReportItem.ITEMTYPE}")
            return when (UIType.valueOf(formReportItem.ITEMTYPE)) {
                UIType.HSingleLineText -> {
                    EditTextFormWidget(context, formReportItem, action)
                }
    
                UIType.HTime, UIType.HDate, UIType.HDateTime, UIType.DateTime, UIType.Date, UIType.Time -> {
                    DateTimeWidget(context, formReportItem, action)
                }
                UIType.HRadioBox, UIType.RadioBox -> {
                    RadioBoxWidget(context, formReportItem, action)
                }
    
                UIType.HList, UIType.HDropList, UIType.DropList -> {
                    SpinnerFormWidget(context, formReportItem, action)
                }
                UIType.Image, UIType.HImage, UIType.HVideo, UIType.Video -> {
                    MediaFormWidget(context, formReportItem, action)
                }
                UIType.TextArea, UIType.HTextArea -> {
                    TextAreaFormWidget(context, formReportItem, action)
                }
                UIType.ChooseButton -> {
                    FormListWidget(context, formReportItem, action)
                }
                UIType.SubForm, UIType.HSubForm -> {
                    SubFormWidget(context, formReportItem, action)
                }
                UIType.HDrawing, UIType.Drawing -> {
                    HandWriteWidget(context, formReportItem, action)
                }
                UIType.Audio, UIType.HAudio -> {
                    AudioFormWidget(context, formReportItem, action)
                }
                UIType.File, UIType.HFile -> {
                    FileWidget(context, formReportItem, action)
                }
                else -> {
                    EditTextFormWidget(context, formReportItem, action)
                }
            }
        }
    }
    

    8、宿主Activity的处理

    1、主要加载流程

    1. 从本地数据库或者服务器获取表单数据
    2. 解析并加载表单数据
      1. 先根据FormItem,通过上面的工具类DynamicFormHelper获取表单域view。
      2. 根据分组信息,把表单域view存放到map里面。
      3. 通过容器layout,加载表单域view。
      4. 处理加载一些有默认值的情况,比如是否隐藏,是否可用等
     /**
         * 表单域的原始数据列表
         */
        private var formTableItemList: List<FormTableItemDTO> = arrayListOf()
    
        /**
         * 根据分组存放表单数据 key是groupName 分组名称
         */
        private val widgetGroupMap = HashMap<String, ArrayList<BaseFormWidget<*>>>()
    
        /**
         * 存放表单域的map key是fieldName
         */
        private val widgetMap = HashMap<String, BaseFormWidget<*>>()
    
    private fun parseFormList() {
            CoroutineScope(Dispatchers.Main).launch {
                for (data in formTableItemList) {
    
                    val widgetData = data.toWidgetEntity()
                    val widget = DynamicFormHelper.getWidget(
                        this@DynamicMainFormActivity,
                        widgetData,
                        this@DynamicMainFormActivity
                    )
                    widgetMap[widgetData.FIELDNAME] = widget
                    val groupName = widgetData.GROUPNAME ?: ""
    
                    var listWidget = widgetGroupMap[groupName]
    
                    if (listWidget == null) {
                        sortList.add(groupName)
                        listWidget = arrayListOf()
                    }
    
                    listWidget.add(widget)
                    widgetGroupMap[groupName] = listWidget
                    val type = UIType.valueOf(widgetData.ITEMTYPE)
                    if (type == UIType.Audio || type == UIType.HAudio) {
                        needAudio = true
                        audioField = widgetData.FIELDNAME
                    }
    
                }
    
                for (fieldName in sortList) {
                    val value = widgetGroupMap[fieldName]
                    if (value.isNullOrEmpty()) {
                        continue
                    }
                    val groupView = GroupFormWidget(
                        this@DynamicMainFormActivity,
                        this@DynamicMainFormActivity
                    ).apply {
                        setValue(fieldName)
                    }.getView()
                    binding.contentLayout.addView(groupView)
    
                    var count = 0
                    for (data in value) {
    
                        val widgetView = data.getView()
                        binding.contentLayout.addView(data.getView())
                        val key = data.formItem?.FIELDNAME ?: ""
                        val mapDataValue = dataMap[key]?.toString() ?: ""
    
                        data.formItem?.let {
                            val dataValue = mapDataValue.ifEmpty {
                                when (it.DEFAULTVALUETYPE) {
                                    "1", "4" -> {
                                        dataMap[key]?.toString() ?: dataMap[it.DEFAULTVALUE]?.toString()
                                        ?: ""
                                    }
                                    "0" -> {
                                        data.formItem?.DEFAULTVALUE ?: ""
                                    }
                                    "6" -> {
                                        withContext(Dispatchers.IO) {
                                            DBOptionUtils.genAutoNumber(
                                                data.formItem?.DEFAULTVALUE,
                                                "$tableid",
                                                key
                                            ).Second
                                        }
                                    }
                                    else -> {
                                        GeneralParams.getValue(it.DEFAULTVALUE)
                                    }
                                }
                            }
    
                            data.setValue(dataValue)
                            if (it.ISSYSTEM == 1 || it.ISHIDDEN == 1) {
                                widgetView.isGone = true
                                count++
                            }
                        }
                        if (isNew == true) {
                            data.setEnable(data.formItem?.ISREADONLY == 0)
                        } else {
                            data.setEnable(false)
                        }
                    }
                    if (count == value.size) {
                        groupView.isGone = true
                    }
    
                }
                if (needAudio) {
    
                    RecordManager.getInstance().setRecordResultListener {
                        Log.e(TAG, "${it.path}")
                        val createDate = AppUtils.getOracleDateTimeNowSSS()
                        val dataFileDTO = FormTableDataFileDTO().apply {
                            ID = AppUtils.getUUID()
                            FORMTABLEID = "$tableid"
                            FIELDNAME = audioField
                            FILENAME = FileUtils.getFileName(it.path)
                            FILEPATH = it.path
                            CREATER = CacheHelper.accountID
                            CREATEDATE = createDate
                            FILETYPE = Constants.AUDIO
                            FILESIZE = FileUtils.getLength(it.path).toDouble()
                            PROCESSED = "-1"
                        }
                        mediaMap[audioField] = arrayListOf(dataFileDTO)
                    }
    
                    RecordManager.getInstance().start()
                }
            }
        }
    

    2、表单数据校验

    对于用户输入的数据需要做一个合法性校验,有的可以在输入的时候做限制,比如一个数字文本,输入的时候就可以限制只能输入数据,其他的就需要在提交的时候校验,比如必填项不能为空等

     private fun check(): Tuple<Boolean, String> {
            for (value in widgetMap) {
                val widget = value.value
                if (widget.isVisible()
                        .not() && widget.formItem?.ISHIDDEN != 1 && widget.formItem?.ISSYSTEM != 1
                ) {
                    continue
                }
    
                if (widget is MediaFormWidget || widget is AudioFormWidget || widget is HandWriteWidget || widget is FileWidget) {
                    if (widget.formItem?.ISREQUIRED == 1) {
                        if (mediaMap[widget.formItem?.FIELDNAME].isNullOrEmpty()) {
                            return Tuple(false, "【${widget.formItem?.ITEMNAME}】不能为空")
                        }
                        continue
                    }
                    continue
    
                }
                if (widget.formItem?.ISREQUIRED == 1) {
                    if (widget.getValue().trim().isEmpty()) {
                        return Tuple(false, "【${widget.formItem?.ITEMNAME}】不能为空")
                    }
                }
            }
            return Tuple(true, "")
        }
    

    3、提交表单

    主要就是获取用户填写的内容,整理并保存到本地或者直接上传云端

    整理数据主要根据widgetMap来处理,遍历widgetMap获取对应的表单域,通过getValue抽象方法获取对应的值,组成map上传到服务端并存到本地。

     suspend fun uploadMainTableData(
            tableID: String, tableName: String,
            widgetMap: HashMap<String, BaseFormWidget<*>>,
            mediaMap: HashMap<String, ArrayList<FormTableDataFileDTO>>
        ): ResultResponse {
            val id = AppUtils.getUUID()
            val stateName = formTableWorkFlowDAO.queryInsertState(tableID)?.STATENAME ?: ""
            val dataMap = HashMap<String, Any>().apply {
                put("FORMTABLEID", tableID)
                put("TABLENAME", tableName)
                put("GUID", id)
                put("STATE", stateName)
            }
            for ((key, value) in widgetMap) {
                val isVisible = value.isVisible()
                val formItem = value.formItem ?: FormWidgetEntity()
                if (isVisible.not() && formItem.ISHIDDEN != 1 && formItem.ISSYSTEM != 1) {
                    continue
                }
    
                if (value is MediaFormWidget || value is HandWriteWidget || value is AudioFormWidget|| value is FileWidget) {
                    continue
                }
    
                if (formItem.DEFAULTVALUETYPE == "6") {
                    val code = CacheHelper.getAutoCode(tableID)
                    val sequenceDTO = formCategorySequenceDAO.queryCode(tableID, formItem.FIELDNAME)
                    sequenceDTO?.CODE = code
                    sequenceDTO?.let { formCategorySequenceDAO.update(it) }
                }
    
                dataMap[key] = value.getValue()
    
            }
            val waterText = AppUtils.getWaterText()
            val textColor = ColorUtils.getColor(R.color.panda)
    
            for ((key, value) in mediaMap) {
                val mediaList = arrayListOf<MediaListParams>()
                for (media in value) {
                    var path: String
    
                    media.FORMTABLEDATAID = id
                    media.FILEPATH?.let {
                        if (media.FILETYPE == Constants.PHOTO) {
                            val imageFile = File(it)
                            val bitmap = ImageUtils.getBitmap(imageFile)
                            val waterBitmap = ImageUtils.addTextWatermark(
                                bitmap,
                                waterText,
                                30,
                                textColor,
                                30F,
                                30F
                            )
                            ImageUtils.save(
                                waterBitmap,
                                imageFile.path,
                                Bitmap.CompressFormat.PNG,
                                true
                            )
                            val compressedImageFile = Compressor.compress(Utils.getApp(), imageFile)
                            media.FILEPATH = compressedImageFile.path
                            media.FILENAME = FileUtils.getFileName(compressedImageFile)
                            media.FILESIZE = FileUtils.getLength(compressedImageFile).toDouble()
                            path = compressedImageFile.path
                        } else {
                            path = it
                        }
    
                        val uploadPath = AppUtils.getUploadFilePath() + media.FILENAME
                        val result = uploadFileRepository.upload(path, uploadPath)
    
                        media.PROCESSED = if (result.code == SUCCESS_CODE) {
                            "1"
                        } else {
                            "-1"
                        }
                        mediaList.add(MediaListParams(uploadPath))
                        if (media.PROCESSED != "1") {
                            formTableDataFileDAO.insert(media)
                        }
    
                    }
    
                }
    
                dataMap[key] = mediaList
            }
        val map = HashMap<String, String>().apply {
                put("command", "directlabel")
                put("compress", "")
            }
            val params =
                UploadMainTableParams(data = arrayListOf<HashMap<String, Any>>().apply { add(dataMap) })
    
            val result = resultCall {
                service.uploadMainTableData(map, params)
            }
    

    以上就是我对Android动态表单的处理思路,欢迎交流,有需要源码的私信我。

    相关文章

      网友评论

          本文标题:Android 动态表单的实现思路

          本文链接:https://www.haomeiwen.com/subject/krdtljtx.html