文章目录

1.home界面布局

在上一篇博客中,我们完成了SplashActivity的全部编写,现在开始需要进行主界面HomeActivity的编写,在这之前,我们还需要优化一下SplashActivity,加入一段动画

  1. 修改SplashActivity,新增initAnimation(),作为初始化动画的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

/**
* 文本控件
*/
private TextView tv_version_name;

/**
* 根布局
*/
private RelativeLayout rl_root;

/**
* 本地版本号
*/
private int mLocalVersionCode;

/**
* 更新时描述信息
*/
private String mVersionDes;

/**
* 更新时的URL
*/
private String mDownloadUrl;

private static final String tag = "SplashActivity";

/**
* Handler对象
*/
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case UPDATE_VERSION:
// 1.弹出对话框,提示用户更新
showUpdateDialog();
break;
case ENTER_HOME:
// 2.直接进入应用程序主界面
enterHome();
break;
case URL_ERROR:
// 3.弹出URL错误
ToastUtil.show(SplashActivity.this,"url异常");
enterHome();
break;
case IO_ERROR:
// 4.弹出IO错误
ToastUtil.show(SplashActivity.this,"IO异常");
enterHome();
break;
case JSON_ERROR:
// 5.弹出JSON错误
ToastUtil.show(SplashActivity.this,"json异常");
enterHome();
break;
default:break;
}
}
};

/**
* 更新新版本的状态码
*/
private static final int UPDATE_VERSION = 100;

/**
* 进入应用程序主界面的状态码
*/
private static final int ENTER_HOME = 101;

/**
* URL地址出错的状态码
*/
private static final int URL_ERROR = 102;

/**
* IO操作出错的状态码
*/
private static final int IO_ERROR = 103;

/**
* JSON解析出错的状态码
*/
private static final int JSON_ERROR = 104;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);

// 初始化UI
initUI();

// 初始化数据
initData();

// 初始化动画
initAnimation();
}


/**
* 1.初始化UI
*/
private void initUI() {
tv_version_name = findViewById(R.id.tv_version_name);
rl_root = findViewById(R.id.rl_root);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.获取应用版本名称
String versionName = getVersionName();
// 2.将应用版本名称设置到文本控件中
tv_version_name.setText("版本名称:" + versionName);
// 3.获取本地(客户端)版本号
mLocalVersionCode = getVersionCode();
// 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
/*
Json中内容应该包括:
1.更新版本的名称 versionName
2.新版本的描述信息 versionDes
3.服务器的版本号 versionCode
4.新版本apk下载地址 downloadUrl
*/
checkVersion();
}

/**
* 3.获取版本应用名称(在清单文件中)
* @return 版本名称
*/
private String getVersionName() {
// 1.获取包管理对象packageManager
PackageManager pm = getPackageManager();
// 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
try {
PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
// 3.获取并返回版本名称
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
// 4.抛出异常
return null;
}

/**
* 4.获取版本号(在清单文件中)
* @return 版本号
*/
private int getVersionCode() {
// 1.获取包管理对象packageManager
PackageManager pm = getPackageManager();
// 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
try {
PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
// 3.获取并返回版本编号
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}

/**
* 5.获取服务端版本号
*/
private void checkVersion() {
// 发送请求,获取数据,参数则为请求json的链接地址
new Thread(){
@Override
public void run() {
// 0.获取message对象
Message msg = Message.obtain();
long startTime = System.currentTimeMillis();// 获取时间戳
try {
// 1.封装url地址
URL url = new URL("http://10.0.2.2:8080/update74.json");
// 2.开启一个链接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 3.设置常见请求参数(请求头)
connection.setConnectTimeout(2000); // 请求超时
connection.setReadTimeout(2000); // 读取超时
//connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
// 4.获取相应码,200为请求成功
if (connection.getResponseCode() == 200){
// 5.以流的形式将数据获取下来
InputStream is = connection.getInputStream();
// 6.将流转换成字符串(工具封装类)
String json = StreamUtil.streamToString(is);
// 7.json解析
JSONObject jsonObject = new JSONObject(json);
String versionName = jsonObject.getString("versionName");
mVersionDes = jsonObject.getString("versionDes");
String versionCode = jsonObject.getString("versionCode");
mDownloadUrl = jsonObject.getString("downloadUrl");
// 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
if (Integer.parseInt(versionCode) > mLocalVersionCode){
// 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
msg.what = UPDATE_VERSION;
}else {
// 10.不需要更新,直接进入应用程序主界面
msg.what = ENTER_HOME;
}
}
}catch (MalformedURLException e) {
e.printStackTrace();
msg.what = URL_ERROR;
}catch (IOException e) {
e.printStackTrace();
msg.what = IO_ERROR;
}
catch (JSONException e) {
e.printStackTrace();
msg.what = JSON_ERROR;
}finally {
// 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
long endTime = System.currentTimeMillis();
if (endTime - startTime < 4000){
try {
Thread.sleep(4000 - (endTime - startTime));
} catch (Exception e) {
e.printStackTrace();
}
}
// 12.发送消息
mHandler.sendMessage(msg);
}
}
}.start();
}

/**
* 6.进入应用程序的主界面
*/
private void enterHome() {
Intent intent = new Intent(this, HomeActivity.class);
startActivity(intent);
finish(); // 开启新界面后,将导航界面销毁掉
}

/**
* 7.弹出更新对话框
*/
private void showUpdateDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
builder.setTitle("版本更新"); // 设置标题
builder.setMessage(mVersionDes); // 设置描述内容
builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 下载apk,需要apk的链接地址,即downloadUrl
downloadApk();
}
});// 积极按钮,“是”
builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 取消对话框,进入主界面
enterHome();
}
});// 消极按钮,“否”
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// 按下回退后,进入主界面,然后隐藏对话框
enterHome();
dialog.dismiss();
}
});// 回退按钮
builder.show();
}

