├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── xyz │ │ └── codedream │ │ └── lib │ │ └── form │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── xyz │ │ │ └── codedream │ │ │ └── lib │ │ │ └── form │ │ │ └── sample │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── xyz │ └── codedream │ └── lib │ └── form │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── xyz │ │ └── codedream │ │ └── lib │ │ └── form │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── xyz │ │ │ └── codedream │ │ │ └── lib │ │ │ └── form │ │ │ ├── AbstractFormField.java │ │ │ ├── BaseViewFormField.java │ │ │ ├── FieldOptions.java │ │ │ ├── FieldResultWriter.java │ │ │ ├── FormField.java │ │ │ ├── FormLayout.java │ │ │ ├── Validator.java │ │ │ ├── ValueConvertor.java │ │ │ ├── annotion │ │ │ ├── FField.java │ │ │ ├── FFieldInput.java │ │ │ ├── FFieldLength.java │ │ │ ├── FFieldLimit.java │ │ │ ├── FFieldNumberRange.java │ │ │ ├── FFieldOptions.java │ │ │ ├── FFieldValidator.java │ │ │ └── Form.java │ │ │ ├── field │ │ │ ├── FormFieldDivider.java │ │ │ ├── FormFieldEditText.java │ │ │ ├── FormFieldMoneyEditText.java │ │ │ ├── FormFieldSpinner.java │ │ │ └── FormFieldTitle.java │ │ │ ├── validator │ │ │ ├── BaseValidator.java │ │ │ ├── ExpressionValidator.java │ │ │ ├── LengthValidator.java │ │ │ ├── NotNullValidator.java │ │ │ └── NumberRangeValidator.java │ │ │ └── window │ │ │ └── DialogSpinnerSelect.java │ └── res │ │ ├── drawable-hdpi │ │ ├── bg_dialog_form_spinner.9.png │ │ ├── bg_edit_style_1.9.png │ │ ├── ic_pwd_eye_hide.png │ │ └── ic_pwd_eye_visible.png │ │ ├── drawable-xxhdpi │ │ └── form_field_end_arrow.png │ │ ├── drawable │ │ ├── btn_pwd_switch.xml │ │ ├── form_spinner_selector.xml │ │ └── text_cursor.xml │ │ ├── layout │ │ ├── dialog_form_spinner.xml │ │ ├── dialog_select_date.xml │ │ ├── field_btn_pwd.xml │ │ ├── field_divider.xml │ │ ├── field_edittext.xml │ │ ├── field_edittext_hint_title.xml │ │ ├── field_spinner.xml │ │ ├── field_spinner_item.xml │ │ └── field_title.xml │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── xyz │ └── codedream │ └── lib │ └── form │ └── ExampleUnitTest.java ├── screenshot ├── result.png └── validate.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | C:\Users\skg\AppData\Roaming\Subversion 48 | 49 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 为什么开发FormLayout? 2 | 3 | 在Android中如果一个表单里有很多个表单项(FormField),甚至是多个页面都有类似的表单的需求,那么这些表单布局编写起来是很繁重的,而且需求变动,难以维护。即使编写不是问题,FormField的逐条验证,逐条提取数据绑定,也是非常头疼和难以维护的。为了解决这些问题,开发了FormLayout组建。 4 | 5 | ### FormLayout设计要求 6 | 1,支持根据model属性,自动生成FormField UI。 7 | 2,支持表单项内容合法性自动校验。 8 | 3,支持表单项从UI到model的反向赋值。 9 | 4,支持自定义验证规则。 10 | 5,支持model属性默认值配置(直接给变量赋值,即可解析为默认值) 11 | 12 | ### FormLayout的实现思路 13 | A.根据Mode生成UI 14 | 通过注解描述model字段属性,包括数据源类型,数据校验规则,默认值等,然后由FormLayout解析并绑定到FormField UI。 15 | Form表单UI支持扩展,添加,需要从FormField继承或者FormField的子类继承。 16 | 17 | B.数据验证支持 18 | 数据校验依赖于Validator链式校验借口,可以组成校验连,每个链节点支持多个子节点。 19 | Validator借口支持默认错误信息,自定义错误信息,模版错误信息。 20 | 21 | C.数据校验工作流程 22 | 当FormLayout调用validateForm();的时候,FormLayout会遍历FormField,并拿到FormField的约束规则,自动校验,如发现任何一个条件失败,则终止校验并通知失败。 23 | 24 | D.数据反填到model 25 | 在解析model属性的时候会同时获取model属性的java.lang.reflect.Field对象,在有了Field和model以后,就可以把FormField的value反填到model了。 26 | 27 | ### 常用注解 28 | 1,@Form注解标识model对象。 29 | 2,@FField描述为model里面哪个属性作为表单项,并声明数据源类型。 30 | 3,@FFieldLength(range = {2, 10})//字符长度限制 31 | 4,@FFieldOptions(EnumOptions.class)//Spinner数据源 32 | 5,@FFieldInput(type = FFieldInput.TYPE_CLASS_NUMBER)//输入类型 33 | 6,@FFieldNumberRange(range = {0.0f, 100000.0f})//数据值范围 34 | 35 | 36 | ### 代码用例 37 | 38 | ```java 39 | @Form 40 | public class MainActivity extends AppCompatActivity implements OnClickListener { 41 | @FField(id = 0, titleId = R.string.form_name) 42 | @FFieldLength(range = {2, 10}) 43 | private String name; 44 | 45 | @FField(id = 1, titleId = R.string.form_gender, type = FieldType.SPINNER) 46 | @FFieldOptions(EnumOptions.class) 47 | private Gender gender; 48 | 49 | @FField(id = 2, titleId = R.string.form_job, notNull = false) 50 | @FFieldLength(range = {2, 20}) 51 | private String job; 52 | 53 | @FField(id = 3, titleId = R.string.form_salary) 54 | @FFieldInput(type = FFieldInput.TYPE_CLASS_NUMBER) 55 | @FFieldNumberRange(range = {0.0f, 100000.0f}) 56 | private float salary; 57 | 58 | public enum Gender { 59 | male, female; 60 | } 61 | 62 | private FormLayout mFormLayout; 63 | 64 | @Override 65 | protected void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | setContentView(R.layout.activity_main); 68 | findViewById(R.id.btn_submit).setOnClickListener(this); 69 | mFormLayout = (FormLayout) findViewById(R.id.formlayout); 70 | mFormLayout.parseAndBindField(this, new ParseFieldFilter() { 71 | @Override 72 | public boolean isSkip(int id) { 73 | return false; 74 | } 75 | 76 | @Override 77 | public void onFieldCreated(int id, FormField f) { 78 | } 79 | }); 80 | } 81 | 82 | private void submit() { 83 | if (mFormLayout.validateForm()) { 84 | StringBuilder sb = new StringBuilder(); 85 | sb.append("FormlayoutResult:\n\n"); 86 | sb.append("Name : ").append(name).append('\n'); 87 | sb.append("Gender : ").append(gender).append('\n'); 88 | sb.append("Job : ").append(job).append('\n'); 89 | sb.append("Salary : ").append(salary).append('\n'); 90 | new AlertDialog.Builder(this) 91 | .setMessage(sb.toString()) 92 | .setPositiveButton(android.R.string.ok, null) 93 | .show(); 94 | } 95 | } 96 | 97 | @Override 98 | public void onClick(View v) { 99 | switch (v.getId()) { 100 | case R.id.btn_submit: 101 | submit(); 102 | break; 103 | } 104 | } 105 | ``` 106 | ### 效果图 107 | ![image](https://github.com/shenkaige/FormLayout/blob/master/screenshot/validate.png) 108 | ![image](https://github.com/shenkaige/FormLayout/blob/master/screenshot/result.png) 109 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "xyz.codedream.lib.form.sample" 8 | minSdkVersion 12 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.3.1' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | compile project(':library') 31 | } 32 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\Android_Studio_sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/xyz/codedream/lib/form/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package xyz.codedream.lib.form.sample; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("xyz.codedream.lib.form.sample", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/codedream/lib/form/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package xyz.codedream.lib.form.sample; 2 | 3 | import android.app.AlertDialog; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.view.View.OnClickListener; 8 | 9 | import xyz.codedream.lib.form.FieldOptions.EnumOptions; 10 | import xyz.codedream.lib.form.FormField; 11 | import xyz.codedream.lib.form.FormLayout; 12 | import xyz.codedream.lib.form.FormLayout.ParseFieldFilter; 13 | import xyz.codedream.lib.form.annotion.FField; 14 | import xyz.codedream.lib.form.annotion.FField.FieldType; 15 | import xyz.codedream.lib.form.annotion.FFieldInput; 16 | import xyz.codedream.lib.form.annotion.FFieldLength; 17 | import xyz.codedream.lib.form.annotion.FFieldNumberRange; 18 | import xyz.codedream.lib.form.annotion.FFieldOptions; 19 | import xyz.codedream.lib.form.annotion.Form; 20 | 21 | @Form 22 | public class MainActivity extends AppCompatActivity implements OnClickListener { 23 | @FField(id = 0, titleId = R.string.form_name) 24 | @FFieldLength(range = {2, 10}) 25 | private String name; 26 | 27 | @FField(id = 1, titleId = R.string.form_gender, type = FieldType.SPINNER) 28 | @FFieldOptions(EnumOptions.class) 29 | private Gender gender; 30 | 31 | @FField(id = 2, titleId = R.string.form_job, notNull = false) 32 | @FFieldLength(range = {2, 20}) 33 | private String job; 34 | 35 | @FField(id = 3, titleId = R.string.form_salary) 36 | @FFieldInput(type = FFieldInput.TYPE_CLASS_NUMBER) 37 | @FFieldNumberRange(range = {0.0f, 100000.0f}) 38 | private float salary; 39 | 40 | public enum Gender { 41 | male, female; 42 | } 43 | 44 | private FormLayout mFormLayout; 45 | 46 | @Override 47 | protected void onCreate(Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | setContentView(R.layout.activity_main); 50 | findViewById(R.id.btn_submit).setOnClickListener(this); 51 | mFormLayout = (FormLayout) findViewById(R.id.formlayout); 52 | mFormLayout.parseAndBindField(this, new ParseFieldFilter() { 53 | @Override 54 | public boolean isSkip(int id) { 55 | return false; 56 | } 57 | 58 | @Override 59 | public void onFieldCreated(int id, FormField f) { 60 | } 61 | }); 62 | } 63 | 64 | private void submit() { 65 | if (mFormLayout.validateForm()) { 66 | StringBuilder sb = new StringBuilder(); 67 | sb.append("FormlayoutResult:\n\n"); 68 | sb.append("Name : ").append(name).append('\n'); 69 | sb.append("Gender : ").append(gender).append('\n'); 70 | sb.append("Job : ").append(job).append('\n'); 71 | sb.append("Salary : ").append(salary).append('\n'); 72 | new AlertDialog.Builder(this) 73 | .setMessage(sb.toString()) 74 | .setPositiveButton(android.R.string.ok, null) 75 | .show(); 76 | } 77 | } 78 | 79 | @Override 80 | public void onClick(View v) { 81 | switch (v.getId()) { 82 | case R.id.btn_submit: 83 | submit(); 84 | break; 85 | } 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 15 | 16 |