TP-Link Deco App Development
Deco App 完整项目结构设计
一、项目根目录结构
DecoApp/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/yourcompany/deco/ # Java/Kotlin源代码
│ │ │ ├── res/ # 资源文件
│ │ │ └── AndroidManifest.xml # 应用配置清单
│ │ └── test/ # 单元测试
│ └── build.gradle # 模块构建配置
├── gradle/ # Gradle配置
└── build.gradle # 项目构建配置
二、Java源代码目录详细结构
java/com/yourcompany/deco/
├── DecoApplication.java # 全局Application类
├── base/ # 基类文件夹
├── data/ # 数据层(Model)
├── ui/ # 界面层(View + ViewModel)
├── utils/ # 工具类
└── widget/ # 自定义控件
1. base/ 文件夹
作用:存放所有基类,减少代码重复
base/
├── BaseActivity.java # Activity基类:统一处理loading、错误提示等
├── BaseViewModel.java # ViewModel基类:统一处理LiveData
├── BaseDialog.java # Dialog基类:统一样式和动画
└── BaseRepository.java # Repository基类:统一错误处理
2. data/ 文件夹
作用:MVVM的Model层,处理所有数据相关操作
data/
├── api/ # 网络接口定义
│ ├── ApiService.java # 所有API接口定义(登录、获取设备等)
│ └── ApiConstants.java # API常量(BaseUrl、错误码等)
│
├── model/ # 数据模型
│ ├── request/ # 请求数据模型
│ │ ├── LoginRequest.java # 登录请求参数
│ │ └── RegisterRequest.java # 注册请求参数
│ │
│ ├── response/ # 响应数据模型
│ │ ├── LoginResponse.java # 登录响应数据
│ │ ├── DeviceResponse.java # 设备信息响应
│ │ └── BaseResponse.java # 响应基类
│ │
│ └── entity/ # 实体类
│ ├── User.java # 用户信息
│ ├── Device.java # 设备信息
│ └── Region.java # 地区信息
│
├── network/ # 网络配置
│ ├── NetworkManager.java # OkHttp配置和初始化
│ ├── HeaderInterceptor.java # 请求头拦截器
│ └── LoggingInterceptor.java # 日志拦截器
│
├── repository/ # 数据仓库
│ ├── AuthRepository.java # 认证相关数据处理
│ └── DeviceRepository.java # 设备相关数据处理
│
└── local/ # 本地存储
├── PreferenceHelper.java # SharedPreferences管理
└── DatabaseHelper.java # 数据库管理(如需要)
3. ui/ 文件夹
作用:MVVM的View和ViewModel层,所有界面相关代码
ui/
├── splash/ # 启动页模块
│ ├── SplashActivity.java # 显示logo,判断登录状态
│ └── SplashViewModel.java # 处理启动逻辑
│
├── agreement/ # 用户协议模块
│ ├── AgreementActivity.java # 显示协议内容,处理同意/拒绝
│ └── AgreementViewModel.java # 保存用户协议状态
│
├── auth/ # 认证相关模块
│ ├── choice/ # 登录/注册选择
│ │ └── AuthChoiceActivity.java # 显示登录/注册按钮
│ │
│ ├── login/ # 登录模块
│ │ ├── LoginActivity.java # 登录界面,包含地区选择
│ │ └── LoginViewModel.java # 处理登录逻辑,保存token
│ │
│ ├── register/ # 注册模块
│ │ ├── RegisterActivity.java # 注册界面,邮箱输入、密码设置
│ │ └── RegisterViewModel.java # 处理注册逻辑
│ │
│ └── verification/ # 邮箱验证模块
│ ├── EmailVerificationActivity.java # 验证提示界面
│ └── EmailVerificationViewModel.java # 处理验证状态
│
├── main/ # 主页模块
│ ├── MainActivity.java # 主界面框架,底部导航
│ ├── MainViewModel.java # 主页数据处理
│ │
│ ├── home/ # 首页(Network标签)
│ │ ├── HomeFragment.java # 显示网络状态、设备列表
│ │ ├── HomeViewModel.java # 获取设备数据
│ │ └── DeviceAdapter.java # 设备列表适配器
│ │
│ ├── family/ # Family标签
│ │ ├── FamilyFragment.java
│ │ └── FamilyViewModel.java
│ │
│ ├── smart/ # Smart标签
│ │ ├── SmartFragment.java
│ │ └── SmartViewModel.java
│ │
│ ├── discover/ # Discover标签
│ │ ├── DiscoverFragment.java
│ │ └── DiscoverViewModel.java
│ │
│ └── more/ # More标签
│ ├── MoreFragment.java
│ └── MoreViewModel.java
│
└── dialog/ # 对话框
├── RegionDialog.java # 地区选择对话框
├── ProblemHelpDialog.java # 问题帮助对话框
└── LoadingDialog.java # 加载中对话框
4. utils/ 文件夹
作用:通用工具类
utils/
├── NetworkUtils.java # 网络状态检查
├── ValidationUtils.java # 输入验证(邮箱、密码格式)
├── DeviceUtils.java # 获取设备信息(UUID、型号等)
├── ToastUtils.java # Toast统一管理
├── LogUtils.java # 日志工具
└── Constants.java # 全局常量
5. widget/ 文件夹
作用:自定义View控件
widget/
├── PasswordEditText.java # 密码输入框(显示/隐藏功能)
├── LoadingButton.java # 带加载状态的按钮
└── CircleImageView.java # 圆形图片(用户头像等)
三、资源文件目录结构
res/
├── layout/ # 布局文件
│ ├── activity_splash.xml
│ ├── activity_agreement.xml
│ ├── activity_auth_choice.xml
│ ├── activity_login.xml
│ ├── activity_register.xml
│ ├── activity_email_verification.xml
│ ├── activity_main.xml
│ ├── fragment_home.xml
│ ├── dialog_region_select.xml
│ ├── dialog_problem_help.xml
│ ├── item_device.xml # 设备列表项
│ └── item_region.xml # 地区列表项
│
├── drawable/ # 图片资源
│ ├── ic_logo.png # Deco logo
│ ├── ic_earth.xml # 地球图标
│ ├── ic_check.xml # 勾选图标
│ └── bg_button.xml # 按钮背景
│
├── values/ # 数值资源
│ ├── strings.xml # 文字
│ ├── colors.xml # 颜色
│ ├── styles.xml # 样式主题
│ └── dimens.xml # 尺寸
│
├── values-zh/ # 中文资源
│ └── strings.xml
│
└── anim/ # 动画资源
├── slide_in_bottom.xml
└── fade_in.xml
四、每个关键文件的具体内容说明
1. DecoApplication.java
作用:应用程序入口,初始化全局配置
内容:
- 初始化OkHttp
- 初始化日志系统
- 初始化SharedPreferences
- 设置全局异常捕获
2. LoginActivity.java
作用:用户登录界面
内容:
- 用户名输入框
- 密码输入框(带显示/隐藏)
- 地区选择(点击地球图标)
- 记住密码复选框
- 忘记密码链接
- 登录按钮点击处理
- 与LoginViewModel交互
3. LoginViewModel.java
作用:处理登录业务逻辑
内容:
- 调用AuthRepository进行登录
- 保存用户信息到本地
- 处理登录结果
- 管理加载状态
- 错误处理
4. ApiService.java
作用:定义所有网络接口
内容:
- 登录接口:/api/v2/account/captchaLogin
- 注册接口
- 获取设备列表接口
- 其他业务接口
5. MainActivity.java
作用:应用主界面框架
内容:
- ViewPager2或FragmentContainerView
- BottomNavigationView底部导航
- Fragment切换逻辑
- 处理返回键
6. HomeFragment.java
作用:首页展示网络和设备信息
内容:
- RecyclerView展示设备列表
- SwipeRefreshLayout下拉刷新
- 网络状态卡片
- 安全防护信息卡片
- 速度测试入口
1. AndroidManifest.xml 配置
<application
android:name=".DecoApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<!-- 启动页 -->
<activity
android:name=".ui.splash.SplashActivity"
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 欢迎页 -->
<activity
android:name=".ui.splash.WelcomeActivity"
android:theme="@style/WelcomeTheme" />
<!-- 协议页 -->
<activity
android:name=".ui.agreement.AgreementActivity" />
</application>
2. SplashActivity.java (启动图标页)
package com.yourcompany.deco.ui.splash;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
public class SplashActivity extends AppCompatActivity {
private static final int SPLASH_DELAY = 1000; // 1秒
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(SplashActivity.this, WelcomeActivity.class);
startActivity(intent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
}
}, SPLASH_DELAY);
}
}
3. activity_splash.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<ImageView
android:id="@+id/iv_logo"
android:layout_width="180dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
android:src="@drawable/deco_logo"
android:scaleType="centerInside" />
</RelativeLayout>
4. WelcomeActivity.java (欢迎页)
package com.yourcompany.deco.ui.splash;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.agreement.AgreementActivity;
public class WelcomeActivity extends AppCompatActivity {
private static final int WELCOME_DELAY = 2000; // 2秒
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(WelcomeActivity.this, AgreementActivity.class);
startActivity(intent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
}
}, WELCOME_DELAY);
}
}
5. activity_welcome.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 背景图 -->
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/welcome_background"
android:scaleType="centerCrop" />
<!-- Logo和文字容器 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:gravity="center_horizontal">
<!-- Deco Logo -->
<ImageView
android:id="@+id/iv_deco_logo"
android:layout_width="240dp"
android:layout_height="80dp"
android:src="@drawable/deco_logo"
android:scaleType="centerInside" />
<!-- 标语文字 -->
<TextView
android:id="@+id/tv_slogan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="Superior Mesh Wi-Fi,\nEverywhere"
android:textSize="24sp"
android:textColor="#666666"
android:gravity="center"
android:lineSpacing="8dp" />
</LinearLayout>
</RelativeLayout>
6. AgreementActivity.java (协议页)
@Override
protected void subscribeViewModel(@Nullable Bundle bundle) {
// 获取视图组件
CheckBox agreement1 = getViewBinding().agreement1;
CheckBox agreement2 = getViewBinding().agreement2;
TextView agreement1Text = getViewBinding().agreement1Text;
TextView agreement2Text = getViewBinding().agreement2Text;
Button nextButton = getViewBinding().nextButton;
// 初始化按钮状态为禁用
nextButton.setEnabled(false);
// 设置第一个协议的文本
String text1 = "I accept the Terms of Use and confirm that I have fully read and understood the Privacy Policy.";
SpannableString spannable1 = new SpannableString(text1);
// 设置"Terms of Use"为可点击
int termsStart = text1.indexOf("Terms of Use");
int termsEnd = termsStart + "Terms of Use".length();
spannable1.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
// 跳转到Terms of Use页面
startActivity(new Intent(AgreementActivity.this, TermsOfUseActivity.class));
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.parseColor("#00BCD4")); // 青色
ds.setUnderlineText(false);
}
}, termsStart, termsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置"Privacy Policy"为可点击
int privacyStart = text1.indexOf("Privacy Policy");
int privacyEnd = privacyStart + "Privacy Policy".length();
spannable1.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
// 跳转到Privacy Policy页面
startActivity(new Intent(AgreementActivity.this, PrivacyPolicyActivity.class));
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.parseColor("#00BCD4")); // 青色
ds.setUnderlineText(false);
}
}, privacyStart, privacyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
agreement1Text.setText(spannable1);
agreement1Text.setMovementMethod(LinkMovementMethod.getInstance());
// 设置第二个协议的文本
String text2 = "I confirm to join the User Experience Improvement Program. I understand that I can opt out of the program any time.";
SpannableString spannable2 = new SpannableString(text2);
// 设置"User Experience Improvement Program"为可点击
int programStart = text2.indexOf("User Experience Improvement Program");
int programEnd = programStart + "User Experience Improvement Program".length();
spannable2.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
// 跳转到User Experience Improvement Program页面
startActivity(new Intent(AgreementActivity.this, UserExperienceProgramActivity.class));
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.parseColor("#00BCD4")); // 青色
ds.setUnderlineText(false);
}
}, programStart, programEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
agreement2Text.setText(spannable2);
agreement2Text.setMovementMethod(LinkMovementMethod.getInstance());
// 只监听checkbox的选中状态变化
CompoundButton.OnCheckedChangeListener checkListener = (buttonView, isChecked) -> {
// 更新按钮状态:两个都选中才启用
boolean bothChecked = agreement1.isChecked() && agreement2.isChecked();
nextButton.setEnabled(bothChecked);
// 可选:改变按钮透明度
nextButton.setAlpha(bothChecked ? 1.0f : 0.5f);
};
agreement1.setOnCheckedChangeListener(checkListener);
agreement2.setOnCheckedChangeListener(checkListener);
// 重要:防止点击TextView触发checkbox
agreement1Text.setHighlightColor(Color.TRANSPARENT);
agreement2Text.setHighlightColor(Color.TRANSPARENT);
// Next按钮点击事件
nextButton.setOnClickListener(v -> {
if (agreement1.isChecked() && agreement2.isChecked()) {
// 继续下一步
// startActivity(new Intent(AgreementActivity.this, NextActivity.class));
// finish();
}
});
}
7. activity_agreement.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E8F8F5">
<!-- 中间装饰背景图 -->
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/welcome_background"
android:scaleType="centerCrop"
android:alpha="0.3" />
<!-- 内容层 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 上部分:Logo和文字 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<!-- Deco Logo -->
<ImageView
android:layout_width="200dp"
android:layout_height="66dp"
android:src="@drawable/deco_logo"
android:scaleType="centerInside" />
<!-- 标语 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="Superior Mesh Wi-Fi,\nEverywhere"
android:textSize="22sp"
android:textColor="#666666"
android:gravity="center"
android:lineSpacing="6dp" />
</LinearLayout>
<!-- 下部分:协议和按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="40dp"
android:paddingRight="40dp"
android:paddingTop="30dp"
android:paddingBottom="40dp">
<!-- 第一个协议 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="20dp">
<CheckBox
android:id="@+id/cb_terms"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="2dp"
android:button="@null"
android:background="@drawable/custom_checkbox"
android:checked="true" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="12dp"
android:text="I accept the Terms of Use and confirm that I have fully read and understood the Privacy Policy."
android:textSize="15sp"
android:textColor="#333333"
android:lineSpacing="3dp" />
</LinearLayout>
<!-- 第二个协议 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="40dp">
<CheckBox
android:id="@+id/cb_user_experience"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="2dp"
android:button="@null"
android:background="@drawable/custom_checkbox"
android:checked="true" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="12dp"
android:text="I confirm to join the User Experience Improvement Program. I understand that I can opt out of the program any time."
android:textSize="15sp"
android:textColor="#333333"
android:lineSpacing="3dp" />
</LinearLayout>
<!-- Continue按钮 -->
<Button
android:id="@+id/btn_continue"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Continue"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_cyan"
android:textAllCaps="false" />
<!-- Disagree and Quit -->
<TextView
android:id="@+id/tv_disagree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="Disagree and Quit"
android:textSize="16sp"
android:textColor="#00D4AA"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
8. 样式文件 styles.xml
<resources>
<!-- 基础主题 -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">#00D4AA</item>
<item name="colorPrimaryDark">#00B493</item>
<item name="colorAccent">#00D4AA</item>
</style>
<!-- 启动页主题 -->
<style name="SplashTheme" parent="AppTheme">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@android:color/white</item>
</style>
<!-- 欢迎页主题 -->
<style name="WelcomeTheme" parent="AppTheme">
<item name="android:windowFullscreen">true</item>
</style>
</resources>
9. button_continue.xml (按钮背景)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#00D4AA" />
<corners android:radius="28dp" />
</shape>
10. checkbox_selector.xml (复选框样式)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_checkbox_checked" android:state_checked="true" />
<item android:drawable="@drawable/ic_checkbox_unchecked" />
</selector>
11. PreferenceHelper.java (保存用户选择)
package com.yourcompany.deco.utils;
import android.content.Context;
import android.content.SharedPreferences;
public class PreferenceHelper {
private static final String PREF_NAME = "deco_prefs";
private static final String KEY_AGREEMENT_ACCEPTED = "agreement_accepted";
private static final String KEY_USER_EXPERIENCE = "user_experience_program";
private static PreferenceHelper instance;
private SharedPreferences prefs;
private PreferenceHelper(Context context) {
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public static PreferenceHelper getInstance(Context context) {
if (instance == null) {
instance = new PreferenceHelper(context);
}
return instance;
}
public void setAgreementAccepted(boolean accepted) {
prefs.edit().putBoolean(KEY_AGREEMENT_ACCEPTED, accepted).apply();
}
public boolean isAgreementAccepted() {
return prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false);
}
public void setUserExperienceProgram(boolean joined) {
prefs.edit().putBoolean(KEY_USER_EXPERIENCE, joined).apply();
}
public boolean isUserExperienceProgramJoined() {
return prefs.getBoolean(KEY_USER_EXPERIENCE, false);
}
}
1. AuthChoiceActivity.java (登录/注册选择页)
package com.yourcompany.deco.ui.auth.choice;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.auth.login.LoginActivity;
import com.yourcompany.deco.ui.auth.register.RegisterActivity;
public class AuthChoiceActivity extends AppCompatActivity {
private Button btnCreateAccount;
private TextView tvHaveAccount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth_choice);
initViews();
setListeners();
}
private void initViews() {
btnCreateAccount = findViewById(R.id.btn_create_account);
tvHaveAccount = findViewById(R.id.tv_have_account);
}
private void setListeners() {
btnCreateAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(AuthChoiceActivity.this, RegisterActivity.class));
}
});
tvHaveAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(AuthChoiceActivity.this, LoginActivity.class));
}
});
}
}
2. activity_auth_choice.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/welcome_background">
<!-- Logo和文字 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="120dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:layout_width="200dp"
android:layout_height="66dp"
android:src="@drawable/deco_logo"
android:scaleType="centerInside" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="Superior Mesh Wi-Fi,\nEverywhere"
android:textSize="20sp"
android:textColor="#666666"
android:gravity="center"
android:lineSpacing="6dp" />
</LinearLayout>
<!-- 按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="100dp"
android:orientation="vertical"
android:paddingLeft="40dp"
android:paddingRight="40dp">
<Button
android:id="@+id/btn_create_account"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Create a TP-Link ID"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_primary"
android:textAllCaps="false" />
<TextView
android:id="@+id/tv_have_account"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:text="Already have an account?"
android:textSize="18sp"
android:textColor="#00D4AA"
android:gravity="center"
android:background="@drawable/button_outline"
android:clickable="true" />
</LinearLayout>
</RelativeLayout>
3. RegisterActivity.java (注册页-邮箱输入)
package com.yourcompany.deco.ui.auth.register;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.dialog.RegionDialog;
public class RegisterActivity extends AppCompatActivity {
private ImageView ivBack;
private TextView tvIsp;
private EditText etEmail;
private TextView tvRegion;
private ImageView ivRegion;
private Button btnNext;
private TextView tvHaveAccount;
private String selectedRegion = "United States";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
initViews();
setListeners();
}
private void initViews() {
ivBack = findViewById(R.id.iv_back);
tvIsp = findViewById(R.id.tv_isp);
etEmail = findViewById(R.id.et_email);
tvRegion = findViewById(R.id.tv_region);
ivRegion = findViewById(R.id.iv_region);
btnNext = findViewById(R.id.btn_next);
tvHaveAccount = findViewById(R.id.tv_have_account);
tvRegion.setText(selectedRegion);
}
private void setListeners() {
ivBack.setOnClickListener(v -> finish());
// 邮箱输入监听
etEmail.addTextChangedListener(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) {
btnNext.setEnabled(s.length() > 0);
}
@Override
public void afterTextChanged(Editable s) {}
});
// 地区选择
View.OnClickListener regionClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
RegionDialog dialog = new RegionDialog(RegisterActivity.this);
dialog.setOnRegionSelectedListener(new RegionDialog.OnRegionSelectedListener() {
@Override
public void onRegionSelected(String region) {
selectedRegion = region;
tvRegion.setText(region);
}
});
dialog.show();
}
};
tvRegion.setOnClickListener(regionClickListener);
ivRegion.setOnClickListener(regionClickListener);
// Next按钮
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String email = etEmail.getText().toString();
Intent intent = new Intent(RegisterActivity.this, SetPasswordActivity.class);
intent.putExtra("email", email);
intent.putExtra("region", selectedRegion);
startActivity(intent);
}
});
// 已有账号
tvHaveAccount.setOnClickListener(v -> {
startActivity(new Intent(RegisterActivity.this, LoginActivity.class));
finish();
});
}
}
4. activity_register.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<!-- 顶部栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:padding="12dp"
android:src="@drawable/ic_back"
android:background="?attr/selectableItemBackgroundBorderless" />
<TextView
android:id="@+id/tv_isp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="16dp"
android:text="ISP"
android:textColor="#333333"
android:textSize="14sp"
android:background="@drawable/bg_isp_tag"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</RelativeLayout>
<!-- 内容区域 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create a TP-Link ID"
android:textSize="28sp"
android:textColor="#000000"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="This will be your TP-Link ID for managing your devices. We will send you an email to this address for verification."
android:textSize="16sp"
android:textColor="#666666"
android:lineSpacing="4dp" />
<!-- 邮箱输入 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="TP-Link ID (Email)"
android:textSize="14sp"
android:textColor="#999999" />
<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:hint="Your content"
android:inputType="textEmailAddress"
android:textSize="16sp"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
<!-- 地区选择 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:id="@+id/iv_region"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_earth" />
<TextView
android:id="@+id/tv_region"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="12dp"
android:text="United States"
android:textSize="16sp"
android:textColor="#00D4AA" />
</LinearLayout>
<!-- Next按钮 -->
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="80dp"
android:text="Next"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_primary"
android:enabled="false"
android:textAllCaps="false" />
<TextView
android:id="@+id/tv_have_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:text="Already have an account?"
android:textSize="16sp"
android:textColor="#999999"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
5. SetPasswordActivity.java (设置密码页)
package com.yourcompany.deco.ui.auth.register;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
public class SetPasswordActivity extends AppCompatActivity {
private ImageView ivBack;
private EditText etPassword;
private EditText etConfirmPassword;
private CheckBox cbTerms;
private CheckBox cbProgram;
private Button btnNext;
private String email;
private String region;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_set_password);
email = getIntent().getStringExtra("email");
region = getIntent().getStringExtra("region");
initViews();
setListeners();
}
private void initViews() {
ivBack = findViewById(R.id.iv_back);
etPassword = findViewById(R.id.et_password);
etConfirmPassword = findViewById(R.id.et_confirm_password);
cbTerms = findViewById(R.id.cb_terms);
cbProgram = findViewById(R.id.cb_program);
btnNext = findViewById(R.id.btn_next);
}
private void setListeners() {
ivBack.setOnClickListener(v -> finish());
TextWatcher textWatcher = 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) {
checkInputs();
}
@Override
public void afterTextChanged(Editable s) {}
};
etPassword.addTextChangedListener(textWatcher);
etConfirmPassword.addTextChangedListener(textWatcher);
cbTerms.setOnCheckedChangeListener((buttonView, isChecked) -> checkInputs());
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 这里应该调用注册API
Intent intent = new Intent(SetPasswordActivity.this, EmailVerificationActivity.class);
intent.putExtra("email", email);
startActivity(intent);
}
});
}
private void checkInputs() {
String password = etPassword.getText().toString();
String confirmPassword = etConfirmPassword.getText().toString();
boolean isValid = password.length() >= 6
&& password.equals(confirmPassword)
&& cbTerms.isChecked();
btnNext.setEnabled(isValid);
}
}
6. activity_set_password.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<!-- 顶部栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:padding="12dp"
android:src="@drawable/ic_back"
android:background="?attr/selectableItemBackgroundBorderless" />
</RelativeLayout>
<!-- 内容区域 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set Your Password"
android:textSize="28sp"
android:textColor="#000000"
android:textStyle="bold" />
<!-- 密码输入 -->
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="40dp"
android:hint="Password"
android:inputType="textPassword"
android:textSize="16sp"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
<!-- 确认密码 -->
<EditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:hint="Confirm Password"
android:inputType="textPassword"
android:textSize="16sp"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
<!-- 协议勾选 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<CheckBox
android:id="@+id/cb_terms"
android:layout_width="24dp"
android:layout_height="24dp"
android:button="@drawable/checkbox_selector" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="I accept the Terms of Use and confirm that I have fully read and understood the Privacy Policy."
android:textSize="14sp"
android:textColor="#666666" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cb_program"
android:layout_width="24dp"
android:layout_height="24dp"
android:button="@drawable/checkbox_selector" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="I confirm to join the User Experience Improvement Program. I understand that I can opt out of the program any time."
android:textSize="14sp"
android:textColor="#666666" />
</LinearLayout>
</LinearLayout>
<!-- Next按钮 -->
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="60dp"
android:text="Next"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_primary"
android:enabled="false"
android:textAllCaps="false" />
</LinearLayout>
</ScrollView>
</LinearLayout>
7. EmailVerificationActivity.java (邮箱验证页)
package com.yourcompany.deco.ui.auth.register;
import android.content.Intent;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.auth.login.LoginActivity;
import com.yourcompany.deco.ui.dialog.ProblemHelpDialog;
public class EmailVerificationActivity extends AppCompatActivity {
private ImageView ivBack;
private TextView tvEmail;
private TextView tvResend;
private Button btnActivated;
private String email;
private CountDownTimer countDownTimer;
private boolean isCountingDown = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_email_verification);
email = getIntent().getStringExtra("email");
initViews();
setListeners();
}
private void initViews() {
ivBack = findViewById(R.id.iv_back);
tvEmail = findViewById(R.id.tv_email);
tvResend = findViewById(R.id.tv_resend);
btnActivated = findViewById(R.id.btn_activated);
tvEmail.setText(email);
}
private void setListeners() {
ivBack.setOnClickListener(v -> finish());
// Resend按钮
tvResend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isCountingDown) {
startCountDown();
// 这里调用重发邮件API
}
}
});
// Activated & Log In按钮
btnActivated.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(EmailVerificationActivity.this, LoginActivity.class);
intent.putExtra("email", email);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
});
// Having problem?链接
TextView tvProblem = findViewById(R.id.tv_having_problem);
tvProblem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ProblemHelpDialog dialog = new ProblemHelpDialog(EmailVerificationActivity.this);
dialog.setEmail(email);
dialog.show();
}
});
}
private void startCountDown() {
isCountingDown = true;
tvResend.setEnabled(false);
countDownTimer = new CountDownTimer(60000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
tvResend.setText("Resend (" + seconds + "s)");
tvResend.setTextColor(getResources().getColor(R.color.text_gray));
}
@Override
public void onFinish() {
isCountingDown = false;
tvResend.setEnabled(true);
tvResend.setText("Resend");
tvResend.setTextColor(getResources().getColor(R.color.primary_color));
}
}.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (countDownTimer != null) {
countDownTimer.cancel();
}
}
}
8. activity_email_verification.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<!-- 顶部栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:padding="12dp"
android:src="@drawable/ic_back"
android:background="?attr/selectableItemBackgroundBorderless" />
</RelativeLayout>
<!-- 内容区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<!-- 邮件图标 -->
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_email_sent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Check Your Inbox"
android:textSize="28sp"
android:textColor="#000000"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="An email has been sent to"
android:textSize="16sp"
android:textColor="#666666" />
<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="username@tp-link.com"
android:textSize="18sp"
android:textColor="#000000"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Please follow the instructions in the email to activate your TP-Link ID within 1 hour."
android:textSize="14sp"
android:textColor="#999999"
android:gravity="center"
android:lineSpacing="4dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Didn't receive email? "
android:textSize="14sp"
android:textColor="#999999" />
<TextView
android:id="@+id/tv_resend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Resend"
android:textSize="14sp"
android:textColor="@color/primary_color"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="4dp" />
</LinearLayout>
<Button
android:id="@+id/btn_activated"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="40dp"
android:text="Activated & Log In"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_primary"
android:textAllCaps="false" />
<TextView
android:id="@+id/tv_having_problem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Having problem?"
android:textSize="14sp"
android:textColor="#999999"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>
9. LoginActivity.java (登录页)
package com.yourcompany.deco.ui.auth.login;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.main.MainActivity;
import com.yourcompany.deco.widget.LoadingButton;
public class LoginActivity extends AppCompatActivity {
private ImageView ivBack;
private EditText etUsername;
private EditText etPassword;
private LoadingButton btnLogin;
private TextView tvForgotPassword;
private TextView tvNoAccount;
private LoginViewModel loginViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loginViewModel = new ViewModelProvider(this).get(LoginViewModel.class);
initViews();
setListeners();
observeViewModel();
// 如果从注册页跳转过来,自动填充邮箱
String email = getIntent().getStringExtra("email");
if (email != null) {
etUsername.setText(email);
}
}
private void initViews() {
ivBack = findViewById(R.id.iv_back);
etUsername = findViewById(R.id.et_username);
etPassword = findViewById(R.id.et_password);
btnLogin = findViewById(R.id.btn_login);
tvForgotPassword = findViewById(R.id.tv_forgot_password);
tvNoAccount = findViewById(R.id.tv_no_account);
}
private void setListeners() {
ivBack.setOnClickListener(v -> finish());
TextWatcher textWatcher = 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) {
checkInputs();
}
@Override
public void afterTextChanged(Editable s) {}
};
etUsername.addTextChangedListener(textWatcher);
etPassword.addTextChangedListener(textWatcher);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = etUsername.getText().toString();
String password = etPassword.getText().toString();
loginViewModel.login(username, password);
}
});
tvForgotPassword.setOnClickListener(v -> {
// 跳转到忘记密码页面
});
tvNoAccount.setOnClickListener(v -> {
// 跳转到注册页面
});
}
private void checkInputs() {
String username = etUsername.getText().toString();
String password = etPassword.getText().toString();
btnLogin.setEnabled(!username.isEmpty() && !password.isEmpty());
}
private void observeViewModel() {
// 观察加载状态
loginViewModel.getIsLoading().observe(this, isLoading -> {
btnLogin.setLoading(isLoading);
});
// 观察登录结果
loginViewModel.getLoginResult().observe(this, result -> {
if (result != null && result.getError_code() == 0) {
// 登录成功,跳转到主页
startActivity(new Intent(LoginActivity.this, MainActivity.class));
finish();
}
});
// 观察错误信息
loginViewModel.getErrorMessage().observe(this, error -> {
if (error != null) {
// 显示错误提示
}
});
}
}
10. activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<!-- 顶部栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:padding="12dp"
android:src="@drawable/ic_back"
android:background="?attr/selectableItemBackgroundBorderless" />
</RelativeLayout>
<!-- 内容区域 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome to Deco"
android:textSize="28sp"
android:textColor="#000000"
android:textStyle="bold" />
<!-- 用户名输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="40dp"
android:orientation="horizontal"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_user"
android:tint="#999999" />
<EditText
android:id="@+id/et_username"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginLeft="12dp"
android:hint="Admin"
android:inputType="textEmailAddress"
android:textSize="16sp"
android:background="@null" />
<ImageView
android:id="@+id/iv_clear_username"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_clear"
android:visibility="gone" />
</LinearLayout>
<!-- 密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:background="@drawable/bg_input_field"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_lock"
android:tint="#999999" />
<EditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginLeft="12dp"
android:hint="Password"
android:inputType="textPassword"
android:textSize="16sp"
android:background="@null" />
</LinearLayout>
<TextView
android:id="@+id/tv_forgot_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="16dp"
android:text="Forgot Password"
android:textSize="14sp"
android:textColor="#999999"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="4dp" />
<!-- 登录按钮 -->
<com.yourcompany.deco.widget.LoadingButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="60dp"
android:text="Log In"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@drawable/button_primary"
android:enabled="false"
android:textAllCaps="false" />
<TextView
android:id="@+id/tv_no_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:text="Don't have an account?"
android:textSize="16sp"
android:textColor="#999999"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
11. LoadingButton.java (带加载状态的按钮)
package com.yourcompany.deco.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.widget.AppCompatButton;
import com.yourcompany.deco.R;
public class LoadingButton extends FrameLayout {
private AppCompatButton button;
private ProgressBar progressBar;
private String originalText;
public LoadingButton(Context context) {
super(context);
init(context);
}
public LoadingButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.widget_loading_button, this, true);
button = findViewById(R.id.button);
progressBar = findViewById(R.id.progress_bar);
}
public void setLoading(boolean loading) {
if (loading) {
originalText = button.getText().toString();
button.setText("");
progressBar.setVisibility(View.VISIBLE);
button.setEnabled(false);
} else {
button.setText(originalText);
progressBar.setVisibility(View.GONE);
button.setEnabled(true);
}
}
public void setText(String text) {
button.setText(text);
originalText = text;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
button.setEnabled(enabled);
}
@Override
public void setOnClickListener(OnClickListener listener) {
button.setOnClickListener(listener);
}
}
12. widget_loading_button.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@android:color/white" />
</FrameLayout>
13. 资源文件
colors.xml
<resources>
<color name="primary_color">#00D4AA</color>
<color name="text_gray">#999999</color>
</resources>
背景drawable文件
bg_input_field.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#F5F5F5" />
<corners android:radius="8dp" />
</shape>
button_primary.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape>
<solid android:color="#CCCCCC" />
<corners android:radius="28dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="#00D4AA" />
<corners android:radius="28dp" />
</shape>
</item>
</selector>
button_outline.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="2dp" android:color="#00D4AA" />
<corners android:radius="28dp" />
</shape>
bg_isp_tag.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFD700" />
<corners android:radius="4dp" />
</shape>
1. 网络层实现
NetworkManager.java (OkHttp配置)
package com.yourcompany.deco.data.network;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import java.util.concurrent.TimeUnit;
public class NetworkManager {
private static NetworkManager instance;
private OkHttpClient okHttpClient;
private static final String BASE_URL_BETA = "https://n-wap-beta.tplinkcloud.com";
private static final String BASE_URL_PROD = "https://n-wap-gw.tplinkcloud.com";
private NetworkManager() {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new HeaderInterceptor())
.addInterceptor(loggingInterceptor)
.build();
}
public static NetworkManager getInstance() {
if (instance == null) {
instance = new NetworkManager();
}
return instance;
}
public OkHttpClient getClient() {
return okHttpClient;
}
public String getBaseUrl() {
// 这里可以根据配置返回测试或生产环境
return BASE_URL_BETA;
}
}
HeaderInterceptor.java
package com.yourcompany.deco.data.network;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request.Builder builder = original.newBuilder()
.header("Content-Type", "application/json; charset=UTF-8")
.header("Accept-Encoding", "gzip")
.header("Connection", "Keep-Alive");
Request request = builder.build();
return chain.proceed(request);
}
}
ApiService.java
package com.yourcompany.deco.data.api;
import com.yourcompany.deco.data.model.request.LoginRequest;
import com.yourcompany.deco.data.model.request.DeviceListRequest;
import com.yourcompany.deco.data.model.response.LoginResponse;
import com.yourcompany.deco.data.model.response.DeviceListResponse;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.*;
import java.util.Map;
public interface ApiService {
@POST("/api/v2/account/captchaLogin")
Call<LoginResponse> login(
@Body LoginRequest request,
@QueryMap Map<String, String> params
);
@POST("/")
Call<DeviceListResponse> getDeviceList(
@Body DeviceListRequest request,
@QueryMap Map<String, String> params
);
}
AuthRepository.java
package com.yourcompany.deco.data.repository;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.yourcompany.deco.data.api.ApiService;
import com.yourcompany.deco.data.model.request.LoginRequest;
import com.yourcompany.deco.data.model.response.LoginResponse;
import com.yourcompany.deco.data.network.NetworkManager;
import com.yourcompany.deco.utils.DeviceUtils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.HashMap;
import java.util.Map;
public class AuthRepository {
private static AuthRepository instance;
private ApiService apiService;
private AuthRepository() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(NetworkManager.getInstance().getBaseUrl())
.client(NetworkManager.getInstance().getClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
apiService = retrofit.create(ApiService.class);
}
public static AuthRepository getInstance() {
if (instance == null) {
instance = new AuthRepository();
}
return instance;
}
public LiveData<LoginResponse> login(String username, String password) {
MutableLiveData<LoginResponse> result = new MutableLiveData<>();
// 构建请求参数
Map<String, String> queryParams = new HashMap<>();
queryParams.put("appName", "TP-Link_aria_Android");
queryParams.put("appVer", "3.8.33");
queryParams.put("netType", "wifi");
queryParams.put("termID", DeviceUtils.getDeviceId());
queryParams.put("ospf", "Android 12");
queryParams.put("brand", "TPLINK");
queryParams.put("locale", "en_US");
queryParams.put("model", DeviceUtils.getDeviceModel());
queryParams.put("termName", DeviceUtils.getDeviceModel());
queryParams.put("termMeta", "1");
LoginRequest request = new LoginRequest();
request.setAppType("TP-Link_aria_Android");
request.setAppVersion("3.8.33");
request.setCloudPassword(password);
request.setCloudUserName(username);
request.setPlatform("Android 12");
request.setRefreshTokenNeeded(false);
request.setTerminalMeta("1");
request.setTerminalName(DeviceUtils.getDeviceModel());
request.setTerminalUUID(DeviceUtils.getDeviceId());
apiService.login(request, queryParams).enqueue(new Callback<LoginResponse>() {
@Override
public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {
if (response.isSuccessful()) {
result.postValue(response.body());
} else {
result.postValue(null);
}
}
@Override
public void onFailure(Call<LoginResponse> call, Throwable t) {
result.postValue(null);
}
});
return result;
}
}
2. 主页实现
MainActivity.java
package com.yourcompany.deco.ui.main;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.yourcompany.deco.R;
import com.yourcompany.deco.ui.main.home.HomeFragment;
import com.yourcompany.deco.ui.main.family.FamilyFragment;
import com.yourcompany.deco.ui.main.smart.SmartFragment;
import com.yourcompany.deco.ui.main.discover.DiscoverFragment;
import com.yourcompany.deco.ui.main.more.MoreFragment;
public class MainActivity extends AppCompatActivity {
private BottomNavigationView bottomNav;
private Fragment currentFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
// 默认显示Home
if (savedInstanceState == null) {
showFragment(new HomeFragment());
}
}
private void initViews() {
bottomNav = findViewById(R.id.bottom_navigation);
bottomNav.setOnNavigationItemSelectedListener(item -> {
Fragment fragment = null;
switch (item.getItemId()) {
case R.id.nav_network:
fragment = new HomeFragment();
break;
case R.id.nav_family:
fragment = new FamilyFragment();
break;
case R.id.nav_smart:
fragment = new SmartFragment();
break;
case R.id.nav_discover:
fragment = new DiscoverFragment();
break;
case R.id.nav_more:
fragment = new MoreFragment();
break;
}
if (fragment != null) {
showFragment(fragment);
return true;
}
return false;
});
}
private void showFragment(Fragment fragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (currentFragment != null) {
transaction.hide(currentFragment);
}
String tag = fragment.getClass().getSimpleName();
Fragment existingFragment = getSupportFragmentManager().findFragmentByTag(tag);
if (existingFragment != null) {
transaction.show(existingFragment);
currentFragment = existingFragment;
} else {
transaction.add(R.id.fragment_container, fragment, tag);
currentFragment = fragment;
}
transaction.commit();
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@android:color/white"
app:menu="@menu/bottom_nav_menu"
app:labelVisibilityMode="labeled"
app:itemTextColor="@drawable/bottom_nav_selector"
app:itemIconTint="@drawable/bottom_nav_selector" />
</LinearLayout>