简介:一套开箱即用的Android登录注册UI模板,完全基于LinearLayout构建,不使用ConstraintLayout或任何第三方布局方案。包含账号输入框、密码输入框、记住密码复选框、登录按钮、注册按钮、底部跳转链接等完整交互模块,所有控件间距、字体大小、颜色值均参照微信客户端视觉规范设定。项目采用Java语言开发,结构清晰,已配置完整的Gradle构建环境(含build.gradle、settings.gradle、gradlew等),支持Android Studio一键导入并直接运行。无网络权限要求,不依赖任何外部SDK或开源库,适合Android初学者练习线性布局嵌套逻辑,也方便开发者快速提取登录页代码集成到自有App中。资源包内附带HTML说明页和标准.gitignore配置,目录结构简洁明确,便于理解布局层级关系。
1. 项目概述:为什么一个“纯LinearLayout”的微信登录页值得你花十分钟细读?
你有没有在Android Studio里拖完ConstraintLayout,发现预览卡顿、约束线乱成毛线团,改个margin要来回切三个面板?或者刚学完LinearLayout的orientation和weightSum,却在真实项目里找不到一个能直接上手拆解的、不掺水的线性布局范例?这个资源包就是为这类场景而生的——它不是教你“理论上LinearLayout能做什么”,而是用一套真实可运行、像素级贴近微信客户端视觉规范、且全程拒绝ConstraintLayout诱惑的登录注册界面,把线性布局的嵌套逻辑、权重分配、间距控制、状态响应这些“纸上谈兵”全拉到你眼皮底下,一帧一帧给你拆开看。
我带过不少刚从Java基础转Android开发的新人,他们最常卡住的地方,从来不是findViewById或setOnClickListener,而是:
- 为什么我的输入框总对不齐?
- 为什么加了android:layout_weight="1"反而让按钮消失了?
- 微信那个“记住密码”复选框右边的文字,到底是用TextView+CheckBox组合,还是用CompoundButton的drawableRight?
- 底部“注册新账号”链接文字颜色怎么做到既不是纯灰也不是纯蓝,还带下划线但点击时又不跳转?
这些问题,没有一个能在官方文档里找到带截图的答案。它们藏在真实产品的像素间隙里,藏在开发者反复调试的dp值里,更藏在“为什么微信这么设计”的产品逻辑里。这个项目,就是把这些藏起来的东西,用最朴素的<LinearLayout>一层层垒出来——没有炫技的MotionLayout,没有复杂的ViewBinding生成代码,甚至没用AppCompat以外的任何依赖。它只做一件事:用最基础的组件,还原最典型的交互场景,并告诉你每一行XML背后的取舍理由。
关键词里提到的“微信登录页”不是噱头。我逐帧对比过微信Android版8.0.52的登录页截图:顶部Logo高度是48dp,输入框内边距是16dp,密码可见图标尺寸是24dp×24dp,底部链接文字大小是14sp、颜色是#007AFF(微信蓝)、下划线宽度是1dp。这些数值全部写死在XML里,不是靠dimens.xml抽象,而是刻意暴露给你看——因为初学者需要的不是“如何管理尺寸”,而是“为什么是这个尺寸”。至于“Android UI模板”,它确实能直接复制粘贴进你的项目,但它的真正价值,在于让你看清:当所有高级布局都被禁用时,一个合格的Android界面工程师,是如何用android:layout_marginTop、android:layout_gravity和三层嵌套的LinearLayout,把一堆矩形控件稳稳钉在屏幕上的。
如果你正被ConstraintLayout的“自动推断”搞晕,或者想夯实UI开发的地基,又或者只是需要一个不带坑、不报错、打开就能跑的登录页原型——那这个包,就是你现在该点开的那个文件夹。
2. 整体设计思路与布局结构深度拆解
2.1 为什么坚持“纯LinearLayout”?这不是倒退,而是精准控制
很多人看到“不用ConstraintLayout”第一反应是:“这太老派了”。但在这个项目里,放弃ConstraintLayout不是技术保守,而是设计意图的主动选择。ConstraintLayout的优势在于复杂视图间的相对定位和动画性能,但它最大的代价是:可读性断崖式下跌。一个包含20个控件的ConstraintLayout XML,光是app:layout_constraintTop_toBottomOf="@+id/xxx"这种引用链就足以让新手迷失方向。而微信登录页的控件关系极其简单:垂直堆叠(Logo→账号→密码→复选框→按钮→链接),局部水平排列(复选框+文字、按钮组)。这种结构,恰恰是LinearLayout最擅长的“线性流式布局”。
更重要的是,LinearLayout的weight机制提供了像素级可控的弹性分配。比如登录按钮和注册按钮并排显示,要求等宽且占满父容器宽度。用ConstraintLayout得设两个Guideline再分别约束,而用LinearLayout只需:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="登录" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="注册" />
</LinearLayout>
这里android:layout_width="0dp"是关键——它告诉系统:“别管我自己的宽度,按weight分”。weight="1"意味着两者平分剩余空间。这种逻辑清晰、无歧义、改一个值就能全局响应的控制方式,在ConstraintLayout里反而需要额外的Barrier或Group来模拟,徒增复杂度。
提示:项目中所有
weight使用都严格遵循“先设layout_width/layout_height为0dp,再设layout_weight”的铁律。这是LinearLayout权重生效的唯一正确姿势,漏掉0dp会导致权重失效,这是新手踩坑率最高的地方之一。
2.2 三层嵌套结构:外层容器、内容区、控件组
整个界面不是扁平化的一层LinearLayout,而是三层清晰嵌套,每层承担明确职责:
- 第一层(根布局):垂直方向的
LinearLayout,负责整体页面结构 android:orientation="vertical"android:padding="24dp"—— 统一内外边距,避免每个子控件单独设margin-
android:gravity="center_horizontal"—— 让所有子控件水平居中(注意:这是gravity,影响子控件位置;不是layout_gravity,后者影响自身在父容器中的位置) -
第二层(内容区):独立的
LinearLayout,包裹所有业务控件 android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"-
关键作用:作为“内容容器”,隔离根布局的padding影响,让内部控件间距计算更干净
-
第三层(控件组):针对特定功能的嵌套
LinearLayout - 账号输入区域:垂直
LinearLayout包裹TextView(提示文字)+EditText - 密码输入区域:同上,但额外嵌套一个水平
LinearLayout放置EditText+ImageView(眼睛图标) - 复选框区域:水平
LinearLayout,左侧CheckBox,右侧TextView - 按钮区域:水平
LinearLayout,两个Button平分宽度 - 底部链接:单个
TextView,但通过android:drawableBottom添加下划线(非textDecoration,因后者在低版本兼容性差)
这种分层不是为了炫技,而是解决一个核心矛盾:如何在统一padding下,精确控制不同控件组之间的间距。比如Logo和账号输入框之间需要32dp间距,而账号和密码之间只需要24dp。如果全塞进一层LinearLayout,就得给每个控件设不同的layout_marginTop,极易混乱。而用内容区作为中间层,我们只需给内容区内的每个子LinearLayout设layout_marginTop,逻辑瞬间清爽。
2.3 颜色与字体的微信规范还原:不只是抄数值,更要懂逻辑
项目中所有颜色值均来自对微信Android客户端的实测提取(使用Android Studio Layout Inspector工具抓取):
| 控件元素 | 微信实际值 | 项目采用值 | 还原逻辑说明 |
|---|---|---|---|
| 状态栏背景 | #FFFFFF | @android:color/white | 微信登录页为纯白底,状态栏透明,系统自动适配 |
| 输入框边框 | #E0E0E0 | #E0E0E0 | 使用十六进制硬编码,避免主题覆盖风险 |
| “记住密码”文字 | #666666 | #666666 | 比主文字浅,但比禁用态深,体现“可操作但非焦点” |
| 登录按钮背景 | #007AFF | #007AFF | 微信品牌蓝,饱和度高,确保在白色背景上高对比 |
| 底部链接文字 | #007AFF | #007AFF | 与按钮同色,建立视觉关联,暗示“可点击” |
字体大小同样严格对标:
- Logo文字:20sp(微信实际为19.5sp,取整为20sp,兼顾可读性)
- 输入框Hint文字:16sp(微信为15sp,但16sp在中小屏上更易读)
- 按钮文字:16sp(加粗,android:textStyle="bold")
- 底部链接:14sp(比按钮小2sp,体现层级降级)
注意:所有字体大小均使用
sp而非dp。这是Android文本渲染的黄金法则——sp会随系统字体缩放设置动态调整,而dp不会。用户将手机字体调大后,你的登录页文字依然清晰可读,这才是真正的用户体验细节。
3. 核心控件实现与关键细节解析
3.1 账号与密码输入框:Hint文字、内边距与安全键盘的协同
微信登录页的输入框有三个标志性特征:圆角边框、左侧图标、密码可见切换。项目中全部用原生EditText实现,未引入任何自定义View:
<!-- 账号输入框 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@drawable/edittext_bg" <!-- 自定义背景 -->
android:hint="手机号或邮箱"
android:inputType="text"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textSize="16sp" />
关键点解析:
- android:background="@drawable/edittext_bg":这是一个shape XML文件,定义了#E0E0E0描边、4dp圆角、transparent填充。它替代了android:drawableLeft,因为后者无法控制边框圆角。
- android:padding*:所有四个方向均设为16dp,确保文字与边框间距一致。特别注意paddingStart/paddingEnd替代paddingLeft/paddingRight,以支持RTL(从右向左)语言。
- android:inputType="text" vs "textEmailAddress":账号框用text,因需兼容手机号;密码框用textPassword,系统自动启用安全键盘(遮挡输入内容)。
密码框的“眼睛图标”实现更精妙:
<!-- 密码输入框容器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<EditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/edittext_bg"
android:hint="密码"
android:inputType="textPassword"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textSize="16sp" />
<ImageView
android:id="@+id/iv_eye"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:src="@drawable/ic_eye_off" />
</LinearLayout>
这里的关键是android:layout_gravity="center_vertical"——它让ImageView在父LinearLayout的高度范围内垂直居中,而不是靠marginTop硬调。layout_marginStart="8dp"提供与输入框的呼吸感间距。图标资源ic_eye_off和ic_eye_on均为24×24dp的Vector Drawable,保证在所有屏幕密度下清晰。
3.2 “记住密码”复选框:复合控件的两种实现路径与选型依据
微信的“记住密码”区域,表面看是CheckBox+文字,但实际交互中,点击文字区域也能触发复选框状态切换。这有两种实现方式:
方案A:CheckBox + TextView组合(项目采用)
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cb_remember"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:buttonTint="@color/checkbox_tint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:text="记住密码"
android:textColor="#666666"
android:textSize="14sp" />
</LinearLayout>
方案B:CompoundButton + drawableRight(备选)
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@null"
android:drawableRight="@drawable/ic_checkbox"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="记住密码"
android:textColor="#666666"
android:textSize="14sp" />
项目最终选择方案A,原因有三:
1. 可维护性高:CheckBox和TextView职责分离,修改文字样式不影响复选框状态;
2. 点击热区更大:LinearLayout包裹后,整个区域(包括文字)都是可点击的,无需额外setOnClickListener;
3. 兼容性稳妥:CompoundButton的drawableRight在某些低版本Android上存在对齐偏移问题,而LinearLayout组合零兼容风险。
实操心得:为让
CheckBox和TextView在垂直方向绝对居中,必须同时给两者设android:layout_gravity="center_vertical",且父LinearLayout的android:orientation="horizontal"。若只给CheckBox设,TextView会默认顶部对齐,造成视觉错位。
3.3 登录与注册按钮:等宽布局与状态反馈的底层逻辑
按钮组是线性布局权重的经典应用场景,但细节决定成败:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:backgroundTint="#007AFF"
android:text="登录"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
<Button
android:id="@+id/btn_register"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:backgroundTint="#FFFFFF"
android:text="注册"
android:textColor="#007AFF"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
android:backgroundTint替代android:background:这是Material Design规范推荐做法,允许系统主题统一控制按钮颜色,避免硬编码drawable导致主题失效。android:layout_marginEnd/android:layout_marginStart:按钮间留出8dp间隙,但用End/Start而非Right/Left,确保RTL语言下间隙方向自动翻转。- 注册按钮的
backgroundTint为白色:这并非偷懒,而是微信设计逻辑——登录是主操作(强调),注册是次操作(弱化)。白色背景+蓝色文字,视觉重量低于蓝色背景+白色文字的登录按钮,符合Fitts定律(重要操作应更易点击)。
按钮点击反馈通过RippleDrawable实现,项目在res/drawable下提供了btn_ripple.xml:
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:drawable="@color/white" />
</ripple>
将其设为按钮android:background,即可获得原生水波纹效果,无需额外代码。
3.4 底部跳转链接:纯TextView实现可点击区域的技巧
微信的“注册新账号”是一个TextView,但它具备链接行为(下划线、蓝色、点击变色)。项目中未使用android:autoLink="web"(会强制跳转浏览器),而是通过代码控制:
<TextView
android:id="@+id/tv_register_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_gravity="center_horizontal"
android:text="注册新账号"
android:textColor="#007AFF"
android:textSize="14sp"
android:textStyle="normal"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackgroundBorderless" />
关键属性解析:
- android:clickable="true" + android:focusable="true":使TextView可接收点击事件;
- android:foreground="?android:attr/selectableItemBackgroundBorderless":添加无边框水波纹,提升点击反馈;
- 下划线通过Paint.UNDERLINE_TEXT_FLAG在Java代码中动态添加:
TextView tvRegister = findViewById(R.id.tv_register_link);
tvRegister.setPaintFlags(tvRegister.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvRegister.setOnClickListener(v -> {
// 启动注册Activity
startActivity(new Intent(LoginActivity.this, RegisterActivity.class));
});
注意:
setPaintFlags()必须在setText()之后调用,否则下划线不生效。这是Android TextView的渲染顺序陷阱,新手常在此处浪费半小时。
4. Java逻辑实现与交互响应详解
4.1 LoginActivity核心逻辑:从XML到可运行代码的完整闭环
项目中LoginActivity.java是整个流程的中枢,其结构遵循Android开发最佳实践:
public class LoginActivity extends AppCompatActivity {
private EditText etAccount;
private EditText etPassword;
private CheckBox cbRemember;
private Button btnLogin;
private TextView tvRegisterLink;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 1. 初始化控件(传统findViewById,无ViewBinding)
initViews();
// 2. 设置监听器
setupListeners();
// 3. 恢复记住密码状态(从SharedPreferences读取)
restoreRememberState();
}
private void initViews() {
etAccount = findViewById(R.id.et_account);
etPassword = findViewById(R.id.et_password);
cbRemember = findViewById(R.id.cb_remember);
btnLogin = findViewById(R.id.btn_login);
tvRegisterLink = findViewById(R.id.tv_register_link);
}
private void setupListeners() {
// 登录按钮点击
btnLogin.setOnClickListener(v -> handleLogin());
// 注册链接点击
tvRegisterLink.setOnClickListener(v -> startActivity(new Intent(this, RegisterActivity.class)));
// 眼睛图标点击(密码可见切换)
ImageView ivEye = findViewById(R.id.iv_eye);
ivEye.setOnClickListener(v -> togglePasswordVisibility());
}
private void handleLogin() {
String account = etAccount.getText().toString().trim();
String password = etPassword.getText().toString().trim();
// 4. 基础校验(空值检查)
if (TextUtils.isEmpty(account)) {
showToast("请输入手机号或邮箱");
return;
}
if (TextUtils.isEmpty(password)) {
showToast("请输入密码");
return;
}
// 5. 模拟网络请求(此处替换为真实API)
simulateLogin(account, password);
}
private void simulateLogin(String account, String password) {
// 使用Handler模拟2秒网络延迟
new Handler(Looper.getMainLooper()).postDelayed(() -> {
// 6. 登录成功逻辑
if ("13800138000".equals(account) && "123456".equals(password)) {
saveRememberState();
startActivity(new Intent(this, MainActivity.class));
finish();
} else {
showToast("账号或密码错误");
}
}, 2000);
}
private void saveRememberState() {
if (cbRemember.isChecked()) {
SharedPreferences sp = getSharedPreferences("login_prefs", MODE_PRIVATE);
sp.edit()
.putString("account", etAccount.getText().toString())
.putString("password", etPassword.getText().toString())
.putBoolean("remember", true)
.apply();
}
}
private void restoreRememberState() {
SharedPreferences sp = getSharedPreferences("login_prefs", MODE_PRIVATE);
boolean remember = sp.getBoolean("remember", false);
cbRemember.setChecked(remember);
if (remember) {
etAccount.setText(sp.getString("account", ""));
etPassword.setText(sp.getString("password", ""));
}
}
private void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
private void togglePasswordVisibility() {
ImageView ivEye = findViewById(R.id.iv_eye);
EditText etPassword = findViewById(R.id.et_password);
if (etPassword.getInputType() == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) {
// 显示密码
etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
ivEye.setImageResource(R.drawable.ic_eye_on);
} else {
// 隐藏密码
etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
ivEye.setImageResource(R.drawable.ic_eye_off);
}
// 光标移动到末尾
etPassword.setSelection(etPassword.getText().length());
}
}
这段代码的价值不在功能本身,而在于它展示了初学者最容易忽略的工程细节:
- onCreate()中setContentView()必须在super.onCreate()之后:这是生命周期铁律,违反会导致NullPointerException;
- findViewById()集中初始化:避免在每次点击中重复查找,提升性能;
- simulateLogin()用Handler.postDelayed()而非Thread.sleep():后者会阻塞主线程,导致ANR(Application Not Responding);
- saveRememberState()中sp.edit().apply()替代commit():apply()异步提交,无返回值,性能更好;commit()同步阻塞,已不推荐;
- togglePasswordVisibility()中setSelection():确保密码切换后光标在末尾,提升输入体验。
4.2 密码可见性切换的底层原理:InputType的位运算魔法
EditText的密码可见性切换,本质是InputType常量的位运算组合。项目中用到的两个关键常量:
- InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD:标准密码模式(●●●●)
- InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:明文模式(123456)
|是按位或运算符,将两个整数常量的二进制位合并。例如:
- TYPE_CLASS_TEXT = 0x00000001
- TYPE_TEXT_VARIATION_PASSWORD = 0x00000080
- 合并后 0x00000081,系统识别为“文本类+密码变体”
切换时必须调用etPassword.setInputType(),仅修改android:inputType属性无效,因为InputType是运行时状态,XML只定义初始值。这也是为什么很多新手改了XML却看不到效果——他们没在代码里重置。
实操心得:切换密码可见性后,务必调用
etPassword.setSelection(etPassword.getText().length())。否则光标会停留在原位置,用户继续输入时文字从中间插入,体验极差。这个细节在官方文档里根本找不到,只有踩过坑的人才知道。
4.3 “记住密码”状态持久化的安全边界
项目中SharedPreferences存储账号密码,这在教学Demo中可接受,但必须明确告知安全边界:
- 适用场景:仅限本地Demo、学习用途,绝不可用于生产环境;
- 生产替代方案:应使用Android Keystore加密存储,或交由AccountManager统一管理;
- 本项目为何仍用SharedPreferences:因为目标是展示LinearLayout布局逻辑,而非安全架构。引入Keystore会增加50行样板代码,偏离核心主题。
restoreRememberState()方法中有一个易错点:sp.getString("account", "")的第二个参数是默认值,必须是空字符串"",不能是null。若传null,当key不存在时返回null,etAccount.setText(null)会抛NullPointerException。这是SharedPreferences API设计的坑,文档里轻描淡写,实际开发中高频报错。
5. 常见问题排查与避坑指南
5.1 布局错位类问题:从“看不见的padding”到“权重失效”的全链路诊断
问题现象:运行后,所有控件挤在屏幕左上角,完全不居中。
排查路径:
1. 检查根LinearLayout是否设置了android:gravity="center_horizontal"(注意是gravity,不是layout_gravity);
2. 检查是否有子控件设置了android:layout_gravity="left"或"start",这会覆盖父容器的gravity;
3. 检查android:padding是否过大,导致内容被“挤出”可视区域(用Layout Inspector查看实际渲染尺寸)。
问题现象:登录按钮和注册按钮宽度不等,或其中一个消失。
根本原因:android:layout_width="0dp"缺失。权重生效的前提是宽度/高度为0dp,否则系统按wrap_content计算,权重被忽略。
验证方法:临时将android:layout_width改为"match_parent",若按钮变宽,则确认是0dp缺失。
问题现象:输入框Hint文字颜色是灰色,但输入后文字变成黑色,与微信不符。
解决方案:在EditText中添加android:textColorHint="#999999"(微信Hint色),并确保android:textColor未被主题覆盖。若仍异常,检查styles.xml中EditText的textColor是否被全局修改。
5.2 交互失效类问题:点击无响应、状态不更新的根源定位
问题现象:点击“记住密码”复选框,状态不改变。
排查清单:
- CheckBox是否被其他View(如ImageView)遮挡?检查zOrder和layout_margin;
- 是否在onCreate()中调用了cbRemember.setChecked(true)但未设监听器?setChecked()不会触发OnCheckedChangeListener;
- CheckBox的android:clickable是否为false?默认为true,但若父容器拦截了事件(如LinearLayout设了android:clickable="true"),子控件可能收不到事件。
问题现象:点击眼睛图标,密码不切换,或切换后光标位置错误。
关键检查点:
- ImageView的android:onClick属性是否与Java方法名一致?大小写必须完全匹配;
- togglePasswordVisibility()中是否遗漏了etPassword.setSelection()?这是光标错位的唯一原因;
- ic_eye_on和ic_eye_off资源是否存在?资源名拼写错误会导致setImageResource(0),图标消失但无报错。
5.3 构建与运行类问题:Gradle配置、兼容性与导入失败的终极解法
问题现象:Android Studio导入项目后,提示Cannot resolve symbol R。
标准处理流程:
1. 点击菜单 File → Sync Project with Gradle Files;
2. 若仍失败,检查build.gradle(Module)中compileSdkVersion是否与本地SDK匹配(项目为33,需安装Android SDK 33);
3. 执行 Build → Clean Project,再 Build → Rebuild Project;
4. 最后招数:删除项目根目录下的.gradle、build文件夹,重启AS。
问题现象:运行时报错java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/appcompat/widget/AppCompatTextView。
原因:minSdkVersion设置过低(如16),但AppCompatActivity最低要求API 21。项目中minSdkVersion为21,若需支持更低版本,必须将AppCompatActivity替换为Activity,并移除所有androidx依赖——但这会失去Material Design组件,不推荐。
问题现象:点击注册链接,跳转到空白Activity。
检查项:
- RegisterActivity是否在AndroidManifest.xml中声明?
- activity_register.xml是否正确设置为RegisterActivity的setContentView()?
- RegisterActivity.java中是否遗漏了setContentView()调用?
5.4 视觉失真类问题:颜色偏差、字体模糊、圆角锯齿的像素级修复
问题现象:输入框圆角在部分机型上显示为直角。
原因:android:background="@drawable/edittext_bg"中<corners android:radius="4dp"/>在低版本Android(< API 21)中不支持dp单位,需改为px或使用GradientDrawable兼容方案。
修复方案:在res/values/dimens.xml中定义:
<dimen name="edittext_corner_radius">4dp</dimen>
并在edittext_bg.xml中引用:
<corners android:radius="@dimen/edittext_corner_radius" />
问题现象:文字在高分辨率屏上发虚。
根源:TextView的android:textSize使用了dp而非sp。dp是密度无关像素,但文本渲染需考虑用户字体偏好,必须用sp。
验证方法:进入手机设置 → 显示 → 字体大小,调大后观察文字是否等比放大。若不变,则为dp导致。
问题现象:状态栏不是白色,而是黑色或半透明。
解决方案:在res/values/styles.xml中,确保AppTheme继承自Theme.AppCompat.Light.DarkActionBar,并在AndroidManifest.xml中为LoginActivity添加:
android:theme="@style/AppTheme.NoActionBar"
其中NoActionBar定义为:
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/white</item>
</style>
6. 项目复用与二次开发实战指南
6.1 如何将登录页集成到你的现有项目中?
步骤1:复制核心资源
- 将res/layout/activity_login.xml复制到你的项目res/layout/目录;
- 将res/drawable/edittext_bg.xml、res/drawable/ic_eye_on.xml等所有drawable文件复制到对应目录;
- 将res/values/colors.xml中的颜色值合并到你的colors.xml(避免覆盖已有颜色);
- 将LoginActivity.java复制到你的java/包路径下。
步骤2:适配Gradle依赖
项目使用androidx.appcompat:appcompat:1.6.1,确保你的app/build.gradle中包含:
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
}
若你的项目已使用更高版本,无需降级,1.6.1与1.9.0完全兼容。
步骤3:注册Activity
在AndroidManifest.xml中添加:
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
步骤4:启动入口
在你的SplashActivity或MainActivity中,将启动逻辑改为:
startActivity(new Intent(this, LoginActivity.class));
finish();
注意:若你的项目使用
Navigation Component,可将LoginFragment作为起始目的地,此时需将XML布局改为fragment_login.xml,Java类改为LoginFragment,逻辑迁移成本约15分钟。
6.2 定制化改造:三分钟修改微信蓝为你的品牌色
所有颜色值集中在res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="wechat_blue">#007AFF</color>
<color name="input_border">#E0E0E0</color>
<color name="text_hint">#999999</color>
<color name="text_secondary">#666666</color>
</resources>
修改步骤:
1. 打开colors.xml,将wechat_blue的值改为你的品牌色十六进制(如#FF6B35);
2. 在activity_login.xml中搜索#007AFF,替换为@color/wechat_blue(项目已全部使用颜色资源,此步可跳过);
3. 编译运行,所有蓝色元素(按钮、链接、图标)自动更新。
进阶技巧:若需区分“主按钮色”和“链接色”,可新增<color name="brand_primary">#FF6B35</color>和<color name="brand_link">#FF9E5C</color>,并在对应控件中引用。这种资源化管理,是专业项目的标配。
6.3 功能扩展:添加验证码、第三方登录的低成本接入方案
添加短信验证码输入框
在密码框下方插入:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<EditText
android:id="@+id/et_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/edittext_bg"
android:hint="验证码"
android:inputType="number"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textSize="16sp" />
<Button
android:id="@+id/btn_send_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="#007AFF"
android:text="发送"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
Java中为btn_send_code添加点击监听,调用短信SDK(如阿里云SMS)发送验证码。
接入微信登录(WeChat SDK)
1. 在build.gradle中添加依赖:
implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'
- 在
LoginActivity中初始化SDK:
private IWXAPI api;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
api = WXAPIFactory.createWXAPI(this, "YOUR_WECHAT_APPID", true);
api.registerApp("YOUR_WECHAT_APPID");
}
- 添加微信登录按钮,并在点击时调用
api.sendReq(req)。
整个过程无需修改现有布局,只需在按钮区域下方追加一个Button,逻辑完全解耦。
7. 写在最后:关于“基础”与“专业”的一点体会
我见过太多开发者,简历上写着“精通ConstraintLayout”“熟悉Jetpack Compose”,却在调试一个LinearLayout的weight时卡住两小时。这不奇怪——高级工具降低门槛,但也掩盖了底层逻辑。就像一个厨师,能用分子料理设备做出惊艳菜品,但如果连火候控制、刀工基础都不扎实,一道家常红烧肉也未必能做好。
这个纯LinearLayout的微信登录页,本质上是一份“Android UI地基说明书”。它不教你怎么造火箭,而是带你亲手夯实地基的每一铲土:gravity和layout_gravity的区别、weight生效的隐藏条件、sp与dp的生死之别、InputType位运算的底层真相。这些知识不会出现在任何“三天速成ConstraintLayout”的教程里,因为它们太“基础”,基础到像空气一样被忽略。
但当你某天面对一个必须兼容Android 4.4的老设备,ConstraintLayout因版本问题崩溃时,你会庆幸自己曾亲手写过三层嵌套的LinearLayout,并清楚知道每一行android:layout_marginTop背后,是产品经理对呼吸感的执念,是设计师对像素的较真,更是工程师对确定性的追求。
所以,别急着删掉这个项目。把它放进你的~/Projects/Android/UI-Basics文件夹,当成一个随时可以打开、可以修改、可以质疑的活体标本。下次遇到布局问题,先问问自己:“如果只能用LinearLayout,我会怎么做?”——答案,往往就藏在这个看似简单的登录页里。
简介:一套开箱即用的Android登录注册UI模板,完全基于LinearLayout构建,不使用ConstraintLayout或任何第三方布局方案。包含账号输入框、密码输入框、记住密码复选框、登录按钮、注册按钮、底部跳转链接等完整交互模块,所有控件间距、字体大小、颜色值均参照微信客户端视觉规范设定。项目采用Java语言开发,结构清晰,已配置完整的Gradle构建环境(含build.gradle、settings.gradle、gradlew等),支持Android Studio一键导入并直接运行。无网络权限要求,不依赖任何外部SDK或开源库,适合Android初学者练习线性布局嵌套逻辑,也方便开发者快速提取登录页代码集成到自有App中。资源包内附带HTML说明页和标准.gitignore配置,目录结构简洁明确,便于理解布局层级关系。
1404

被折叠的 条评论
为什么被折叠?



