From a96f9d9a8cb351a3000aacbb131f29eb65391746 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Mon, 15 Dec 2025 12:19:40 +0800 Subject: [PATCH 01/65] basic custom form --- app/src/main/AndroidManifest.xml | 3 + .../lab/container/CustomFormActivity.java | 126 +++++++++++++++++ .../huozige/lab/container/MainActivity.java | 2 + .../lab/container/proxy/LocalKvProxy.java | 39 ++++- .../proxy/support/customform/FormAdapter.java | 111 +++++++++++++++ .../customform/model/BaseFormItem.java | 35 +++++ .../customform/model/SelectFormItem.java | 76 ++++++++++ .../customform/model/TextFormItem.java | 80 +++++++++++ .../customform/viewholder/BaseViewHolder.java | 20 +++ .../viewholder/SelectViewHolder.java | 133 ++++++++++++++++++ .../customform/viewholder/TextViewHolder.java | 87 ++++++++++++ .../proxy/support/realm/LocalKvHelper.java | 59 ++++++++ .../proxy/support/realm/LocalKv_Bundle.java | 6 + .../container/utilities/ConfigManager.java | 15 +- .../custom_form_bg_edittext_error.xml | 6 + .../custom_form_bg_edittext_normal.xml | 6 + .../main/res/layout/custom_form_activity.xml | 38 +++++ .../layout/custom_form_item_form_select.xml | 60 ++++++++ .../res/layout/custom_form_item_form_text.xml | 51 +++++++ app/src/main/res/values/colors.xml | 3 + 20 files changed, 951 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/huozige/lab/container/CustomFormActivity.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/FormAdapter.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/BaseFormItem.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/SelectFormItem.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/TextFormItem.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/viewholder/BaseViewHolder.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/viewholder/SelectViewHolder.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/customform/viewholder/TextViewHolder.java create mode 100644 app/src/main/java/com/huozige/lab/container/proxy/support/realm/LocalKvHelper.java create mode 100644 app/src/main/res/drawable/custom_form_bg_edittext_error.xml create mode 100644 app/src/main/res/drawable/custom_form_bg_edittext_normal.xml create mode 100644 app/src/main/res/layout/custom_form_activity.xml create mode 100644 app/src/main/res/layout/custom_form_item_form_select.xml create mode 100644 app/src/main/res/layout/custom_form_item_form_text.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69c9c32..903215d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -143,6 +143,9 @@ android:name=".proxy.support.biometric.BiometricAuthActivity" android:exported="false" /> + + formItems = new ArrayList<>(); + + // 用户名 + TextFormItem usernameItem = new TextFormItem("username", "用户名", "请输入用户名", true); + usernameItem.setMinLength(1); + usernameItem.setMaxLength(20); + formItems.add(usernameItem); + + // 密码 + TextFormItem passwordItem = new TextFormItem("password", "密码", "请输入密码", true); + passwordItem.setInputType(android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + //formItems.add(passwordItem); + + // 邮箱 + TextFormItem emailItem = new TextFormItem("email", "邮箱", "请输入邮箱", true); + emailItem.setRegexPattern("^[A-Za-z0-9+_.-]+@(.+)$"); + //formItems.add(emailItem); + + // 性别选择 + SelectFormItem genderItem = new SelectFormItem("gender", "性别", "请选择性别", true); + genderItem.addOption("male", "男"); + genderItem.addOption("female", "女"); + genderItem.addOption("other", "其他"); + //formItems.add(genderItem); + + // 城市选择 + SelectFormItem cityItem = new SelectFormItem("city", "城市", "请选择城市", false); + cityItem.addOption("beijing", "北京"); + cityItem.addOption("shanghai", "上海"); + cityItem.addOption("guangzhou", "广州"); + cityItem.addOption("shenzhen", "深圳"); + //formItems.add(cityItem); + + adapter.setFormItems(formItems); + } + + private void setupListeners() { + btnSubmit.setOnClickListener(v -> onSubmit()); + } + + private void onSubmit() { + // 验证表单 + if (adapter.validateAll()) { + // 收集数据 + Map formData = adapter.collectFormData(); + + LocalKvHelper.upsertVRange(formData, null); + + Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "请检查表单输入", Toast.LENGTH_SHORT).show(); + // 滚动到第一个错误项 + scrollToFirstError(); + } + } + + private void scrollToFirstError() { + List items = adapter.getFormItems(); + for (int i = 0; i < items.size(); i++) { + if (items.get(i).getErrorMessage() != null) { + recyclerView.scrollToPosition(i); + break; + } + } + } +} diff --git a/app/src/main/java/com/huozige/lab/container/MainActivity.java b/app/src/main/java/com/huozige/lab/container/MainActivity.java index ab05b81..69b7959 100644 --- a/app/src/main/java/com/huozige/lab/container/MainActivity.java +++ b/app/src/main/java/com/huozige/lab/container/MainActivity.java @@ -61,6 +61,7 @@ public class MainActivity extends BaseActivity { static final int MENU_ID_SETTINGS = 2; static final int MENU_ID_HELP = 3; static final int MENU_ID_ABOUT = 4; + static final int MENU_ID_OFFLINE_PLUS = 5; @SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled"}) @Override @@ -312,6 +313,7 @@ public boolean onOptionsItemSelected(MenuItem item) { Toast.makeText(this, "应用处于离线模式,无法打开【设置】页面。", Toast.LENGTH_LONG).show(); } else { startActivity(new Intent(this, OptionSettingsActivity.class)); + //startActivity(new Intent(this, CustomFormActivity.class)); } break; case MENU_ID_HELP: diff --git a/app/src/main/java/com/huozige/lab/container/proxy/LocalKvProxy.java b/app/src/main/java/com/huozige/lab/container/proxy/LocalKvProxy.java index f6c670e..94b77d5 100644 --- a/app/src/main/java/com/huozige/lab/container/proxy/LocalKvProxy.java +++ b/app/src/main/java/com/huozige/lab/container/proxy/LocalKvProxy.java @@ -8,9 +8,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import io.realm.Realm; +import io.realm.RealmModel; /** * 让页面存取本地KV数据库 @@ -25,7 +29,6 @@ */ public class LocalKvProxy extends AbstractProxy { - private static final String VERSION_NA = "N/A"; private static final String VALUE_NA = "DATA_NOT_FOUND"; private static final String KEY_TEMPLATE = "%s|%s"; @@ -60,7 +63,7 @@ private String getEntryHost() { */ @JavascriptInterface public void upsert(String key, String valueString) { - upsertV(key, valueString, VERSION_NA); + upsertV(key, valueString, VERSION_DEFAULT); } /** @@ -72,7 +75,7 @@ public void upsert(String key, String valueString) { */ @JavascriptInterface public void retrieve(String key, String cell) { - retrieveV(key, VERSION_NA, cell); + retrieveV(key, VERSION_DEFAULT, cell); } /** @@ -83,7 +86,7 @@ public void retrieve(String key, String cell) { */ @JavascriptInterface public String retrieve2(String key) { - return retrieveV2(key, VERSION_NA); + return retrieveV2(key, VERSION_DEFAULT); } /** @@ -119,6 +122,34 @@ public void upsertV(String key, String valueString, String version) { }); } + public void upsertVRange(Map valueList, String version) { + + registryForFeatureUsageAnalyze("use_localdb_feature", "upsert"); + + if (null == version) { + version = VERSION_DEFAULT; + } + + String finalVersion = version; + writeInfoLog("批量更新本地缓存中的项目,数量" + valueList.size() + ",版本:" + finalVersion); + + Realm.getDefaultInstance().executeTransaction(transactionRealm -> { + + Collection bundles = Collections.emptyList(); + + valueList.forEach((String key, String valueString) -> { + + String bKey = String.format(KEY_TEMPLATE, getEntryHost(), key); + + bundles.add(new LocalKv_Bundle(bKey, valueString, finalVersion)); + }); + transactionRealm.insertOrUpdate(bundles); + + + writeInfoLog("本地缓存已更新"); + }); + } + /** * 注册到页面的 localKv.retrieveV(key,version,cell)方法 * 从本地数据库中查找值,并写入单元格 diff --git a/app/src/main/java/com/huozige/lab/container/proxy/support/customform/FormAdapter.java b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/FormAdapter.java new file mode 100644 index 0000000..7d41434 --- /dev/null +++ b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/FormAdapter.java @@ -0,0 +1,111 @@ +package com.huozige.lab.container.proxy.support.customform; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.huozige.lab.container.R; +import com.huozige.lab.container.proxy.support.customform.model.BaseFormItem; +import com.huozige.lab.container.proxy.support.customform.viewholder.BaseViewHolder; +import com.huozige.lab.container.proxy.support.customform.viewholder.SelectViewHolder; +import com.huozige.lab.container.proxy.support.customform.viewholder.TextViewHolder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FormAdapter extends RecyclerView.Adapter { + + private List formItems = new ArrayList<>(); + + public void setFormItems(List items) { + this.formItems = new ArrayList<>(items); + notifyDataSetChanged(); + } + + public void addFormItem(BaseFormItem item) { + formItems.add(item); + notifyItemInserted(formItems.size() - 1); + } + + public List getFormItems() { + return new ArrayList<>(formItems); + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + switch (viewType) { + case BaseFormItem.TYPE_TEXT: + View textView = inflater.inflate(R.layout.custom_form_item_form_text, parent, false); + return new TextViewHolder(textView); + + case BaseFormItem.TYPE_SELECT: + View selectView = inflater.inflate(R.layout.custom_form_item_form_select, parent, false); + return new SelectViewHolder(selectView); + +// case BaseFormItem.TYPE_SWITCH: +// View switchView = inflater.inflate(R.layout.item_form_switch, parent, false); +// return new SwitchViewHolder(switchView); +// +// case BaseFormItem.TYPE_DATE: +// View dateView = inflater.inflate(R.layout.item_form_date, parent, false); +// return new DateViewHolder(dateView); + + default: + throw new IllegalArgumentException("Unknown view type: " + viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { + BaseFormItem item = formItems.get(position); + holder.bind(item, position); + } + + @Override + public int getItemCount() { + return formItems.size(); + } + + @Override + public int getItemViewType(int position) { + return formItems.get(position).getItemType(); + } + + // 验证所有表单项 + public boolean validateAll() { + boolean isValid = true; + + for (int i = 0; i < formItems.size(); i++) { + BaseFormItem item = formItems.get(i); + if (!item.validate()) { + isValid = false; + // 更新对应项的UI + notifyItemChanged(i); + } else { + item.clearError(); + notifyItemChanged(i); + } + } + + return isValid; + } + + // 收集表单数据 + public Map collectFormData() { + Map formData = new HashMap<>(); + + for (BaseFormItem item : formItems) { + formData.put(item.getId(), item.getValue()); + } + + return formData; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/BaseFormItem.java b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/BaseFormItem.java new file mode 100644 index 0000000..0f1976a --- /dev/null +++ b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/BaseFormItem.java @@ -0,0 +1,35 @@ +package com.huozige.lab.container.proxy.support.customform.model; + +public abstract class BaseFormItem { + public static final int TYPE_TEXT = 1; + public static final int TYPE_SELECT = 2; + public static final int TYPE_SWITCH = 3; + public static final int TYPE_DATE = 4; + + private String id; + private String title; + private String hint; + private boolean required; + private String errorMessage; + + public BaseFormItem(String id, String title, String hint, boolean required) { + this.id = id; + this.title = title; + this.hint = hint; + this.required = required; + } + + public abstract int getItemType(); + + // Getter/Setter + public String getId() { return id; } + public String getTitle() { return title; } + public String getHint() { return hint; } + public boolean isRequired() { return required; } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public abstract String getValue(); + public abstract boolean validate(); + public abstract void clearError(); +} \ No newline at end of file diff --git a/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/SelectFormItem.java b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/SelectFormItem.java new file mode 100644 index 0000000..41090cb --- /dev/null +++ b/app/src/main/java/com/huozige/lab/container/proxy/support/customform/model/SelectFormItem.java @@ -0,0 +1,76 @@ +package com.huozige.lab.container.proxy.support.customform.model; + +import java.util.ArrayList; +import java.util.List; + +public class SelectFormItem extends BaseFormItem { + public static class Option { + private String value; + private String displayText; + + public Option(String value, String displayText) { + this.value = value; + this.displayText = displayText; + } + + public String getValue() { return value; } + public String getDisplayText() { return displayText; } + } + + private List