/**
* 8.APK下载
*/
private void downloadApk() {
// 需要apk下载链接地址,放置apk的所在路径
// 1.判断sd卡是否可用,是否挂载
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
// 2.获取sd卡路径
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "app-release.apk";
Log.i(tag,"路径为:" + path);
// 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,回调方法)
HttpUtils httpUtils = new HttpUtils();
httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
@Override
public void onSuccess(ResponseInfo<File> responseInfo) {
// 下载成功(下载过后的放置在sd卡中apk)
Log.i(tag,"下载成功!");
File file = responseInfo.result;
installApk(file);
}

@Override
public void onFailure(HttpException e, String s) {
// 下载失败
Log.i(tag,"下载失败!");
e.printStackTrace();
}

@Override
public void onStart() {
// 刚刚开始下载
Log.i(tag,"刚刚开始下载!");
super.onStart();
}

@Override
public void onLoading(long total, long current, boolean isUploading) {
// 下载过程(下载文件大小,当前的下载位置,是否正在下载)
Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
super.onLoading(total, current, isUploading);
}
});
}
}

/**
* 9.APK安装
* @param file 安装文件
*/
private void installApk(File file) {
// 系统应用界面,源码,安装apk入口
Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.DEFAULT");
// 文件作为数据源
// intent.setData(Uri.fromFile(file));
// 设置安装的类型
// intent.setType("application/vnd.android.package-archive");
intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
startActivityForResult(intent,0);
}

/**
* 10.开启一个Activity后,返回结果
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
enterHome();
super.onActivityResult(requestCode, resultCode, data);
}

/**
* 11.初始化动画
*/
private void initAnimation() {
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(3000);
rl_root.startAnimation(alphaAnimation);
}
}
  1. 修改activity_splash.xml,给RelativeLayout增加id,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/launcher_bg"
tools:context=".activity.SplashActivity">

<!-- android:shadowRadius="5" 表示阴影所在范围 -->
<TextView
android:id="@+id/tv_version_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:shadowDx="1"
android:shadowDy="1"
android:shadowColor="#f00"
android:shadowRadius="5"
android:text="版本名称"/>

<ProgressBar
android:layout_below="@id/tv_version_name"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

</RelativeLayout>
  1. 为了让一些样式得到复用,我们将一些样式抽取到style.xml中,代码如下:
<resources>

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">"colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowNoTitle">true</item>
</style>

<style name="TitleStyle">"android:gravity">center</item>
<item name="android:textSize">20sp</item>
<item name="android:textColor">#000</item>
<item name="android:padding">10dp</item>
<item name="android:background">#0f0</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>

</resources>
  1. 开始编写HomeActivity,首先编写activity_home.xml,添加相应控件,注意这里有一个具有跑马灯效果的TextView控件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.HomeActivity">

<!-- 将对应属性抽取到样式当中去 -->
<TextView
android:text="功能列表"style="@style/TitleStyle"/>

<!-- android:ellipsize="end" 添加省略点的所在位置 -->
<!-- 跑马灯 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:ellipsize="marquee"/>

</LinearLayout>

2.自定义获取焦点的TextView

在上一节中,我们设置了具有跑马灯效果的TextView,然而,该控件并未实现该效果,实际上是因为焦点没有进行自定义获取的原因

  1. 修改activity_home.xml,对TextView控件进行相应修改,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.HomeActivity">

<!-- 将对应属性抽取到样式当中去 -->
<TextView
android:text="功能列表"style="@style/TitleStyle"/>

<!-- 想要实现跑马灯效果,需实现下面三条属性 -->
<!-- 1.android:ellipsize="end" 添加省略点的所在位置 -->
<!-- 2.android:focus="true" 获取焦点 -->
<!-- 3.android:focusableInTouchMode="true" 触摸时仍然获取焦点 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"/>

</LinearLayout>

注意:经测试,在高版本的Android环境下,仅在xml中对原生控件进行修改已经达不到跑马灯效果了,如果想要实现这个功能的读者,可以尝试自定义View,及以下将要讲解的部分

  1. 现在想要实现一个具有跑马灯效果的自定义控件,在包下新建view包,存放自定义控件,并新建FocusTextView类,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;

import androidx.annotation.Nullable;

/**
* 能够获取焦点的自定义TextView
*/
public class FocusTextView extends TextView {

/**
* 通过在Java代码来创建控件
* @param context 上下文
*/
public FocusTextView(Context context) {
super(context);
}

/**
* 通过在xml代码来创建控件
* @param context 上下文
* @param attrs 属性
*/
public FocusTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

/**
* 通过在xml代码(结合Style)来创建控件
* @param context 上下文
* @param attrs 属性
* @param defStyleAttr 样式
*/
public FocusTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

/**
* 获取焦点的方法
* @return
*/
@Override
public boolean isFocused() {
return true;
}
}
  1. 修改activity_home.xml,使用自定义控件FocusTextView来替换原有的TextView,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.HomeActivity">

<!-- 将对应属性抽取到样式当中去 -->
<TextView
android:text="功能列表"style="@style/TitleStyle"/>

<!-- 想要实现跑马灯效果,需实现下面三条属性 -->
<!-- 1.android:ellipsize="end" 添加省略点的所在位置
2.android:focus="true" 获取焦点
3.android:focusableInTouchMode="true" 触摸时仍然获取焦点
(可选)4.android:marqueeRepeatLimit="marquee_forever" 永远滚动 -->
<!--
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"/>
-->
<com.example.mobilesafe.view.FocusTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"/>

</LinearLayout>

3.自定义控件回顾

上一节我们写了一个简单的自定义控件,这里我们来回顾一下这个过程

  1. 创建一个类,继承至TextView
  2. 重写其构造方法
  3. 重写相应的属性方法
  4. 在xml布局文件中使用(注意调用时需要全路径名称)

4.九宫格使用

由于主菜单采用九宫格样式,这里可以使用GridView来实现

  1. 修改activity_home.xml,添加gridview控件,并进行相应配置,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.HomeActivity">

<!-- 将对应属性抽取到样式当中去 -->
<TextView
android:text="功能列表"style="@style/TitleStyle"/>

