多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

仿微信登录注册界面:LinearLayout布局与交互避坑实战

仿微信登录注册界面:LinearLayout布局与交互避坑实战 简介一套基于安卓开发环境的仿微信登录、注册界面线性布局工程包面向初学安卓布局与Java界面开发的读者演示如何用线性布局组织登录页和注册页的输入框、按钮、链接跳转并补充验证码、重复密码、登录状态持久化等典型交互。压缩包内共1289个文件核心包括Java源码、XML布局、dex与class编译产物、Gradle构建脚本、APK安装包以及JSON等配置文件整体约21.28MB结构完整便于导入项目后直接运行和对照修改。目前已有2732人学习浏览适合课程设计、自学仿写或快速搭建同类型界面的参考。资源内从登录注册界面设计、线性布局属性配置到Java事件监听的实现层次分明可结合源码查看布局权重分配、圆角输入框、状态选择器背景、错误提示和页面切换等细节帮助理解微信风格界面的完整实现思路。1. 仿微信登录注册界面的资源能落地到什么程度刚学 Android 那阵子我一度以为仿个微信登录页就是两个输入框加一个按钮的事结果真正动手时才发现输入框被软键盘顶得看不到焦点注册页返回时整个应用跟着退出手机转一下屏刚输的密码全没了。这套基于 Android Studio Java 写的仿微信登录、注册界面资源核心就一个字把线性布局LinearLayout用透并把最常见的四个问题一次性演示到位——登录布局、注册布局、页面跳转、登录态保存。适合刚补完 Java 基础、正拿课程设计练手的同学也适合想快速搭出一套表单模板的一线开发。界面没有引入复杂架构没用数据库改用 SharedPreferences 保存一个模拟登录标记方便把注意力放在布局和基础交互上。2. 线性布局选型与仿微信界面拆解orientation、weight 与嵌套边界的取舍把仿微信登录界面拆开看真正决定成败的不是控件多不多而是布局容器的选择和嵌套深度控制。这套资源里的两张 XML 布局都是线性布局登录页一个垂直容器注册页在垂直容器里套了两个水平容器整体结构非常透明。下面把三个关键设计逐一拆开。2.1 为什么仿微信登录页的主布局是 LinearLayout 而不是 ConstraintLayout现在很多教程上来就推 ConstraintLayout但登录注册表单是典型的“从上到下堆叠”线性布局是自然选择。LinearLayout 是 Android 中最基础的布局之一按垂直vertical或水平horizontal方向线性排列子视图依次排开代码阅读顺序和界面显示顺序完全一致新手排错时一眼能看出来问题在哪。登录页的 XML 骨架大致是这样LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:paddingStart24dp android:paddingEnd24dp android:paddingTop80dp ImageView android:layout_width72dp android:layout_height72dp android:layout_gravitycenter_horizontal android:srcmipmap/ic_launcher android:contentDescriptionstring/app_logo / EditText android:idid/etUsername android:layout_widthmatch_parent android:layout_heightwrap_content android:hint微信号 / 手机号 android:padding16dp android:backgrounddrawable/bg_input_normal / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop16dp EditText android:idid/etPassword android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint密码 android:inputTypetextPassword android:padding16dp android:backgrounddrawable/bg_input_normal / TextView android:idid/tvTogglePwd android:layout_widthwrap_content android:layout_heightwrap_content android:padding16dp android:text显示 / /LinearLayout Button android:idid/btnLogin android:layout_widthmatch_parent android:layout_height48dp android:layout_marginTop32dp android:text登录 android:textColor#FFFFFF android:backgrounddrawable/bg_btn_login / TextView android:idid/tvToRegister android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop16dp android:text注册新账号 android:gravitycenter android:textColor#07C160 / /LinearLayout这段代码的逻辑是外层垂直 LinearLayout 把图标、账号输入框、密码行、登录按钮、注册入口依次排下来。密码框没有直接放在外层而是先用一个水平 LinearLayout 包了一层密码 EditText 设置layout_width0dp和layout_weight1让密码框自动占满“显示”文案以外的剩余空间。这就是layout_weight的核心用法按权重分配剩余区域。wrap_content的按钮不会被压缩因为是按内容量取宽。相比之下ConstraintLayout 虽然能拖拽式完成同样界面但新手容易拖出几十条约束关系看着毫无头绪而线性布局嵌套两层就能表达清楚。代价是性能嵌套深就慢这个在第 4 章会展开。2.2 注册界面多出来的字段验证码行与协议行的布局写法注册页字段明显变多手机号、验证码、密码、确认密码、协议勾选、注册按钮。这套资源里没有把所有控件一股脑塞进垂直布局而是用水平容器分组。验证码行的写法很有代表性LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal EditText android:idid/etVerifyCode android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint验证码 android:inputTypenumber android:padding16dp android:backgrounddrawable/bg_input_normal / Button android:idid/btnGetCode android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginStart12dp android:text获取验证码 android:textSize14sp / /LinearLayout这个水平容器里“获取验证码”按钮用wrap_content验证码输入框用0dp weight1效果就是按钮永远保持自己该有的宽度输入框吃掉剩余所有宽度。如果想三个控件等分可以把三个都设成0dp且weight1此时剩余空间会被三等分。协议文案也是注册页容易忽略的地方。微信风格里“注册即代表同意协议”是一整行灰色小字我一般放一个 TextView不单独加 CheckBox因为后续用代码控制点击范围更灵活TextView android:idid/tvAgreement android:layout_widthmatch_parent android:layout_heightwrap_content android:text我已阅读并同意《用户协议》和《隐私政策》 android:textSize12sp android:gravitycenter android:textColor#999999 /这里有一个细节注册页顶部和一二级控件之间有较大layout_marginTop目的不是为了好看而是避开系统状态栏和键盘弹起后的视觉压迫。如果直接把控件顶到屏幕最上方键盘弹出来时输入框就会被压缩到难以点击。2.3 圆角输入框与微信绿按钮drawable 与 styles 的一次定义、两处复用微信界面给人“干净”的第一印象来自三样东西浅灰背景输入框、绿色主按钮、字体色系统统一。这套资源把这些都抽成了可复用 drawable 和主题色不是每个页面单独写颜色值。输入框圆角背景shape xmlns:androidhttp://schemas.android.com/apk/res/android solid android:color#F5F5F5 / corners android:radius8dp / stroke android:width1dp android:color#E5E5E5 / /shape登录按钮背景shape xmlns:androidhttp://schemas.android.com/apk/res/android solid android:color#07C160 / corners android:radius6dp / /shape主题样式resources style nameTheme.LoginDemo parentTheme.AppCompat.Light.NoActionBar item namecolorPrimary#07C160/item item namecolorPrimaryDark#058C4A/item item namecolorAccent#07C160/item /style /resources两个背景文件放在res/drawable下样式放在res/values/styles.xml下然后在AndroidManifest.xml中给登录和注册两个 Activity 设置android:themestyle/Theme.LoginDemo。这样两页用的绿色完全一致改主题色时只动一处。按钮、输入框的圆角值建议单独抽到dimens.xml避免两处半径差 1dp 导致肉眼可见的不协调。这里有个反直觉点colorAccent不只会影响按钮和输入框选中态颜色还会影响光标颜色和部分控件的默认背景着色。也就是说如果你在 EditText 里发现“背景怎么不是我要的灰色”多半是主题色在作怪。这个问题后面有一节专门讲。3. 把 XML 变成可交互的登录注册流程监听器、状态切换与登录态持久化布局文件只是皮真正的登录注册逻辑在 Java 代码里。这套资源里最值得抄作业的部分是绑定监听、页面切换回传数据和登录态持久化三块的配合方式。3.1 findViewById 绑定与点击监听从按钮到输入框的完整链路登录页 Java 代码的核心结构如下public class LoginActivity extends AppCompatActivity { private static final int REQUEST_CODE_REGISTER 1001; private EditText etUsername, etPassword; private Button btnLogin; private TextView tvToRegister; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); etUsername findViewById(R.id.etUsername); etPassword findViewById(R.id.etPassword); btnLogin findViewById(R.id.btnLogin); tvToRegister findViewById(R.id.tvToRegister); btnLogin.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (TextUtils.isEmpty(username)) { etUsername.setError(请输入微信号/手机号); return; } if (TextUtils.isEmpty(password)) { etPassword.setError(请输入密码); return; } // 这里只是本地模拟替换成你的网络请求即可 saveLoginState(username); startActivity(new Intent(LoginActivity.this, MainActivity.class)); finish(); } }); tvToRegister.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { Intent intent new Intent(LoginActivity.this, RegisterActivity.class); startActivityForResult(intent, REQUEST_CODE_REGISTER); } }); } }这段代码的逻辑链路很直白findViewById把 XML 里定义好的控件引用拿过来然后给登录按钮注册点击监听器。点击时先从两个 EditText 取出文本trim()去掉首尾空格再用TextUtils.isEmpty判空。判空失败时setError会在输入框下方显示一行红色提示文字并且输入框右侧会出现一个感叹号图标这是 Android TextView 自带的交互反馈不需要额外引入控件。这里有一个容易被忽略的参数inputType。XML 里密码框设置的是textPassword所以 Java 代码不用关心是否转成密文点阵系统会处理。如果写成text密码就会明文显示等于是自己给自己埋雷。点击“登录”后先调saveLoginState(username)再跳转MainActivity并finish()登录页。finish()的作用是让登录页从返回栈中移除这样用户在主界面按返回键不会退回到登录页。这种“登录后销毁登录页”的习惯能避免很多返回栈混乱问题。3.2 Intent 在登录页与注册页之间切换返回栈与数据回传登录页点击“注册新账号”时代码没有用普通的startActivity后finish()而是startActivityForResult。原因是产品上的一个常见诉求用户在注册页输完手机号并注册成功后回到登录页时应该自动把手机号回填到账号输入框再顺手把光标放到密码框。用老接口startActivityForResult在 Java 工程里最直观。注册页的提交按钮处理如下// RegisterActivity 内部 String phone etPhone.getText().toString().trim(); String password etPassword.getText().toString().trim(); String confirmPassword etConfirmPassword.getText().toString().trim(); if (TextUtils.isEmpty(phone)) { etPhone.setError(请输入手机号); return; } if (password.length() 6) { etPassword.setError(密码至少 6 位); return; } if (!password.equals(confirmPassword)) { etConfirmPassword.setError(两次密码不一致); return; } Intent result new Intent(); result.putExtra(registered_phone, phone); setResult(RESULT_OK, result); finish();登录页接收返回数据Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode REQUEST_CODE_REGISTER resultCode RESULT_OK data ! null) { String phone data.getStringExtra(registered_phone); if (!TextUtils.isEmpty(phone)) { etUsername.setText(phone); etPassword.requestFocus(); } } }setResultfinish是 Android 老一套的数据回传方式。setResult只负责把结果暂存真正返回动作由finish触发系统再把结果交给前面等待的onActivityResult。这里的关键点是登录页跳转注册页时千万不能finish()登录页否则注册页 finish 后返回栈里已经没有登录页用户会直接回到桌面或者退出应用。如果你用的是新版 Android Studio 模板startActivityForResult可能被标为废弃官方推荐 Activity Result API。但资源包做得比较保守老接口依赖最低版本更小对于课程设计和初学理解仍然够用我在自己的老项目里也一直保留这种写法。3.3 SharedPreferences 持久化登录态自动登录与退出登录登录成功的状态需要跨 Activity 甚至跨启动存在。这套资源用 SharedPreferences 存一个布尔标记够用且直观。public void saveLoginState(String username) { SharedPreferences sp getSharedPreferences(login_pref, MODE_PRIVATE); sp.edit() .putBoolean(is_login, true) .putString(username, username) .apply(); }主界面启动时检查SharedPreferences sp getSharedPreferences(login_pref, MODE_PRIVATE); boolean isLogin sp.getBoolean(is_login, false); if (!isLogin) { startActivity(new Intent(this, LoginActivity.class)); finish(); return; }退出登录public void logout() { SharedPreferences sp getSharedPreferences(login_pref, MODE_PRIVATE); sp.edit().clear().apply(); startActivity(new Intent(this, LoginActivity.class)); finish(); }三个参数值得单独说明MODE_PRIVATE表示这个 SharedPreferences 文件只允许当前应用读写apply()是异步落盘不会卡 UI 线程commit()是同步落盘返回值表示是否成功日常 UI 线程里别用commit()clear().apply()会清空整个文件里该应用的所有键值适合退出登录这种“一刀切”场景。提示SharedPreferences 只适合存“是否登录”“用户名展示”这类非敏感数据。密码和真实 token 不要这样明文存要用加密存储或系统级 Keystore。资源里存明文是为了演示流程生产环境必须替换。另外退出登录后跳转登录页需要finish()主界面再把登录页作为栈顶。如果主界面还有别的上级页面建议补一个Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK把这些历史页面全部清掉这也是我在实际项目里常用的“后悔药”。4. 避坑指南线性布局仿微信界面最常见的五个翻车现场这套资源我前前后后帮三个同学调试过翻车点高度集中几乎都跟软键盘、主题色覆盖、返回栈和状态丢失有关。下面按现象到原因到解决的思路整理成五条可复用的排查记录。4.1 输入框被软键盘遮挡windowSoftInputMode 配置的一个隐藏前提现象在AndroidManifest.xml里给登录页配了android:windowSoftInputModestateHidden|adjustResize运行时点击底部输入框输入框仍然被弹出的软键盘盖住登录按钮完全看不到。原因adjustResize并不总是生效。如果 Activity 的主题是 Fullscreen 或 Immersive 类型系统窗口会因为全屏逻辑吞掉这次 resize另外如果软键盘模式在 styles 和 Manifest 两处都配了Manifest 会覆盖 style但覆盖后的值可能与预期不符。更隐蔽的坑是根布局外层如果不加 ScrollView内容区没有可滚动空间resize 后系统只能压缩布局压缩到一定程度输入框照样被顶出可视区。解决统一在 Manifest 的 Activity 节点配一次软键盘模式去掉 styles 里的重复配置布局根部包一层 ScrollView并给 ScrollView 设置android:fillViewporttrue。activity android:name.LoginActivity android:windowSoftInputModestateHidden|adjustResize /做完这两步键盘弹起时整个页面会被顶上去而不是遮住输入框。判断是否修复成功的方法是点击验证码输入框看“获取验证码”按钮是否完整可见。如果按钮仍然被挡多半是沉浸式状态栏和fitsSystemWindows属性冲突需要给根布局加android:fitsSystemWindowstrue但注意这个属性和 adjustResize 要配合测试容易顾此失彼。4.2 EditText 的背景被主题覆盖backgroundTint 的颜色叠加现象在 XML 里给 EditText 设置了android:backgrounddrawable/bg_input_normal运行后并不是想象中的浅灰圆角框而是带了一条默认的下划线或者紫蓝色调。原因Android 5.0 之后EditText 默认会使用主题里的colorAccent和colorControlNormal绘制背景着色层backgroundTint体系会叠加在自定义背景之上把 drawable 原本的颜色“染”掉。很多新手只改了android:background不知道还有个隐藏的 tint 在作怪。解决给 EditText 显式关闭背景着色EditText android:idid/etUsername android:layout_widthmatch_parent android:layout_heightwrap_content android:backgrounddrawable/bg_input_normal android:backgroundTintnull android:hint微信号 / 手机号 /如果不想每处都写backgroundTint可以在应用的styles.xml里把colorControlNormal置为透明item namecolorControlNormalandroid:color/transparent/item但这会影响整个主题下所有 EditText、CheckBox 等控件的默认着色改动面较大我一般只在单个页面用第一种方案减少副作用。4.3 注册成功返回登录页却“退出了应用”Activity 堆栈混乱现象在注册页点击“注册并登录”注册页关闭后没有回到登录页而是直接回到桌面或者退出登录页再进来按返回键时把整个应用关掉了。原因大多是跳转时把不该销毁的页面销毁了。常见错误是点击“注册”时写了startActivity(registerIntent); finish();把登录页也移出了返回栈。注册页再finish()时返回栈里已经没有登录页系统只能去找更底层的其它 Activity找不到就退回桌面。另一种情况是把两个页面都设置为singleTask或singleTop互相覆盖导致状态混乱。解决登录页跳注册页时不要finish()等注册页通过setResult返回后继续处理。如果确实需要清掉登录页上面的残留页面用标志位Intent loginIntent new Intent(this, LoginActivity.class); loginIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(loginIntent); finish();FLAG_ACTIVITY_CLEAR_TOP会清除目标 Activity 之上的所有页面SINGLE_TOP则让栈顶已存在的登录页直接复用而不是新建实例。这两招合起来能解决绝大多数“返回键退出应用”的玄学问题。4.4 横竖屏一翻转输入内容全没了Activity 重建与状态保存现象在注册页输到一半手机从竖屏转横屏界面是重新显示了但刚输入的手机号、验证码全部清空用户只能从头填。原因默认情况下配置变更orientation、screenSize会触发 Activity 销毁重建。你不主动保存输入框内容系统不会替你保留。解决有两种路线。路线一是在 Manifest 里给该 Activity 配置android:configChangesorientation|screenSize这样转屏时 Activity 不重建而是回调onConfigurationChanged输入内容自然还在。这种做法省事但不够通用。路线二更推荐保存和恢复输入状态。Override protected void onSaveInstanceState(Bundle outState) { outState.putString(username, etUsername.getText().toString()); outState.putString(password, etPassword.getText().toString()); super.onSaveInstanceState(outState); } Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); if (savedInstanceState ! null) { etUsername.setText(savedInstanceState.getString(username)); etPassword.setText(savedInstanceState.getString(password)); } }注意onRestoreInstanceState的调用时机在onStart之后此时控件已经初始化完成所以可以直接setText。密码框恢复明文在演示项目里无所谓但真实项目出于安全考虑最好让用户重新输入只在账号框做恢复。4.5 多重 layout_weight 导致界面加载变慢嵌套层级的隐性开销现象注册页字段多垂直 LinearLayout 里嵌了三个水平 LinearLayout每个里面的输入框都设置了layout_weight低端机上打开页面明显卡顿点击输入框时响应也有延迟。原因layout_weight不是免费的。系统在测量阶段会先按wrap_content或0dp测量一遍各子视图再计算剩余空间按权重重新分配嵌套层数越深测量次数成倍增加。如果每个水平容器里都有两个以上的weight控件整个页面的测量开销会在低端机上暴露出来。解决控制嵌套深度只在真正需要“按比例分配剩余空间”的地方使用weight。像验证码行这种“输入框 按钮”的固定组合只有一个weight是合理的但如果一排有四个等宽控件还要用四个weight去分不如换成GridLayout或者局部引入ConstraintLayout。线性布局适合纵向表单堆叠遇到复杂网格就换工具别硬撑。5. 进阶技巧从“能看”打磨到“能上线”资源里给出的界面能运行但离“像一个正式产品”还差几层细节。这里说三个我自己常用的打磨手法每个改动都不大效果却很明显。5.1 输入框聚焦变色selector 状态选择器的用法把输入框背景从静态 drawable 换成状态选择器实现“点击后边框变绿”的微信风格反馈selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_focusedtrue android:drawabledrawable/bg_input_focused / item android:state_enabledfalse android:drawabledrawable/bg_input_disabled / item android:drawabledrawable/bg_input_normal / /selectorbg_input_focused只需改stroke颜色为#07C160。EditText 的background引用这个 selector 后获得焦点时系统自动切换 drawable不需要写setOnFocusChangeListener。5.2 登录按钮的加载态与禁用态防止用户重复点击登录按钮常见做法是点击后立即禁用按钮并显示加载文案btnLogin.setEnabled(false); btnLogin.setText(登录中...); btnLogin.postDelayed(() - { btnLogin.setEnabled(true); btnLogin.setText(登录); }, 2000);按钮的背景也要改成 selector让state_enabledfalse时显示灰色圆角背景否则按钮禁用后颜色不变用户会以为还能点。5.3 我自己的回归检查清单下面这份清单是我后来每次写登录注册都会跑一遍的检查项场景检查点空值校验不输入直接点登录输入框下方是否出现红色提示键盘遮挡点击底部输入框键盘弹起后按钮是否仍然可见二次点击登录中按钮是否禁用能否重复提交注册回填注册成功返回登录页账号是否自动回填转屏恢复输入一半转屏内容是否保存或提示重新输入退出登录退出后按返回键是否直接退到桌面这套资源里的 LinearLayout 布局思路、SharedPreferences 登录态、Activity 回传代码正好覆盖了我工作里最常碰到的几处。从那以后我每次写完登录注册都强制走一遍上面的检查清单再也不敢在没验证软键盘和返回键之前就说“界面写完了”。希望帮到你。本文还有配套的精品资源点击获取
返回列表