简介:这份Android应用源码项目以高仿QQ登录界面为核心,面向在校学生、个人开发者及企业技术人员,可用于毕业设计参考、日常学习研究或公司项目技术选型。资源包共70个文件,包含44个png图片资源、22个xml布局与配置文件和4个java核心逻辑代码,整体约752KB,体积轻量便于快速导入Android Studio运行调试。其中xml文件承担界面布局与资源定义,java文件实现登录交互与业务逻辑,png则提供图标与背景素材,结构清晰、层次分明。目前已有119人学习下载,适合作为Android入门到进阶的实战案例。通过研读该源码,读者可掌握登录页面的布局技巧、控件使用、事件处理与资源组织方式,理解仿主流应用UI的实现思路,并在此基础上进行二次开发或功能扩展,为课程设计、项目答辩或技术积累提供可直接复用的参考方案。
1. 高仿 QQ 登录页:一个 Android 源码项目能教会你的真实工程能力
很多人第一次拿到「Android 应用源码之高仿 QQ 登陆」这类压缩包时,第一反应是解压、用 Android Studio 打开、点运行,然后盯着模拟器里那个蓝色登录界面发呆——看起来挺像,但接下来呢?这个项目真正的价值,不在于它「像不像 QQ」,而在于它把 Android 应用开发中最基础也最容易翻车的几块拼图凑齐了:布局嵌套与约束、输入框状态管理、软键盘与焦点交互、按钮防抖、本地校验逻辑、Activity 跳转与生命周期。这些能力,你在任何一本教材里都能看到名词,但只有在一个完整可运行的工程里,你才能看到它们是怎么咬合在一起的。
这篇文章面向的是手里已经拿到这个源码包、或者正准备下载类似安卓应用源码的开发者。我会按「先跑通、再拆解、后改造」的顺序,把高仿 QQ 登录这个场景拆成可复现的步骤。你不需要有很深的 Android 功底,但需要装好 Android Studio,并且愿意打开 Layout Inspector 和 Logcat 去看真实运行状态。如果你只是想找一个能直接上架的登录模块,那这个项目大概率会让你失望——它的定位是学习范本,不是生产级 SDK。但如果你想知道一个登录页从 XML 到 Java/Kotlin 逻辑到底经历了什么,它是一块很好的磨刀石。
2. 把源码跑起来:环境、依赖与第一次编译的四个卡点
2.1 导入前的环境核对清单
拿到一个来源不明的 Android 源码压缩包,最忌讳的就是直接 File → Open 然后等 Gradle 自己折腾。我一般会先做三件事:看目录结构、看 gradle 版本、看 compileSdk 和 targetSdk。高仿 QQ 登录这类项目通常不会太新,很多是几年前的教学工程,Gradle 插件版本和当前 Android Studio 自带版本往往对不上。
先解压,然后在根目录找这几个文件:
# 在解压后的项目根目录执行 ls -la # 重点看这几个文件是否存在 # build.gradle (项目级) # app/build.gradle (模块级) # gradle/wrapper/gradle-wrapper.properties # settings.gradlegradle-wrapper.properties里的distributionUrl决定了这个项目期望的 Gradle 版本。如果它写的是gradle-6.x而你的 Android Studio 是较新版本,直接打开大概率会卡在 “Gradle sync failed”。我的做法是:先按项目原本的版本配,实在配不上再升。升级时改distributionUrl和项目级build.gradle里的 Android Gradle Plugin 版本,两者要匹配。
app/build.gradle里重点看compileSdkVersion、minSdkVersion、targetSdkVersion。高仿 QQ 登录项目常见的是compileSdkVersion 28到30之间。如果你的 Android Studio 没有安装对应 SDK,它会提示你下载,这一步别跳过。
提示:不要一上来就点 “Upgrade Gradle plugin”,先让项目按原样跑通一次,再考虑升级。升级引入的变量太多,第一次跑不通你分不清是环境问题还是代码问题。
2.2 依赖冲突与 AndroidX 迁移
老项目第二个高频翻车点是 support 库和 AndroidX 混用。如果app/build.gradle里出现com.android.support:appcompat-v7,而你的 Android Studio 默认启用 AndroidX,编译时会报 “This project uses AndroidX dependencies, but the ‘android.useAndroidX’ property is not enabled”。
两个选择:要么在gradle.properties里关掉 AndroidX,要么用 Refactor → Migrate to AndroidX 迁移。我一般选迁移,因为新项目迟早要走这条路。迁移前先 commit 一次,迁移后重点检查import android.support.开头的语句是否都变成了import androidx.。
// app/build.gradle 中迁移后常见的依赖形态 dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.material:material:1.9.0' // 高仿登录页常用到的圆形头像库,注意版本要能对上 implementation 'de.hdodenhof:circleimageview:3.1.0' }这里的circleimageview是很多高仿登录页用来做圆形头像的库。如果你的项目里没有这个依赖但代码里引用了CircleImageView,编译会报找不到符号。解决办法是在app/build.gradle里补上,或者把代码里的CircleImageView换成ShapeableImageView(Material 库自带)。
2.3 第一次运行的验证标准
编译通过不等于跑通。第一次运行,我建议用真机而不是模拟器,因为登录页涉及软键盘弹出、输入框焦点切换,模拟器的软键盘行为和真机有差异。安装后重点验证四件事:
第一,应用图标和名称是否正常显示,有没有出现两个图标或者名称是乱码。第二,启动后是否直接进入登录页,有没有闪退。第三,点击输入框,软键盘是否弹出,输入框是否被顶上去或者被遮挡。第四,输入账号密码后点击登录按钮,是否有响应——哪怕只是弹一个 Toast。
如果启动就闪退,先看 Logcat 的AndroidRuntime异常。高仿 QQ 登录项目最常见的闪退原因是:在onCreate里引用了不存在的资源 ID,或者setContentView的布局文件里有自定义 View 没有正确配置构造方法。
// 自定义 View 必须实现这三个构造方法,否则在 XML 中引用会崩溃 public class ClearEditText extends AppCompatEditText { public ClearEditText(Context context) { super(context); } public ClearEditText(Context context, AttributeSet attrs) { super(context, attrs); } public ClearEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }很多高仿登录页会自定义一个带清除按钮的EditText,如果只写了前两个构造方法,在 XML 里引用时不会报错,但运行时可能因为defStyleAttr缺失导致主题属性解析异常。补全三个构造方法是基本操作。
2.4 用 Layout Inspector 看布局层级
跑通之后,别急着改代码。先用 Android Studio 的 Layout Inspector(View → Tool Windows → Layout Inspector)连上真机,看登录页的 View 树。高仿 QQ 登录页通常会有比较深的嵌套:ConstraintLayout套LinearLayout套RelativeLayout,中间还夹着自定义 View。
看 View 树的目的是找两样东西:一是层级深度超过 5 层的分支,二是宽高出现wrap_content但实际被拉伸的节点。这两类节点是后续做性能优化和适配的切入点。比如 QQ 登录页顶部那个头像,很多实现是放在一个RelativeLayout里,用layout_centerHorizontal居中,但外层又套了一个LinearLayout做垂直排列。这种嵌套在 1080p 屏幕上没问题,但在小屏设备上可能导致头像被压缩。
3. 拆解登录页核心模块:从 XML 布局到输入校验的完整链路
3.1 布局文件的结构与关键属性
高仿 QQ 登录页的布局通常分四块:顶部头像区、账号输入区、密码输入区、登录按钮区。我见过的实现里,最常见的是用ConstraintLayout做根布局,然后用Guideline做垂直定位参考线。
<!-- activity_login.xml 的核心结构 --> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg_login"> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/iv_avatar" android:layout_width="80dp" android:layout_height="80dp" android:src="@mipmap/ic_avatar_default" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="80dp"/> <EditText android:id="@+id/et_account" android:layout_width="0dp" android:layout_height="48dp" android:hint="QQ号/手机号/邮箱" android:inputType="text" android:maxLines="1" app:layout_constraintTop_toBottomOf="@id/iv_avatar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="40dp" android:layout_marginStart="32dp" android:layout_marginEnd="32dp"/> <EditText android:id="@+id/et_password" android:layout_width="0dp" android:layout_height="48dp" android:hint="密码" android:inputType="textPassword" android:maxLines="1" app:layout_constraintTop_toBottomOf="@id/et_account" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp" android:layout_marginStart="32dp" android:layout_marginEnd="32dp"/> <Button android:id="@+id/btn_login" android:layout_width="0dp" android:layout_height="48dp" android:text="登录" android:enabled="false" app:layout_constraintTop_toBottomOf="@id/et_password" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="32dp" android:layout_marginStart="32dp" android:layout_marginEnd="32dp"/> </androidx.constraintlayout.widget.ConstraintLayout>这段布局里几个参数值得说清楚。layout_width="0dp"配合constraintStart_toStartOf和constraintEnd_toEndOf是 ConstraintLayout 里实现「撑满剩余宽度」的标准写法,比match_parent更可控。android:inputType="textPassword"会让输入内容显示为圆点,但注意它不会自动禁止截屏,如果需要防截屏要在 Activity 里加FLAG_SECURE。android:maxLines="1"配合inputType="text"可以防止输入多行文本,但更严谨的做法是同时加android:singleLine="true"(已废弃但兼容老版本)或android:imeOptions="actionNext"来引导键盘的「下一项」按钮。
3.2 输入框的实时校验与按钮状态联动
登录按钮默认enabled="false",只有账号和密码都非空时才启用。这个逻辑通常用TextWatcher实现。
public class LoginActivity extends AppCompatActivity { private EditText etAccount; private EditText etPassword; private Button btnLogin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); etAccount = findViewById(R.id.et_account); etPassword = findViewById(R.id.et_password); btnLogin = findViewById(R.id.btn_login); TextWatcher watcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { // 两个输入框都非空才启用按钮 boolean accountOk = etAccount.getText().toString().trim().length() > 0; boolean passwordOk = etPassword.getText().toString().trim().length() > 0; btnLogin.setEnabled(accountOk && passwordOk); } }; etAccount.addTextChangedListener(watcher); etPassword.addTextChangedListener(watcher); } }afterTextChanged是三个回调里唯一能安全修改其他 View 状态的地方,因为此时文本已经完成变更。trim()是为了防止用户只输入空格就启用按钮。这里有个细节:如果账号输入框有「清除」按钮,点击清除后afterTextChanged也会触发,按钮状态会正确回退到禁用。
按钮的enabled状态变化最好配合background的 selector 使用,否则禁用状态和启用状态看起来一样,用户会困惑。
<!-- res/drawable/btn_login_selector.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false"> <shape android:shape="rectangle"> <solid android:color="#B0B0B0"/> <corners android:radius="24dp"/> </shape> </item> <item android:state_enabled="true"> <shape android:shape="rectangle"> <solid android:color="#12B7F5"/> <corners android:radius="24dp"/> </shape> </item> </selector>把这个 selector 设给btn_login的android:background,按钮禁用时是灰色,启用时是 QQ 蓝。注意state_enabled的顺序,false要写在true前面,否则启用状态会覆盖禁用状态。
3.3 登录点击的防抖与本地校验
登录按钮的点击事件里,第一件事不是发请求,而是防抖。用户可能连点,如果不处理,会触发多次登录逻辑。
public class LoginActivity extends AppCompatActivity { private static final long CLICK_INTERVAL = 1000L; private long lastClickTime = 0L; private void setupLoginButton() { btnLogin.setOnClickListener(v -> { long now = System.currentTimeMillis(); if (now - lastClickTime < CLICK_INTERVAL) { return; // 1 秒内重复点击直接忽略 } lastClickTime = now; String account = etAccount.getText().toString().trim(); String password = etPassword.getText().toString().trim(); // 本地格式校验 if (!isValidAccount(account)) { etAccount.setError("账号格式不正确"); return; } if (password.length() < 6) { etPassword.setError("密码至少 6 位"); return; } // 通过校验后再执行登录逻辑 performLogin(account, password); }); } private boolean isValidAccount(String account) { // QQ号纯数字,手机号11位数字,邮箱含@ if (account.matches("\\d{5,11}")) return true; return account.contains("@") && account.contains("."); } private void performLogin(String account, String password) { // 这里通常是发网络请求,源码项目里可能只是模拟延迟 btnLogin.setEnabled(false); btnLogin.setText("登录中..."); // 模拟请求 btnLogin.postDelayed(() -> { btnLogin.setEnabled(true); btnLogin.setText("登录"); Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show(); }, 1500); } }setError是EditText自带的错误提示,会在输入框右侧显示一个红色感叹号,点击后弹出错误文本。这个 API 在 Material 主题下表现不太一样,如果项目用了 Material Components 主题,建议改用TextInputLayout的setError。
防抖的CLICK_INTERVAL设 1000ms 是经验值。太短起不到防抖作用,太长用户会觉得按钮没反应。如果登录请求本身有 loading 状态,防抖可以适当放宽到 500ms,因为 loading 状态已经阻止了重复点击。
3.4 软键盘遮挡与焦点切换
登录页最影响体验的问题就是软键盘弹出后遮挡输入框或按钮。Android 的软键盘处理有三种模式:adjustResize、adjustPan、adjustNothing。登录页推荐用adjustResize,让窗口内容整体上移。
在AndroidManifest.xml里给 LoginActivity 设置:
<activity android:name=".LoginActivity" android:windowSoftInputMode="adjustResize|stateHidden" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>stateHidden表示进入页面时不自动弹出键盘,adjustResize表示键盘弹出时压缩窗口高度。注意在 Android 11 及以上,adjustResize需要配合WindowCompat.setDecorFitsSystemWindows(window, false)或者使用WindowInsets监听才能正确工作。
焦点切换方面,账号输入框的imeOptions设为actionNext,密码输入框设为actionDone,这样键盘右下角会显示「下一项」和「完成」。
<EditText android:id="@+id/et_account" android:imeOptions="actionNext" android:inputType="text" ... /> <EditText android:id="@+id/et_password" android:imeOptions="actionDone" android:inputType="textPassword" ... />然后在代码里处理actionDone:
etPassword.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE) { btnLogin.performClick(); // 触发登录按钮点击 return true; } return false; });performClick()会走完整的点击监听逻辑,包括防抖和校验,比直接调用performLogin更安全。
4. 避坑与排查:高仿登录页最容易翻车的五个地方
4.1 输入框在部分机型上被键盘顶飞
现象:在小米、华为等定制 ROM 上,软键盘弹出后整个布局被顶上去,顶部头像消失,或者输入框跑到屏幕外。
原因:adjustResize在这些 ROM 上行为不一致,部分 ROM 默认是adjustPan,导致窗口整体平移而不是压缩。
解决:在根布局外层套一个ScrollView,或者用NestedScrollView,让内容可滚动。同时在 Activity 里监听WindowInsets,动态调整底部 padding。
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root_layout), (v, insets) -> { Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); v.setPadding(0, 0, 0, imeInsets.bottom); return insets; });4.2 密码输入框的textPassword导致字体不一致
现象:密码框输入时字体和账号框不一样,或者光标位置偏移。
原因:textPassword会使用monospace字体族,部分 ROM 下还会改变光标宽度。
解决:在 XML 里显式指定android:fontFamily="sans-serif",或者用TextInputLayout的passwordToggleEnabled替代原生textPassword。
4.3 自定义清除按钮点击无效
现象:输入框右侧的清除图标点击没反应。
原因:清除按钮的点击区域太小,或者drawableRight的触摸事件没有正确处理。
解决:不要用drawableRight做清除按钮,改用FrameLayout包裹EditText和ImageView,给ImageView设置足够的padding和minWidth。
<FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/et_account" android:layout_width="match_parent" android:layout_height="48dp" android:paddingEnd="48dp"/> <ImageView android:id="@+id/iv_clear" android:layout_width="48dp" android:layout_height="48dp" android:layout_gravity="end" android:padding="12dp" android:src="@drawable/ic_clear" android:visibility="gone"/> </FrameLayout>4.4 登录按钮在低版本 Android 上背景不生效
现象:在 Android 5.0 以下设备上,按钮背景变成默认灰色,selector 不生效。
原因:android:background在低版本上对Button的默认样式覆盖不彻底。
解决:改用AppCompatButton,或者在主题里把colorButtonNormal设为透明。更彻底的做法是用androidx.appcompat.widget.AppCompatButton替代原生Button。
4.5 源码里的网络请求代码直接崩溃
现象:点击登录后应用闪退,Logcat 显示NetworkOnMainThreadException。
原因:源码项目里可能在主线程直接写了HttpURLConnection或OkHttp的同步请求。
解决:把网络请求放到子线程,或者用OkHttp的enqueue异步方法。如果只是学习布局,可以把网络请求部分注释掉,用postDelayed模拟。
// 错误写法:主线程同步请求 // URL url = new URL("https://example.com/login"); // HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 正确写法:异步请求 OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url("https://example.com/login").build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { runOnUiThread(() -> Toast.makeText(LoginActivity.this, "网络错误", Toast.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { runOnUiThread(() -> { // 更新 UI }); } });5. 从高仿到可用:把登录页改造成能接真实接口的模块
5.1 抽离登录逻辑到 ViewModel
高仿项目的登录逻辑通常全写在 Activity 里,改造成可用模块的第一步是把逻辑抽到ViewModel。这样配置变更(比如旋转屏幕)时登录状态不会丢失。
public class LoginViewModel extends ViewModel { private MutableLiveData<LoginState> loginState = new MutableLiveData<>(); public LiveData<LoginState> getLoginState() { return loginState; } public void login(String account, String password) { loginState.setValue(LoginState.loading()); // 模拟请求 new Handler(Looper.getMainLooper()).postDelayed(() -> { if (account.equals("123456") && password.equals("123456")) { loginState.setValue(LoginState.success("token_abc")); } else { loginState.setValue(LoginState.error("账号或密码错误")); } }, 1500); } }LoginState是一个密封类,包含loading、success、error三种状态。Activity 里观察loginState,根据状态更新 UI。
5.2 用 EncryptedSharedPreferences 存 token
登录成功后拿到的 token 不能明文存SharedPreferences。用EncryptedSharedPreferences做加密存储。
MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); SharedPreferences sharedPreferences = EncryptedSharedPreferences.create( context, "login_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); sharedPreferences.edit().putString("token", token).apply();这个 API 需要androidx.security:security-crypto依赖。注意它在部分低端设备上初始化较慢,建议放在子线程。
5.3 登录页的深色模式适配
高仿 QQ 登录页通常只有一套浅色背景。如果要适配深色模式,需要在res/values-night下放一套颜色资源。
<!-- res/values-night/colors.xml --> <resources> <color name="login_background">#121212</color> <color name="login_text_hint">#888888</color> <color name="login_button_bg">#0D8BD9</color> </resources>然后在布局里引用@color/login_background而不是硬编码颜色值。判断深色模式是否生效,可以在开发者选项里强制切换,或者用AppCompatDelegate.setDefaultNightMode。
5.4 一个验证改造是否成功的检查清单
改造完成后,按这个清单逐项验证:旋转屏幕后登录状态是否保留;token 是否加密存储(用adb backup导出后检查);深色模式下所有文字是否可读;登录按钮在快速连点 5 次后是否只触发一次请求;软键盘弹出时登录按钮是否可见。
我自己的习惯是,每次改完登录模块,都会在真机上用adb shell dumpsys activity看当前 Activity 栈,确认没有多余的 LoginActivity 实例。高仿项目里经常出现登录成功后没有finish()的情况,导致用户按返回键又回到登录页。这个坑我踩过不止一次,后来养成了在startActivity之后立刻finish()的习惯。希望帮到你。
本文还有配套的精品资源,点击获取