<!-- 想要实现跑马灯效果,需实现下面三条属性 -->
<!-- 1.android:ellipsize="end" 添加省略点的所在位置
2.android:focus="true" 获取焦点
3.android:focusableInTouchMode="true" 触摸时仍然获取焦点
(可选)4.android:marqueeRepeatLimit="marquee_forever" 永远滚动 -->
<!--
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"/>
-->
<com.example.mobilesafe.view.FocusTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
android:textColor="#000"
android:singleLine="true"
android:padding="5dp"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"/>

<!-- android:numColumns 指定列数 -->
<GridView
android:id="@+id/gv_home"
android:numColumns="3"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>
  1. 修改HomeActivity,处理GridView相关逻辑,添加适配器内部类,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}
}
  1. 在res/layout下新建gridview_item.xml,作为子项的布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_launcher"/>

<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模块标题"/>

</LinearLayout>

5.设置中心——条目布局结构

在上一节中,我们完成了主界面的编写,这一节我们开始编写主界面中九个条目(模块)的点击事件,这里我们首先编写九宫格中最后一个条目:设置中心

  1. 修改HomeActivity,修改initData()方法,为GridView注册相应的点击事件,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 8:
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}
}
  1. 在activity包下新建SettingActivity,作为设置中心的活动,代码模板套用EmptyActivity即可,然后修改activity_setting.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.SettingActivity"
android:orientation="vertical">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:text="自动更新设置"
android:textColor="#000"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/tv_des"
android:text="自动更新已关闭"
android:textColor="#000"
android:textSize="18sp"
android:layout_below="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<CheckBox
android:id="@+id/cb_box"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<View
android:background="#000"
android:layout_below="@id/tv_des"
android:layout_width="match_parent"
android:layout_height="1dp"/>

</RelativeLayout>

</LinearLayout>

6.设置中心——自定义组合控件构成布局结构

在上一节中我们实现了一个条目,根据需求设置界面是由许多个这样的条目组成的,为了简化开发,我们可以使用自定义组合控件来封装这些组件,形成一个整体,如图所示:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_手机卫士

组合控件的核心思想是:

  • 将已经编写好的布局文件,抽取到一个类中去做管理,下次害需要使用此布局结构的时候,直接使用组合控件对应的对象即可
  • 将组合控件的布局,抽取到单独的一个xml中
  • 通过一个单独的类,去加载此段布局文件

下面开始编写代码

  1. 在res/layout目录下新建setting_item_view.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="wrap_content">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:text="自动更新设置"
android:textColor="#000"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/tv_des"
android:text="自动更新已关闭"
android:textColor="#000"
android:textSize="18sp"
android:layout_below="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<CheckBox
android:id="@+id/cb_box"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<View
android:background="#000"
android:layout_below="@id/tv_des"
android:layout_width="match_parent"
android:layout_height="1dp"/>

</RelativeLayout>

</RelativeLayout>
  1. 在view下新建SettingItemView,作为组合控件的类,继承RelativeLayout,注意这里在调用inflate时第三个参数需要使用this来挂载到view上,原理如图所示:
  2. Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_手机卫士_02

  3. 代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {
public SettingItemView(Context context) {
this(context,null);
}

public SettingItemView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
View.inflate(context,R.layout.setting_item_view,this);
// 2.获取自定义组合控件中的每个控件的实例
TextView tv_title = findViewById(R.id.tv_title);
TextView tv_des = findViewById(R.id.tv_des);
CheckBox cb_box = findViewById(R.id.cb_box);

}
}
  1. 修改activity_setting.xml,调用刚刚编写好的自定义组合控件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.SettingActivity"
android:orientation="vertical">

<TextViewstyle="@style/TitleStyle"
android:text="设置中心"/>

<com.example.mobilesafe.view.SettingItemView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

</LinearLayout>

7.设置中心——自定义组合控件中相关方法

在上一节中,我们编写了简单的自定义组合控件,这一节需要完善相关的方法

修改SettingItemView,新增isCheck()和setCheck()方法,实现在点击CheckBox时,改变TextView的文本(未选中/已选中),代码如下:

package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

/**
* CheckBox
*/
private CheckBox cb_box;

/**
* 文本描述控件
*/
private TextView tv_des;

public SettingItemView(Context context) {
this(context,null);
}

public SettingItemView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
View.inflate(context,R.layout.setting_item_view,this);
// 2.获取自定义组合控件中的每个控件的实例
TextView tv_title = findViewById(R.id.tv_title);
tv_des = findViewById(R.id.tv_des);
cb_box = findViewById(R.id.cb_box);
}

/**
* 1.判断SettingItemView是否选中
* @return SettingItemView是否选中状态
*/
public boolean isCheck() {
// 有CheckBox的选中结果,决定当前条目是否开启
return cb_box.isChecked();
}

/**
* 2.设置SettingItemView的选中状态
* @param isCheck 是否作为开启的变量,由点击过程中做传递
*/
public void setCheck(boolean isCheck){
// 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
cb_box.setChecked(isCheck);
if (isCheck){
// 开启
tv_des.setText("自动更新已开启");
}else {
// 关闭
tv_des.setText("自动更新已关闭");
}
}
}

8.设置中心——选中SettingItemView条目状态切换

上一节中我们完善了自定义组合控件的一些相关方法,现在需要进行进一步地完善

  1. 修改activity_setting.xml,给自定义控件加上id,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.SettingActivity"
android:orientation="vertical">

<TextViewstyle="@style/TitleStyle"
android:text="设置中心"/>

<com.example.mobilesafe.view.SettingItemView
android:id="@+id/siv_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

</LinearLayout>
  1. 修改SettingActivity,新增initUpdate(),作为修改CheckBox状态的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);

// 初始化更新
initUpdate();
}

/**
* 1.初始化"更新"条目的方法
*/
private void initUpdate() {
final SettingItemView siv_update = findViewById(R.id.siv_update);
siv_update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.获取之前的选中状态
boolean isCheck = siv_update.isCheck();
// 2.取反选中状态
siv_update.setCheck(!isCheck);
}
});

}
}

9.设置中心——事件传递 & 相应规则

上一节中我们完成了CheckBox的切换,但是逻辑中还存在一个涉及到事件传递的小Bug,这里我们来修复一下

先来复盘一下这个bug的产生原因:

SettingActivity对于布局文件的根布局,获取点击事件,此事件传递给SettingImageView

  • 若点击在SettingitemView非CheckBox区域,事件就由SettingImageView去做响应
  • 若点击在SettingitemViewCheckBox区域,事件就由SettingImageView传递给CheckBox,然后由CheckBox去做响应,则SettingItemView就不能再去响应此事件,则相应的点击事件则不会执行

为了解决这个问题,这里使用一个最简单粗暴的方法:禁止CheckBox可以被点击并且禁用其焦点,这样CheckBox的点击事件就无法消费,便会传回给上一级控件,也就是SettingImageView,原理图如下:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_Android_03

代码如下:

<?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="wrap_content">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:text="自动更新设置"
android:textColor="#000"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/tv_des"
android:text="自动更新已关闭"
android:textColor="#000"
android:textSize="18sp"
android:layout_below="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<CheckBox
android:id="@+id/cb_box"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<View
android:background="#000"
android:layout_below="@id/tv_des"
android:layout_width="match_parent"
android:layout_height="1dp"/>

</RelativeLayout>

</RelativeLayout>

10.设置中心——事件传递机制

在上面的这些小节中,我们大致完成了“设置中心”模块的编写,现在让我们以一张通俗易懂的图来复习一下前面因为遇到bug而面对的事件传递机制:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_json_04

11.设置中心——sp工具类编写

解决了第一个bug,现在需要面对第二个bug:需要保存条目选中/未选中的状态,不然会导致从当前Activity回退再进来时状态的不一致,这里可以选中使用SharedPreference来实现,这里就编写一个工具类来方便以后在代码中进行相应调用

在utils包下新建SharedPreferencesUtil,作为工具类,其中主要的api和读取和写入,代码如下:

package com.example.mobilesafe.utils;

import android.content.Context;
import android.content.SharedPreferences;

public class SharedPreferencesUtil {

private static SharedPreferences sp;

/**
* 1.写入(boolean)
* @param ctx 上下文
* @param key 键
* @param value 值
*/
public static void putBoolean(Context ctx,String key,boolean value){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
sp.edit().putBoolean(key,value).commit();
}

/**
* 2.读取(boolean)
* @param ctx 上下文
* @param key 键
* @param defValue (默认)值
* @return 默认值或者相应结果
*/
public static boolean getBoolean(Context ctx,String key,boolean defValue){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
return sp.getBoolean(key,defValue);
}
}

12.设置中心——sp存储更新状态

上一节中我们完成了sharedpreferences的工具类编写,现在就需要使用它来存储更新状态

  1. 修改SettingActivity,修改initUpdate()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);

// 初始化更新
initUpdate();
}

/**
* 1.初始化"更新"条目的方法
*/
private void initUpdate() {
final SettingItemView siv_update = findViewById(R.id.siv_update);
// 0.从sp中获取已有的开关状态,然后根据这一次存储的结果去做决定
boolean open_update = SharedPreferencesUtil.getBoolean(this, ConstantValue.OPEN_UPDATE, false);
siv_update.setCheck(open_update);
siv_update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.获取之前的选中状态
boolean isCheck = siv_update.isCheck();
// 2.取反选中状态
siv_update.setCheck(!isCheck);
// 3.将该状态存储到sp中
SharedPreferencesUtil.putBoolean(getApplicationContext(),ConstantValue.OPEN_UPDATE,!isCheck);
}
});

}
}
  1. 在包下新建constant包,然后在包下新建ConstantValue,作为存放一些静态常量的类,方便之后调用,代码如下:
package com.example.mobilesafe.constant;

public class ConstantValue {

/**
* 记录更新的状态
*/
public static final String OPEN_UPDATE = "open_update";

}

13.设置中心——使用sp来判断客户端是否需要更新

前面的小节我们使用了sp存储了更新状态,为了让该设置(即自动更新的功能)生效,这里我们再延伸一下,用于SplashActivity中的更新功能

修改SplashActivity,修改initData()方法,使用Sharepreferences(记录了更新状态,选择自动更新就会进入更新逻辑,若没有选择自动更新则直接进入主界面)来判断是否需要更新,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

/**
* 文本控件
*/
private TextView tv_version_name;

/**
* 根布局
*/
private RelativeLayout rl_root;

/**
* 本地版本号
*/
private int mLocalVersionCode;

/**
* 更新时描述信息
*/
private String mVersionDes;

/**
* 更新时的URL
*/
private String mDownloadUrl;

private static final String tag = "SplashActivity";

/**
* Handler对象
*/
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case UPDATE_VERSION:
// 1.弹出对话框,提示用户更新
showUpdateDialog();
break;
case ENTER_HOME:
// 2.直接进入应用程序主界面
enterHome();
break;
case URL_ERROR:
// 3.弹出URL错误
ToastUtil.show(SplashActivity.this,"url异常");
enterHome();
break;
case IO_ERROR:
// 4.弹出IO错误
ToastUtil.show(SplashActivity.this,"IO异常");
enterHome();
break;
case JSON_ERROR:
// 5.弹出JSON错误
ToastUtil.show(SplashActivity.this,"json异常");
enterHome();
break;
default:break;
}
}
};

/**
* 更新新版本的状态码
*/
private static final int UPDATE_VERSION = 100;

/**
* 进入应用程序主界面的状态码
*/
private static final int ENTER_HOME = 101;

/**
* URL地址出错的状态码
*/
private static final int URL_ERROR = 102;

/**
* IO操作出错的状态码
*/
private static final int IO_ERROR = 103;

/**
* JSON解析出错的状态码
*/
private static final int JSON_ERROR = 104;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);

// 初始化UI
initUI();

// 初始化数据
initData();

// 初始化动画
initAnimation();
}


/**
* 1.初始化UI
*/
private void initUI() {
tv_version_name = findViewById(R.id.tv_version_name);
rl_root = findViewById(R.id.rl_root);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.获取应用版本名称
String versionName = getVersionName();
// 2.将应用版本名称设置到文本控件中
tv_version_name.setText("版本名称:" + versionName);
// 3.获取本地(客户端)版本号
mLocalVersionCode = getVersionCode();
// 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
/*
Json中内容应该包括:
1.更新版本的名称 versionName
2.新版本的描述信息 versionDes
3.服务器的版本号 versionCode
4.新版本apk下载地址 downloadUrl
*/
if (SharedPreferencesUtil.getBoolean(this, ConstantValue.OPEN_UPDATE,false)){
checkVersion();
}
else {
// 这里不调用enterHome(),是因为直接调用会很快进入主界面(HomeActivity),跳过闪屏页面(SplashActivity)
// 也不选择Thread.sleep(4000)后再enterHome(),是因为在主线程阻塞4秒风险太大,若到达7秒则会ANR
// 所以选择发送消息的形式来实现在没有选择“自动更新”的情况下直接进入主界面,即时发送消息后,延时4秒钟,否则会太快进入主界面
mHandler.sendEmptyMessageDelayed(ENTER_HOME,4000);
}
}

/**
* 3.获取版本应用名称(在清单文件中)
* @return 版本名称
*/
private String getVersionName() {
// 1.获取包管理对象packageManager
PackageManager pm = getPackageManager();
// 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
try {
PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
// 3.获取并返回版本名称
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
// 4.抛出异常
return null;
}

/**
* 4.获取版本号(在清单文件中)
* @return 版本号
*/
private int getVersionCode() {
// 1.获取包管理对象packageManager
PackageManager pm = getPackageManager();
// 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
try {
PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
// 3.获取并返回版本编号
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}

/**
* 5.获取服务端版本号
*/
private void checkVersion() {
// 发送请求,获取数据,参数则为请求json的链接地址
new Thread(){
@Override
public void run() {
// 0.获取message对象
Message msg = Message.obtain();
long startTime = System.currentTimeMillis();// 获取时间戳
try {
// 1.封装url地址
URL url = new URL("http://10.0.2.2:8080/update74.json");
// 2.开启一个链接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 3.设置常见请求参数(请求头)
connection.setConnectTimeout(2000); // 请求超时
connection.setReadTimeout(2000); // 读取超时
//connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
// 4.获取相应码,200为请求成功
if (connection.getResponseCode() == 200){
// 5.以流的形式将数据获取下来
InputStream is = connection.getInputStream();
// 6.将流转换成字符串(工具封装类)
String json = StreamUtil.streamToString(is);
// 7.json解析
JSONObject jsonObject = new JSONObject(json);
String versionName = jsonObject.getString("versionName");
mVersionDes = jsonObject.getString("versionDes");
String versionCode = jsonObject.getString("versionCode");
mDownloadUrl = jsonObject.getString("downloadUrl");
// 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
if (Integer.parseInt(versionCode) > mLocalVersionCode){
// 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
msg.what = UPDATE_VERSION;
}else {
// 10.不需要更新,直接进入应用程序主界面
msg.what = ENTER_HOME;
}
}
}catch (MalformedURLException e) {
e.printStackTrace();
msg.what = URL_ERROR;
}catch (IOException e) {
e.printStackTrace();
msg.what = IO_ERROR;
}
catch (JSONException e) {
e.printStackTrace();
msg.what = JSON_ERROR;
}finally {
// 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
long endTime = System.currentTimeMillis();
if (endTime - startTime < 4000){
try {
Thread.sleep(4000 - (endTime - startTime));
} catch (Exception e) {
e.printStackTrace();
}
}
// 12.发送消息
mHandler.sendMessage(msg);
}
}
}.start();
}

/**
* 6.进入应用程序的主界面
*/
private void enterHome() {
Intent intent = new Intent(this, HomeActivity.class);
startActivity(intent);
finish(); // 开启新界面后,将导航界面销毁掉
}

/**
* 7.弹出更新对话框
*/
private void showUpdateDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
builder.setTitle("版本更新"); // 设置标题
builder.setMessage(mVersionDes); // 设置描述内容
builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 下载apk,需要apk的链接地址,即downloadUrl
downloadApk();
}
});// 积极按钮,“是”
builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 取消对话框,进入主界面
enterHome();
}
});// 消极按钮,“否”
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// 按下回退后,进入主界面,然后隐藏对话框
enterHome();
dialog.dismiss();
}
});// 回退按钮
builder.show();
}

/**
* 8.APK下载
*/
private void downloadApk() {
// 需要apk下载链接地址,放置apk的所在路径
// 1.判断sd卡是否可用,是否挂载
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
// 2.获取sd卡路径
// String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "app-release.apk";
String path = (SplashActivity.this).getExternalFilesDir(null) + File.separator + "app-release.apk";
Log.i(tag,"路径为:" + path);
// 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,回调方法)
HttpUtils httpUtils = new HttpUtils();
httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
@Override
public void onSuccess(ResponseInfo<File> responseInfo) {
// 下载成功(下载过后的放置在sd卡中apk)
Log.i(tag,"下载成功!");
File file = responseInfo.result;
installApk(file);
}

@Override
public void onFailure(HttpException e, String s) {
// 下载失败
Log.i(tag,"下载失败!");
e.printStackTrace();
}

@Override
public void onStart() {
// 刚刚开始下载
Log.i(tag,"刚刚开始下载!");
super.onStart();
}

@Override
public void onLoading(long total, long current, boolean isUploading) {
// 下载过程(下载文件大小,当前的下载位置,是否正在下载)
Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
super.onLoading(total, current, isUploading);
}
});
}
}

/**
* 9.APK安装
* @param file 安装文件
*/
private void installApk(File file) {
// 系统应用界面,源码,安装apk入口
Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.DEFAULT");
// 文件作为数据源
// intent.setData(Uri.fromFile(file));
// 设置安装的类型
// intent.setType("application/vnd.android.package-archive");
intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
startActivityForResult(intent,0);
}

/**
* 10.开启一个Activity后,返回结果
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
enterHome();
super.onActivityResult(requestCode, resultCode, data);
}

/**
* 11.初始化动画
*/
private void initAnimation() {
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(3000);
rl_root.startAnimation(alphaAnimation);
}
}

注意:

1.在else分支中,这里不调用enterHome(),是因为直接调用会很快进入主界面(HomeActivity),跳过闪屏页面(SplashActivity)
2.也不选择Thread.sleep(4000)后再enterHome(),是因为在主线程阻塞4秒风险太大,若到达7秒则会ANR
3.所以选择发送消息的形式来实现在没有选择“自动更新”的情况下直接进入主界面,即时发送消息后,延时4秒钟,否则会太快进入主界面

14.设置中心——自定义属性申明

现在我们完成了设置中心中“自动更新”的功能,如图所示:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_json_05

为了增加其他的功能,需要增加其他的条目(即复用之前的自定义组合控件),为了让每个条目都有所区别,这里就需要运用到一些自定义属性

参照源码,在res/value下新建attrs.xml,存放一些用到的自定义属性,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="com.example.mobilesafe.view.SettingItemView">
<!-- 标题内容 -->
<attr name="destitle" format="string"/>
<!-- 单选框关闭时的文本内容 -->
<attr name="desoff" format="string"/>
<!-- 单选框开启时的文本内容 -->
<attr name="deson" format="string"/>
</declare-styleable>
</resources>

15.设置中心——构造方法中获取自定义属性值

上一节中我们申明了自定义属性,这一节需要我们在构造方法中使用这些属性,大概分为这几步:

  • 定义名空间namespace:类似于​​xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"​
  • 在控件中调用相关属性:类似于​​mobilesafe:destitle="自动更新设置"​
  • 需要在view类中获取相应属性值

现在开始编写代码

  1. 修改activity_setting.xml,调用自定义属性,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.SettingActivity"
android:orientation="vertical">

<TextViewstyle="@style/TitleStyle"
android:text="设置中心"/>

<!-- 自动更新 -->
<com.example.mobilesafe.view.SettingItemView
xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
android:id="@+id/siv_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
mobilesafe:destitle="自动更新设置"
mobilesafe:desoff="自动更新已关闭"
mobilesafe:deson="自动更新已开启"/>

</LinearLayout>
  1. 修改SettingItemView,添加initAttrs()方法,用于修改自定义属性集合,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

/**
* CheckBox
*/
private CheckBox cb_box;

/**
* 文本描述控件
*/
private TextView tv_des;

public SettingItemView(Context context) {
this(context,null);
}

public SettingItemView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
View.inflate(context,R.layout.setting_item_view,this);
// 2.获取自定义组合控件中的每个控件的实例
TextView tv_title = findViewById(R.id.tv_title);
tv_des = findViewById(R.id.tv_des);
cb_box = findViewById(R.id.cb_box);
// 3.获取自定义以及原生属性,从AttributeSet attrs参数中获取
initAttrs(attrs);
}

/**
* 1.判断SettingItemView是否选中
* @return SettingItemView是否选中状态
*/
public boolean isCheck() {
// 有CheckBox的选中结果,决定当前条目是否开启
return cb_box.isChecked();
}

/**
* 2.设置SettingItemView的选中状态
* @param isCheck 是否作为开启的变量,由点击过程中做传递
*/
public void setCheck(boolean isCheck){
// 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
cb_box.setChecked(isCheck);
if (isCheck){
// 开启
tv_des.setText("自动更新已开启");
}else {
// 关闭
tv_des.setText("自动更新已关闭");
}
}

/**
* 3.初始化属性
* @param attrs 维护好的属性集合
*/
private void initAttrs(AttributeSet attrs) {
}
}

16.设置中心——给自定义组合控件内部控件赋值

上一节中我们定义了initAttrs()方法,现在来完善它的具体实现

  1. 修改SettingItemView,修改initAttrs()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

/**
* 自定义的命名空间
*/
private static final String NAMESPACE = "http://schemas.android.com/apk/res/com.example.mobilesafe";

/**
* CheckBox
*/
private CheckBox cb_box;

/**
* 文本描述控件
*/
private TextView tv_des;

/**
* 自定义命名空间:标题
*/
private String mDestitle;

/**
* 自定义命名空间:关闭按钮后文本
*/
private String mDesoff;

/**
* 自定义命名空间:开启按钮后文本
*/
private String mDeson;

public SettingItemView(Context context) {
this(context,null);
}

public SettingItemView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
View.inflate(context,R.layout.setting_item_view,this);
// 2.获取自定义组合控件中的每个控件的实例
TextView tv_title = findViewById(R.id.tv_title);
tv_des = findViewById(R.id.tv_des);
cb_box = findViewById(R.id.cb_box);
// 3.获取自定义以及原生属性,从AttributeSet attrs参数中获取
initAttrs(attrs);
// 4.为控件赋值
tv_title.setText(mDestitle);

}

/**
* 1.判断SettingItemView是否选中
* @return SettingItemView是否选中状态
*/
public boolean isCheck() {
// 有CheckBox的选中结果,决定当前条目是否开启
return cb_box.isChecked();
}

/**
* 2.设置SettingItemView的选中状态
* @param isCheck 是否作为开启的变量,由点击过程中做传递
*/
public void setCheck(boolean isCheck){
// 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
cb_box.setChecked(isCheck);
if (isCheck){
// 开启
tv_des.setText(mDeson);
}else {
// 关闭
tv_des.setText(mDesoff);
}
}

/**
* 3.初始化属性
* @param attrs 维护好的属性集合
*/
private void initAttrs(AttributeSet attrs) {
mDestitle = attrs.getAttributeValue(NAMESPACE, "destitle");
mDesoff = attrs.getAttributeValue(NAMESPACE, "desoff");
mDeson = attrs.getAttributeValue(NAMESPACE, "deson");
}
}
  1. 修改setting_item_view.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="wrap_content">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:textColor="#000"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/tv_des"
android:textColor="#000"
android:textSize="18sp"
android:layout_below="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<CheckBox
android:id="@+id/cb_box"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<View
android:background="#000"
android:layout_below="@id/tv_des"
android:layout_width="match_parent"
android:layout_height="1dp"/>

</RelativeLayout>

</RelativeLayout>

17.手机防盗——是否有密码区分对话框类型

设置中心的第一个功能——自动更新可以先告一段落了,现在我们先编写九宫格中的第二个模块——手机防盗,其中的一个功能就是设置密码,如图所示:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_设置中心_06

在首次使用该功能时,需要设置一个初始密码,之后才能进入,后面再从主界面进入时,需要输入初始密码,才能使用该功能,如图所示:

Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密_android_07

现在我们先来实现这个对话框的逻辑

  1. 修改SharedPreferencesUtil,添加读写字符串的方法,用作存储密码,代码如下:
package com.example.mobilesafe.utils;

import android.content.Context;
import android.content.SharedPreferences;

public class SharedPreferencesUtil {

private static SharedPreferences sp;

/**
* 1.写入(boolean)
* @param ctx 上下文
* @param key 键
* @param value 值
*/
public static void putBoolean(Context ctx,String key,boolean value){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
sp.edit().putBoolean(key,value).commit();
}

/**
* 2.读取(boolean)
* @param ctx 上下文
* @param key 键
* @param defValue (默认)值
* @return 默认值或者相应结果
*/
public static boolean getBoolean(Context ctx,String key,boolean defValue){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
return sp.getBoolean(key,defValue);
}

/**
* 3.写入(string)
* @param ctx 上下文
* @param key 键
* @param value 值
*/
public static void putString(Context ctx,String key,String value){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
sp.edit().putString(key,value).commit();
}

/**
* 4.读取(string)
* @param ctx 上下文
* @param key 键
* @param defValue (默认)值
* @return 默认值或者相应结果
*/
public static String getString(Context ctx,String key,String defValue){
if (sp == null){
sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
}
return sp.getString(key,defValue);
}
}
  1. 修改ConstantValue,新增一个代表密码的标识符,代码如下:
package com.example.mobilesafe.constant;

public class ConstantValue {

/**
* 记录更新的状态
*/
public static final String OPEN_UPDATE = "open_update";

/**
* 手机防盗——设置密码的状态
*/
public static final String MOBILE_SAFE_PASSWORD = "mobile_safe_password";

}
  1. 修改HomeActivity,添加showDialog(),作为弹出密码编辑的对话框的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
case 8:
// 设置中心
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}

/**
* 4.手机防盗——密码对话框
*/
private void showDialog() {
// 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
if (TextUtils.isEmpty(password)){
// 2.初始设置密码对话框
showSetPasswordDialog();
}else {
// 3.确认密码对话框
showConfirmPasswordDialog();
}
}

/**
* 5.初次设置密码对话框
*/
private void showSetPasswordDialog() {
}

/**
* 6.再次设置密码对话框
*/
private void showConfirmPasswordDialog() {
}
}

18.手机防盗——设置密码对话框

上一节中我们完成了密码对话框的骨架,现在来进一步完善编写的两个方法

  1. 修改HomeActivity,完善showSetPasswordDialog()方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
break;
case 8:
// 设置中心
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}

/**
* 4.手机防盗——密码对话框
*/
private void showDialog() {
// 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
if (TextUtils.isEmpty(password)){
// 2.初始设置密码对话框
showSetPasswordDialog();
}else {
// 3.确认密码对话框
showConfirmPasswordDialog();
}
}

/**
* 5.初次设置密码对话框
*/
private void showSetPasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
AlertDialog dialog = builder.create();
View view = View.inflate(this, R.layout.dialog_set_password, null);
dialog.setView(view);
dialog.show();
}

/**
* 6.再次设置密码对话框
*/
private void showConfirmPasswordDialog() {
}
}
  1. 在res/layout下新建dialog_set_password,作为初始化密码的对话框的布局,代码如下:
<?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">

<TextViewstyle="@style/TitleStyle"
android:text="设置密码"
android:background="#f00"/>

<EditText
android:id="@+id/et_set_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="设置密码"/>

<EditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="确认密码"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/btn_submit"
android:text="确认"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

<Button
android:id="@+id/btn_cancel"
android:text="取消"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

</LinearLayout>

</LinearLayout>

19.手机防盗——对话初次设置密码验证过程

上一节中我们完成了初次设置密码的对话框的布局,现在需要完善相应的逻辑

  1. 修改HomeActivity,修改showSetPasswordDialog()方法,新建一个套用Empty Activity模板的TestActivity作为测试Activity,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.utils.ToastUtil;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
break;
case 8:
// 设置中心
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}

/**
* 4.手机防盗——密码对话框
*/
private void showDialog() {
// 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
if (TextUtils.isEmpty(password)){
// 2.初始设置密码对话框
showSetPasswordDialog();
}else {
// 3.确认密码对话框
showConfirmPasswordDialog();
}
}

/**
* 5.初次设置密码对话框
*/
private void showSetPasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AlertDialog dialog = builder.create();
final View view = View.inflate(this, R.layout.dialog_set_password, null);
dialog.setView(view);
dialog.show();
Button btn_submit = view.findViewById(R.id.btn_submit);
Button btn_cancel = view.findViewById(R.id.btn_cancel);
btn_submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText et_set_password = view.findViewById(R.id.et_set_password);
EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
String password = et_set_password.getText().toString();
String confirmpassword = et_confirm_password.getText().toString();
if (!TextUtils.isEmpty(password) && !TextUtils.isEmpty(confirmpassword)){
if(password.equals(confirmpassword)){
// 进入手机防盗模块
Intent intent = new Intent(getApplicationContext(), TestActivity.class);
startActivity(intent);
dialog.dismiss();
}else {
// 提示用户确认密码有误
ToastUtil.show(getApplicationContext(),"确认密码错误");
}
}else {
// 提示用户密码输入有为空
ToastUtil.show(getApplicationContext(),"请输入密码");
}
}
});
btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}

/**
* 6.再次设置密码对话框
*/
private void showConfirmPasswordDialog() {
}
}
  1. 修改dialog_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">

<TextViewstyle="@style/TitleStyle"
android:text="设置密码"
android:background="#f00"/>

<EditText
android:id="@+id/et_set_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="设置密码"/>

<EditText
android:id="@+id/et_confirm_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="确认密码"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/btn_submit"
android:text="确认"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

<Button
android:id="@+id/btn_cancel"
android:text="取消"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

</LinearLayout>

</LinearLayout>

20.手机防盗——确认密码对话框编写

前面我们完成了初次设置密码对话框的逻辑,现在需要编写确认密码对话框的逻辑

  1. 在res/layout下新建dialog_confirm_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">

<TextViewstyle="@style/TitleStyle"
android:text="确认密码"
android:background="#f00"/>

<EditText
android:id="@+id/et_confirm_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="确认密码"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/btn_submit"
android:text="确认"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

<Button
android:id="@+id/btn_cancel"
android:text="取消"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

</LinearLayout>

</LinearLayout>
  1. 修改HomeActivity,修改showConfirmPasswordDialog()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.utils.ToastUtil;

public class HomeActivity extends AppCompatActivity {

/**
* 存储标题
*/
private String[] mTitleStrs;

/**
* 存储图像
*/
private int[] mDrawableIds;

/**
* 网格对象
*/
private GridView gv_home;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);

// 初始化UI
initUI();

// 初始化数据
initData();
}

/**
* 1.初始化UI
*/
private void initUI() {
gv_home = findViewById(R.id.gv_home);
}

/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
break;
case 8:
// 设置中心
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}

/**
* 3.自定义的数据适配器类
*/
class MyAdapter extends BaseAdapter{

@Override
public int getCount() {
// 统计条目的总数
return mTitleStrs.length;
}

@Override
public Object getItem(int position) {
// 根据索引获取对象
return mTitleStrs[position];
}

@Override
public long getItemId(int position) {
// 获取索引
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 获取视图
View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
TextView tv_title = view.findViewById(R.id.tv_title);
ImageView iv_icon = view.findViewById(R.id.iv_icon);
tv_title.setText(mTitleStrs[position]);
iv_icon.setBackgroundResource(mDrawableIds[position]);
return view;
}
}

/**
* 4.手机防盗——密码对话框
*/
private void showDialog() {
// 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
if (TextUtils.isEmpty(password)){
// 2.初始设置密码对话框
showSetPasswordDialog();
}else {
// 3.确认密码对话框
showConfirmPasswordDialog();
}
}

/**
* 5.初次设置密码对话框
*/
private void showSetPasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AlertDialog dialog = builder.create();
final View view = View.inflate(this, R.layout.dialog_set_password, null);
dialog.setView(view);
dialog.show();
Button btn_submit = view.findViewById(R.id.btn_submit);
Button btn_cancel = view.findViewById(R.id.btn_cancel);
btn_submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText et_set_password = view.findViewById(R.id.et_set_password);
EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
String password = et_set_password.getText().toString();
String confirmpassword = et_confirm_password.getText().toString();
if (!TextUtils.isEmpty(password) && !TextUtils.isEmpty(confirmpassword)){
if(password.equals(confirmpassword)){
// 进入手机防盗模块
Intent intent = new Intent(getApplicationContext(), TestActivity.class);
startActivity(intent);
dialog.dismiss();
// 将密码存储到sp中
SharedPreferencesUtil.putString(getApplicationContext(),ConstantValue.MOBILE_SAFE_PASSWORD,password);
}else {
// 提示用户确认密码有误
ToastUtil.show(getApplicationContext(),"确认密码错误");
}
}else {
// 提示用户密码输入有为空
ToastUtil.show(getApplicationContext(),"请输入密码");
}
}
});
btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}

/**
* 6.再次设置密码对话框
*/
private void showConfirmPasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AlertDialog dialog = builder.create();
final View view = View.inflate(this, R.layout.dialog_confirm_password, null);
dialog.setView(view);
dialog.show();
Button btn_submit = view.findViewById(R.id.btn_submit);
Button btn_cancel = view.findViewById(R.id.btn_cancel);
btn_submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
String confirmpassword = et_confirm_password.getText().toString();
if (!TextUtils.isEmpty(confirmpassword)){
// 从sp中获取密码
String password = SharedPreferencesUtil.getString(getApplicationContext(), ConstantValue.MOBILE_SAFE_PASSWORD, "");
if(password.equals(confirmpassword)){
// 进入手机防盗模块
Intent intent = new Intent(getApplicationContext(), TestActivity.class);
startActivity(intent);
dialog.dismiss();
}else {
// 提示用户确认密码有误
ToastUtil.show(getApplicationContext(),"确认密码错误");
}
}else {
// 提示用户密码输入有为空
ToastUtil.show(getApplicationContext(),"请输入密码");
}
}
});
btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}
}

21.手机防盗——MD5加密过程

之前的小节中,我们完成了密码的设置和存储,但是密码是用明文存储在sp中的,为了应用安全,所以需要使用MD5加密算法进行一个加密

MD5:将字符串转换成32位的字符串(16进制),不可逆

在utils包下新建MD5Util,作为MD5加密算法的工具类,代码如下:

package com.example.mobilesafe.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

/**
* 给指定字符串按照MD5算法进行加密
* @param password 待加密的字符型
*/
public static void encoder(String password){
try {
// 1.指定加密算法类型
MessageDigest digest = MessageDigest.getInstance("MD5");
// 2.将需要加密的字符串中转换成byte类型的数组,然后进行随机的哈希过程
byte[] bs = digest.digest(password.getBytes());
// 3.循环遍历数组,然后让其生成32位字符串,固定写法
StringBuffer stringBuffer = new StringBuffer();
for (byte b : bs) {
int i = b & 0xff;
// 4.将int类型的i转换成16进制字符
String hexString = Integer.toHexString(i);
if (hexString.length() < 2){
hexString = "0" + hexString;
}
// 5.字符串拼接
stringBuffer.append(hexString);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}