Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast

文章目录

  • 1.高级工具——手机震动效果
  • 2.高级工具——电话号码归属地的显示需求
  • 3.高级工具——服务中电话状态的监听
  • 4.高级工具——服务和归属地显示状态的绑定
  • 5.高级工具——自定义Toast
  • 6.高级工具——选择Toast样式 & 自定义组合控件
  • 7.高级工具——Toast样式单选框设置
  • 8.高级工具——Toast显示样式修改
  • 9.高级工具——Toast提示框位置
  • 10.高级工具——Toast提示框拖拽过程中坐标的移动计算规则
  • 11.高级工具——Toast提示框移动过程中的容错处理
  • 12.高级工具——Toast提示框移动时提示信息进行相应移动

1.高级工具——手机震动效果

在上一节中,我们实现了编辑框抖动的效果。现在我们想要进一步实现手机震动的效果,修改QueryAddressActivity,主要使用到Vibrator类,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AddressDao;

public class QueryAddressActivity extends AppCompatActivity {

    private EditText et_phone;

    private Button btn_query;

    private TextView tv_query_result;

    private String mAddress;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.控制使用查询结果
            tv_query_result.setText(mAddress);
        }
    };

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

        // 初始化UI
        initUI();
    }

    private void initUI() {
        et_phone = findViewById(R.id.et_phone);
        btn_query = findViewById(R.id.btn_query);
        tv_query_result = findViewById(R.id.tv_query_result);

        // 1.点击查询功能,注册按钮的点击事件
        btn_query.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String phone = et_phone.getText().toString();
                if (!TextUtils.isEmpty(phone)){
                    // 2.查询是耗时操作,需要开启子线程
                    query(phone);
                }else {
                    Animation animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.shake);
                    et_phone.startAnimation(animation);
                    // 手机震动效果
                    Vibrator vibrator = (android.os.Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
                    vibrator.vibrate(2000); // 震动毫秒值
                    vibrator.vibrate(new long[]{2000,5000,2000,5000},-1); // 规律震动(震动规则(不震动时间,震动时间,不震动时间,震动时间,...),重复次数(-1表示不要重复震动))
                }
            }
        });

        // 5.实时查询
        et_phone.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) {
                // 文本发生改变时
            }

            @Override
            public void afterTextChanged(Editable s) {
                // 文本发生改变后
                String phone = et_phone.getText().toString();
                query(phone);
            }
        });

    }

    /**
     * 查询操作,在子线程中
     * @param phone 查询电话号码
     */
    private void query(final String phone) {
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phone);
                // 3.消息机制,告知主线程查询结束,可以去使用查询结果
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }
}

注意使用手机震动功能需要在清单文件中声明权限,代码如下:

<uses-permission android:name="android.permission.VIBRATE" /> 

2.高级工具——电话号码归属地的显示需求

我们之前获取到了电话号码归属地的信息,现在需要想要显示这个信息,这里可以使用Toast来实现。

首先在“设置中心”里新增这个条目,用于判断是否要打开显示电话归属地的功能,如图中的红框所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第1张图片

当电话打过来时,界面上会显示来电归属地的显示信息方框,并且该信息方框可以任意拖动,如图所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第2张图片
该显示需求具有以下特点:

  1. 电话归属地信息,在手机窗体上悬浮显示;
  2. 由于需要悬浮在手机上,所以悬浮框不停留在手机卫士应用(界面)中,这里就需要使用到Toast来实现了;
  3. 官方提供的Toast展示效果没有达到预期,需要我们自定义一个美观的Toast,这个Toast能够跟随手势去移动

另外,归属地的显示需求还可以在“设置中心”里进行配置,比如说Toast的方框是透明/不透明的,如图中的红框所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第3张图片

最后,在“设置中心”还可以配置Toast的方框的显示位置并记录,还可以在配置中自由挪动,如图所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第4张图片

3.高级工具——服务中电话状态的监听

这一节我们着手开始编写上一节中提及的功能,首先修改activity_setting.xml,添加一个我们之前实现的自定义View——SettingItemView,代码如下:


<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">

    <TextView
        style="@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="自动更新已开启"/>

    
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="电话归属地的显示设置"
        mobilesafe:desoff="归属地的显示已关闭"
        mobilesafe:deson="归属地的显示已开启"/>

LinearLayout>

随后,修改SettingActivity,添加initAddress()方法,用于初始化刚刚创建好的第二个条目,代码如下:

package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

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

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.service.AddressService;
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();

        // 初始化显示电话号码归属地
        initAddress();
    }

    /**
     * 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);
            }
        });
    }


    /**
     * 2.初始化“显示电话号码归属地”的方法
     */
    private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }
}

为了让Toast悬浮在手机上,这里可以开启一个Service,在Service中管理Toast的代码逻辑,整个业务的步骤大致如下:

  1. 点击是否开启归属地显示的自定义组合控件SettingItemView;
  2. 开启后,即会启动ServiceService中会管理Toast,管理的流程如下;
    1. 只有在来电的时候(响铃)状态显示Toast
    2. 挂断电话时,移除Toast
  3. 关闭后,就会关闭Service,取消Toast的显示。

新建AddressService,作为电话监听时管理归属地信息显示的Service,完善相应逻辑,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    break;
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
    }
}

注意,读取电话状态同样需要声明权限,在清单文件中添加以下代码即可:

    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

为了便于测试,修改AddressService,添加showToast()方法,用于显示来电号码信息,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.widget.Toast;

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    showToast(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 打印Toast
     */
    private void showToast(String phoneNumber) {
        Toast.makeText(getApplicationContext(), phoneNumber, Toast.LENGTH_SHORT).show();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
    }
}

4.高级工具——服务和归属地显示状态的绑定

为了让“设置中心”里针对第二个条目的设置保存下来,这里可以采用判断服务的状态来进行绑定。之所以没有选用sp进行存储,是因为可能会出现手机内存不够从而清理掉sp的情况。

在util包下新建一个ServiceUtil类,作为判断服务是否开启的工具类,t添加isRunning()方法,完善相应逻辑,代码如下:

package com.example.mobilesafe.utils;

import android.app.ActivityManager;
import android.content.Context;

import java.util.List;

public class ServiceUtil {

    /**
     * 判断服务是否运行
     * @param serviceName 服务名
     * @param context 上下文环境
     * @return 运行结果
     */
    public static boolean isRunning(Context context,String serviceName){
        // 1.获取activityManager管理者对象,可以去获取当前手机正在运行的所有服务
        ActivityManager mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        // 2.获取手机中正在运行的服务(多少个服务)
        List<ActivityManager.RunningServiceInfo> runningServices = mActivityManager.getRunningServices(100);
        // 3.遍历获取的所有的服务集合,拿到每一个服务的类名称和传递进来的类名称作比对,如果一致说明服务正在运行
        for (ActivityManager.RunningServiceInfo runningService : runningServices) {
            if (runningService.service.getClassName().equals(serviceName)){
                return true;
            }
        }
        return false;
    }
}

修改SettingActivity,修改initAddress(),使用刚刚编写好的ServiceUtil类来判断服务是否开启,并设置其状态,代码如下:

private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 通过ServiceUtil来判断服务是否开启
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.AddressService");
        siv_address.setCheck(isRunning);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }

5.高级工具——自定义Toast

我们之前完成了当电话响铃时监听到电话号码的功能,并且将其绑定到了功能上,现在需要自定义一个满足显示需求的Toast。

修改AddressService,修改showToast(),根据源码查找设置Toast的相应方法,并且进行相应修改,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;

import com.example.mobilesafe.R;

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    // Layout对象
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    // 自定义的Toast布局
    private View mViewToast;

    // 获取窗体对象
    private WindowManager mWindowsManager;

    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);

        // 5.获取窗体对象
        mWindowsManager = (WindowManager) getSystemService(WINDOW_SERVICE);
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动,挂断电话时需要移除Toast
                    if (mWindowsManager != null && mViewToast != null){
                        mWindowsManager.removeView(mViewToast);
                    }
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    showToast(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 打印Toast
     */
    private void showToast(String phoneNumber) {
        // 自定义Toast
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
        params.format = PixelFormat.TRANSLUCENT;
        params.type = WindowManager.LayoutParams.TYPE_PHONE; // 在响铃的时候显示Toast,和电话类型一致
        params.gravity = Gravity.LEFT + Gravity.TOP; // 指定位置到左上角
        // 自定义了Toast的布局,需要将xml转换成view,将Toast挂到windowManager窗体上
        mViewToast = View.inflate(this, R.layout.toast_view, null);
        mWindowsManager.addView(mViewToast,params); // 在窗体上挂载View
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
    }
}

由于需要在窗体上挂载View,需要在清单文件中声明对应权限,代码如下:

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

在res/layout下添加一个名为toast_view.xml的布局文件,作为自定义Toast的布局,代码如下:


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_toast"
        android:background="@drawable/call_locate_white"
        android:text="电话来了"
        android:gravity="center"
        android:drawableLeft="@android:drawable/ic_menu_call"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

LinearLayout>

为了显示来电归属地的信息,进一步修改AddressService中的showToast()方法,在其中调用一个新建的query(),作为查询来电号码归属地的方法。由于涉及到线程操作,所以需要创建Handler对象,整体代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AddressDao;

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    // Layout对象
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    // 自定义的Toast布局
    private View mViewToast;

    // 获取窗体对象
    private WindowManager mWindowsManager;

    // 归属地信息
    private String mAddress;

    // 文本控件
    private TextView tv_toast;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            tv_toast.setText(mAddress);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);

        // 5.获取窗体对象
        mWindowsManager = (WindowManager) getSystemService(WINDOW_SERVICE);
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动,挂断电话时需要移除Toast
                    if (mWindowsManager != null && mViewToast != null){
                        mWindowsManager.removeView(mViewToast);
                    }
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    showToast(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 打印Toast
     */
    private void showToast(String phoneNumber) {
        // 自定义Toast
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
        params.format = PixelFormat.TRANSLUCENT;
        params.type = WindowManager.LayoutParams.TYPE_PHONE; // 在响铃的时候显示Toast,和电话类型一致
        params.gravity = Gravity.LEFT + Gravity.TOP; // 指定位置到左上角
        // 自定义了Toast的布局,需要将xml转换成view,将Toast挂到windowManager窗体上
        mViewToast = View.inflate(this, R.layout.toast_view, null);
        tv_toast = mViewToast.findViewById(R.id.tv_toast);
        mWindowsManager.addView(mViewToast,params); // 在窗体上挂载View

        // 获取了来电号码以后,需要做来电号码查询
        query(phoneNumber);
    }

    private void query(final String phoneNumber){
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phoneNumber);
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
    }
}

6.高级工具——选择Toast样式 & 自定义组合控件

我们之前完成了设置中心里“电话归属地显示设置”的功能,现在需要实现第三个功能——即设置归属地显示风格,如图中的红框所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第5张图片

点击红框标明的条目后,会弹出一个Dialog供选择样式,如图所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第6张图片
这一节就来实现这个功能。

由于该条目的右侧为箭头而非方框,并且文字显示也略有区别,所以这里需要再次自定义组合控件。

在View包下新建SettingClickView,作为新条目的组合控件,并在res/layout下新建setting_click_view.xml,作为新条目的布局,代码分别如下:

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 SettingClickView extends RelativeLayout {

    /**
     * 标题描述控件
     */
    TextView tv_title;

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

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

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

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

    /**
     * 设置标题内容
     * @param title 标题内容
     */
    public void setTitle(String title){
        tv_title.setText(title);
    }

    /**
     * 设置文本内容
     * @param des 文本内容
     */
    public void setDes(String des){
        tv_des.setText(des);
    }
}

<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"/>

        <ImageView
            android:id="@+id/iv_image"
            android:background="@drawable/jiantou1_pressed"
            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>

接下来,在activity_setting.xml中使用刚刚创建好的自定义组合控件,代码如下:


<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">

    <TextView
        style="@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="自动更新已开启"/>

    
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="电话归属地的显示设置"
        mobilesafe:desoff="归属地的显示已关闭"
        mobilesafe:deson="归属地的显示已开启"/>

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

LinearLayout>

然后,修改SharedPreferencesUtil,添加getInt()/setInt(),作为从sp中获取整数型数值的方法,代码如下:

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);
    }

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

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

    /**
     * 7.移除节点
     * @param ctx 上下文
     * @param key 键
     */
    public static void remove(Context ctx, String key) {
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        sp.edit().remove(key).commit();
    }
}

为了方便管理sp,修改ConstantValue,添加名为TOAST_STYLE的静态变量,作为记录归属地信息显示样式的标识符,代码如下:

package com.example.mobilesafe.constant;

public class ConstantValue {

    /**
     * 设置中心——记录更新的状态
     */
    public static final String OPEN_UPDATE = "open_update";

    /**
     * 设置中心——记录归属地信息显示的样式
     */
    public static final String TOAST_STYLE = "toast_style";

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

    /**
     * 手机防盗——四个界面是否设置完成的状态
     */
    public static final String SETUP_OVER = "setup_over";

    /**
     * 手机防盗——SIM卡绑定序列号
     */
    public static final String SIM_NUMBER = "sim_number";

    /**
     * 手机防盗——联系人电话号码
     */
    public static final String CONTACT_PHONE = "contact_phone";

    /**
     * 手机防盗——是否开启防盗保护总开关
     */
    public static final String OPEN_SECURITY = "open_security";
}

最后,修改SettingActivity,添加initToastStyle(),作为初始化条目的方法,代码如下:

package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

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

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.service.AddressService;
import com.example.mobilesafe.utils.ServiceUtil;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingClickView;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

    // 描述文字所在的字符串数组
    private String[] mToastStyleDes;

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

        // 初始化更新
        initUpdate();

        // 初始化显示电话号码归属地
        initAddress();

        // 初始化电话号码归属地的显示样式
        initToastStyle();
    }

    /**
     * 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);
            }
        });
    }


    /**
     * 2.初始化“显示电话号码归属地”的方法
     */
    private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 通过ServiceUtil来判断服务是否开启
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.AddressService");
        siv_address.setCheck(isRunning);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }

    /**
     * 3.初始化“显示号码归属地显示样式”的方法
     */
    private void initToastStyle(){
        final SettingClickView scv_toast_style = findViewById(R.id.scv_toast_style);
        scv_toast_style.setTitle("电话归属地样式选择");
        // 1.创建描述文字所在的String类型数组
        mToastStyleDes = new String[]{"透明", "橙色", "蓝色", "灰色", "绿色"};
        // 2.通过Sp获取Toast显示样式的索引值(int),用于描述文字
        int toast_style = SharedPreferencesUtil.getInt(this, ConstantValue.TOAST_STYLE, 0);
        // 3.通过索引值获取字符串数组中的文字,显示给描述内容的控件上
        scv_toast_style.setDes(mToastStyleDes[toast_style]);
    }
}

7.高级工具——Toast样式单选框设置

上一节中我们初始化了自定义组合控件,这一节中我们将完成点击组合控件后弹出的单选框的设置。

修改SettingActivity,修改initToastStyle()方法,完善点击事件监听逻辑,并在其中添加showToastStyleDialog()方法,作为选择Toast样式的Dialog对话框,代码如下:

package com.example.mobilesafe.activity;

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

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.service.AddressService;
import com.example.mobilesafe.utils.ServiceUtil;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingClickView;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

    // 描述文字所在的字符串数组
    private String[] mToastStyleDes;

    // 条目的索引值
    private int mToaststyle;

    // 自定义组合控件SettingClickView
    private SettingClickView scv_toast_style;

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

        // 初始化更新
        initUpdate();

        // 初始化显示电话号码归属地
        initAddress();

        // 初始化电话号码归属地的显示样式
        initToastStyle();
    }

    /**
     * 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);
            }
        });
    }


    /**
     * 2.初始化“显示电话号码归属地”的方法
     */
    private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 通过ServiceUtil来判断服务是否开启
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.AddressService");
        siv_address.setCheck(isRunning);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }

    /**
     * 3.初始化“显示号码归属地显示样式”的方法
     */
    private void initToastStyle(){
        scv_toast_style = findViewById(R.id.scv_toast_style);
        scv_toast_style.setTitle("电话归属地样式选择");
        // 1.创建描述文字所在的String类型数组
        mToastStyleDes = new String[]{"透明", "橙色", "蓝色", "灰色", "绿色"};
        // 2.通过Sp获取Toast显示样式的索引值(int),用于描述文字
        mToaststyle = SharedPreferencesUtil.getInt(this, ConstantValue.TOAST_STYLE, 0);
        // 3.通过索引值获取字符串数组中的文字,显示给描述内容的控件上
        scv_toast_style.setDes(mToastStyleDes[mToaststyle]);
        // 4.监听点击事件,弹出对话框
        scv_toast_style.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 5.选择Toast样式的对话框
                showToastStyleDialog();
            }
        });
    }

    /**
     * 创建选中显示样式的对话框
     */
    private void showToastStyleDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置图标
        builder.setTitle("请选择归属地显示样式"); // 设置标题
        builder.setSingleChoiceItems(mToastStyleDes, mToaststyle, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 1.记录选中的索引值
                SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.TOAST_STYLE,which);
                // 2.关闭对话框
                dialog.dismiss();
                // 3.显示选中色值文字
                scv_toast_style.setDes(mToastStyleDes[which]);
            }
        }); // 单个选择条目对应的事件监听(String类型的数组,选中条目索引值,监听器)
        // “取消”按钮的点击事件监听
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        builder.show(); // 展示对话框
    }
}

8.高级工具——Toast显示样式修改

上一节中我们将设置显示样式的对话框编写完成了,这一节就来实现点击对话框中的设置选项后,对归属地信息的显示样式进行相应的修改。

修改AddressService,主要修改showToast()方法,从sp中获取相应索引值,然后根据索引值从已存储好资源图像id的数组中取出相应资源文件,再给文本控件设置背景资源即可,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;

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

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    // Layout对象
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    // 自定义的Toast布局
    private View mViewToast;

    // 获取窗体对象
    private WindowManager mWindowsManager;

    // 归属地信息
    private String mAddress;

    // 归属地信息显示控件
    private TextView tv_toast;

    // 存储资源图片id的数组
    private int[] mDrawableIds;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            tv_toast.setText(mAddress);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);

        // 5.获取窗体对象
        mWindowsManager = (WindowManager) getSystemService(WINDOW_SERVICE);
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动,挂断电话时需要移除Toast
                    if (mWindowsManager != null && mViewToast != null){
                        mWindowsManager.removeView(mViewToast);
                    }
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    showToast(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 打印Toast
     */
    private void showToast(String phoneNumber) {
        // 自定义Toast
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
        params.format = PixelFormat.TRANSLUCENT;
        params.type = WindowManager.LayoutParams.TYPE_PHONE; // 在响铃的时候显示Toast,和电话类型一致
        params.gravity = Gravity.LEFT + Gravity.TOP; // 指定位置到左上角
        // 自定义了Toast的布局,需要将xml转换成view,将Toast挂到windowManager窗体上
        mViewToast = View.inflate(this, R.layout.toast_view, null);
        tv_toast = mViewToast.findViewById(R.id.tv_toast);

        // 从sp中后去色值文字的索引,匹配图片,用作展示
        mDrawableIds = new int[]{R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal};
        int toastStyle = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.TOAST_STYLE, 0);
        tv_toast.setBackgroundResource(mDrawableIds[toastStyle]);

        mWindowsManager.addView(mViewToast,params); // 在窗体上挂载View

        // 获取了来电号码以后,需要做来电号码查询
        query(phoneNumber);
    }

    private void query(final String phoneNumber){
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phoneNumber);
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
    }
}

9.高级工具——Toast提示框位置

上面的小节中,我们完成了归属地信息的显示以及显示框的样式调整,接下来需要完成显示框的位置设置,如图中红框所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第7张图片

点击该条目后,会进入一个透明的Activity。在该Activity中可以根据手势移动来调整归属地的显示位置,如图所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第8张图片

该需求的整体特性如下:

  1. 在设置界面中添加一个可点击条目(自定义控件),点击此条目就会弹出Activity(半透明);
  2. “双击居中”的View,和描述文字在不同的竖直(上下)区域;
  3. 限制View的可拖动范围;
  4. View在双击时需要居中。

修改SettingActivity,新增initLocation(),作为初始化自定义组合控件的方法,代码如下:

package com.example.mobilesafe.activity;

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

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.service.AddressService;
import com.example.mobilesafe.utils.ServiceUtil;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingClickView;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

    // 描述文字所在的字符串数组
    private String[] mToastStyleDes;

    // 条目的索引值
    private int mToaststyle;

    // 自定义组合控件SettingClickView
    private SettingClickView scv_toast_style;

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

        // 初始化更新
        initUpdate();

        // 初始化显示电话号码归属地
        initAddress();

        // 初始化电话号码归属地的显示样式
        initToastStyle();

        // 初始化电话号码归属地的显示位置
        initLocation();
    }

    /**
     * 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);
            }
        });
    }


    /**
     * 2.初始化“显示电话号码归属地”的方法
     */
    private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 通过ServiceUtil来判断服务是否开启
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.AddressService");
        siv_address.setCheck(isRunning);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }

    /**
     * 3.初始化“显示号码归属地显示样式”的方法
     */
    private void initToastStyle(){
        scv_toast_style = findViewById(R.id.scv_toast_style);
        scv_toast_style.setTitle("电话归属地样式选择");
        // 1.创建描述文字所在的String类型数组
        mToastStyleDes = new String[]{"透明", "橙色", "蓝色", "灰色", "绿色"};
        // 2.通过Sp获取Toast显示样式的索引值(int),用于描述文字
        mToaststyle = SharedPreferencesUtil.getInt(this, ConstantValue.TOAST_STYLE, 0);
        // 3.通过索引值获取字符串数组中的文字,显示给描述内容的控件上
        scv_toast_style.setDes(mToastStyleDes[mToaststyle]);
        // 4.监听点击事件,弹出对话框
        scv_toast_style.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 5.选择Toast样式的对话框
                showToastStyleDialog();
            }
        });
    }

    /**
     * 4.初始化“显示号码归属地显示位置”的方法
     */
    private void initLocation(){
        SettingClickView scv_location = findViewById(R.id.scv_location);
        scv_location.setTitle("归属地提示框的位置");
        scv_location.setDes("设置归属地提示框的位置");
        scv_location.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(getApplicationContext(),ToastLocationActivity.class));
            }
        });
    }

    /**
     * 创建选中显示样式的对话框
     */
    private void showToastStyleDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置图标
        builder.setTitle("请选择归属地显示样式"); // 设置标题
        builder.setSingleChoiceItems(mToastStyleDes, mToaststyle, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 1.记录选中的索引值
                SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.TOAST_STYLE,which);
                // 2.关闭对话框
                dialog.dismiss();
                // 3.显示选中色值文字
                scv_toast_style.setDes(mToastStyleDes[which]);
            }
        }); // 单个选择条目对应的事件监听(String类型的数组,选中条目索引值,监听器)
        // “取消”按钮的点击事件监听
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        builder.show(); // 展示对话框
    }
}

修改activity_setting.xml,在布局中添加对应的自定义控件,代码如下:


<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">

    <TextView
        style="@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="自动更新已开启"/>

    
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="电话归属地的显示设置"
        mobilesafe:desoff="归属地的显示已关闭"
        mobilesafe:deson="归属地的显示已开启"/>

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

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

LinearLayout>

在activity下新增ToastLocationActivity,作为进入设置归属地信息显示框的位置的Activity。先修改ToastLocationActivity的布局activity_toast_location.xml,注意修改根布局的background,用于给透明样式的Activity填色,代码如下:


<RelativeLayout 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:background="#accc"
    tools:context=".activity.ToastLocationActivity">
    
    <ImageView
        android:id="@+id/iv_drag"
        android:background="@drawable/drag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/btn_top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按中提示框拖拽到任意位置"
        android:gravity="center"
        android:visibility="invisible"
        android:background="@drawable/function_greenbutton_pressed"/>

    <Button
        android:id="@+id/btn_bottom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按中提示框拖拽到任意位置"
        android:gravity="center"
        android:visibility="visible"
        android:layout_alignParentBottom="true"
        android:background="@drawable/function_greenbutton_pressed"/>

RelativeLayout>

修改ToastLocationActivity在清单文件中的样式,选择透明样式,代码如下:

<activity android:name=".activity.ToastLocationActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar"/>

10.高级工具——Toast提示框拖拽过程中坐标的移动计算规则

上一节中,我们完成了点击“拖拽提示框”条目时,出现的透明布局,效果如图所示:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第9张图片

现在想要实现拖动“双击居中”图片的功能,需要计算拖拽过程中的坐标移动规则,这里专门列出一个小节来进行分析。

移动的过程可以参考以下示意图:

Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第10张图片
其中,获取坐标的方法有两个,例如getX()getRawX(),它们的区别如下图所示:
Android开发实战《手机安全卫士》——6.“高级工具”模块拓展 & 自定义Toast_第11张图片
根据上面贴出的原理图,修改ToastLocationActivity,添加initUI(),作为初始化UI以及给图片设置触摸监听器的方法,代码如下:

package com.example.mobilesafe.activity;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import com.example.mobilesafe.R;


public class ToastLocationActivity extends Activity {

    private ImageView iv_drag;

    private Button btn_top;

    private Button btn_bottom;

    private int startX;

    private int startY;

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

        // 初始化UI
        initUI();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        iv_drag = findViewById(R.id.iv_drag);
        btn_top = findViewById(R.id.btn_top);
        btn_bottom = findViewById(R.id.btn_bottom);

        // 监听某一个控件的拖拽过程(按下(1)、移动(多次)、抬起(1))
        iv_drag.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int moveX = (int) event.getRawX();
                        int moveY = (int) event.getRawY();
                        int disX = moveX - startX;
                        int disY = moveY - startY;

                        // 1.获取当前控件所在屏幕的(左,上)角的位置
                        int left = iv_drag.getLeft() + disX; // 左侧坐标
                        int top = iv_drag.getTop() + disY; // 上侧坐标
                        int right = iv_drag.getRight() + disX; // 右侧坐标
                        int bottom = iv_drag.getBottom() + disY; // 下侧坐标

                        // 2.告知移动的控件,根据计算出来的坐标去做展示
                        iv_drag.layout(left,top,right,bottom);
                        break;
                    case MotionEvent.ACTION_UP:
                        break;
                }
                return false;
            }
        });
    }

}

11.高级工具——Toast提示框移动过程中的容错处理

上一节中我们完善了Toast提示框在移动过程中的坐标变化,这一节中我们来处理一下移动过程中可能会出现的一些错误(将提示框移出屏幕)。

首先修改ConstantValue,添加两个静态变量LOCATION_XLOCATION_Y,用于记录每次移动完提示框后的左上角的坐标点,代码如下:

package com.example.mobilesafe.constant;

public class ConstantValue {

    /**
     * 设置中心——记录更新的状态
     */
    public static final String OPEN_UPDATE = "open_update";

    /**
     * 设置中心——记录归属地信息显示的样式
     */
    public static final String TOAST_STYLE = "toast_style";

    /**
     * 设置中心——记录归属地信息位置(左上角)的x坐标
     */
    public static final String LOCATION_X = "location_x";

    /**
     * 设置中心——记录归属地信息位置(左上角)的y坐标
     */
    public static final String LOCATION_Y = "location_y";

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

    /**
     * 手机防盗——四个界面是否设置完成的状态
     */
    public static final String SETUP_OVER = "setup_over";

    /**
     * 手机防盗——SIM卡绑定序列号
     */
    public static final String SIM_NUMBER = "sim_number";

    /**
     * 手机防盗——联系人电话号码
     */
    public static final String CONTACT_PHONE = "contact_phone";

    /**
     * 手机防盗——是否开启防盗保护总开关
     */
    public static final String OPEN_SECURITY = "open_security";

}

修改ToastLocationActivity,修改initUI(),完善移动时的坐标变化逻辑,代码如下:

package com.example.mobilesafe.activity;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;

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


public class ToastLocationActivity extends Activity {

    private ImageView iv_drag;

    private Button btn_top;

    private Button btn_bottom;

    private int startX;

    private int startY;

    private WindowManager mWindowManager;

    private int mScreenHeight;

    private int mScreenWidth;

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

        // 初始化UI
        initUI();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        iv_drag = findViewById(R.id.iv_drag);
        btn_top = findViewById(R.id.btn_top);
        btn_bottom = findViewById(R.id.btn_bottom);

        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
        mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();

        // 从sp中读取的坐标值
        int locationX = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_X, 0);
        int locationY = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_Y, 0);

        // 左上角的坐标作用在iv_drag上,由于该控件在相对布局中,所以其所在位置的规则需要由相对布局提供
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); // 指定宽高
        layoutParams.leftMargin = locationX; // 配置左上角的X坐标
        layoutParams.topMargin = locationY; // 配置左上角的Y坐标

        // 将以上规则作用在iv_drag上
        iv_drag.setLayoutParams(layoutParams);


        // 监听某一个控件的拖拽过程(按下(1)、移动(多次)、抬起(1))
        iv_drag.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int moveX = (int) event.getRawX();
                        int moveY = (int) event.getRawY();
                        int disX = moveX - startX;
                        int disY = moveY - startY;

                        // 1.获取当前控件所在屏幕的(左,上)角的位置
                        int left = iv_drag.getLeft() + disX; // 左侧坐标
                        int top = iv_drag.getTop() + disY; // 上侧坐标
                        int right = iv_drag.getRight() + disX; // 右侧坐标
                        int bottom = iv_drag.getBottom() + disY; // 下侧坐标

                        // 容错处理(iv_drag不能拖拽出手机屏幕)
                        if (left < 0){ // 左边缘不能超出屏幕
                            return true;
                        }
                        if (right > mScreenWidth){ // 右边缘不能超出屏幕
                            return true;
                        }
                        if (top < 0){ // 上边缘不能超出屏幕
                            return true;
                        }
                        if (bottom > mScreenHeight - 22){ // 下边缘(屏幕的高度 - 22 = 底边缘的显示最大值)不能超出屏幕
                            return true;
                        }

                        // 2.告知移动的控件,根据计算出来的坐标去做展示
                        iv_drag.layout(left,top,right,bottom);

                        // 3.重置一次起始坐标
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        // 4.记录上次移动后的坐标位置
                        SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_X,iv_drag.getLeft());
                        SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_Y,iv_drag.getTop());
                        break;
                }
                // 在当前的情况下返回false表示不响应事件,返回true才表示响应事件
                return true;
            }
        });
    }
}

12.高级工具——Toast提示框移动时提示信息进行相应移动

上面的小节中我们完成了提示框的拖曳,现在需要完成当提示框拖动到屏幕下方时提示信息从下面移动到上面,反之亦然的效果。

修改ToastLocationActivity,修改initUI(),完善提示框移动时提示信息的移动逻辑,代码如下:

package com.example.mobilesafe.activity;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;

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


public class ToastLocationActivity extends Activity {

    private ImageView iv_drag;

    private Button btn_top;

    private Button btn_bottom;

    private int startX;

    private int startY;

    private WindowManager mWindowManager;

    private int mScreenHeight;

    private int mScreenWidth;

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

        // 初始化UI
        initUI();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        iv_drag = findViewById(R.id.iv_drag);
        btn_top = findViewById(R.id.btn_top);
        btn_bottom = findViewById(R.id.btn_bottom);

        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
        mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();

        // 从sp中读取的坐标值
        int locationX = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_X, 0);
        int locationY = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_Y, 0);

        // 左上角的坐标作用在iv_drag上,由于该控件在相对布局中,所以其所在位置的规则需要由相对布局提供
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); // 指定宽高
        layoutParams.leftMargin = locationX; // 配置左上角的X坐标
        layoutParams.topMargin = locationY; // 配置左上角的Y坐标

        // 将以上规则作用在iv_drag上
        iv_drag.setLayoutParams(layoutParams);

        if (locationY > mScreenHeight / 2){ // 移动到屏幕的下半部分
            btn_bottom.setVisibility(View.INVISIBLE);
            btn_top.setVisibility(View.VISIBLE);
        }else { // 移动到屏幕的上半部分
            btn_bottom.setVisibility(View.VISIBLE);
            btn_top.setVisibility(View.INVISIBLE);
        }

        // 监听某一个控件的拖拽过程(按下(1)、移动(多次)、抬起(1))
        iv_drag.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int moveX = (int) event.getRawX();
                        int moveY = (int) event.getRawY();
                        int disX = moveX - startX;
                        int disY = moveY - startY;

                        // 1.获取当前控件所在屏幕的(左,上)角的位置
                        int left = iv_drag.getLeft() + disX; // 左侧坐标
                        int top = iv_drag.getTop() + disY; // 上侧坐标
                        int right = iv_drag.getRight() + disX; // 右侧坐标
                        int bottom = iv_drag.getBottom() + disY; // 下侧坐标

                        // 容错处理(iv_drag不能拖拽出手机屏幕)
                        if (left < 0){ // 左边缘不能超出屏幕
                            return true;
                        }
                        if (right > mScreenWidth){ // 右边缘不能超出屏幕
                            return true;
                        }
                        if (top < 0){ // 上边缘不能超出屏幕
                            return true;
                        }
                        if (bottom > mScreenHeight - 22){ // 下边缘(屏幕的高度 - 22 = 底边缘的显示最大值)不能超出屏幕
                            return true;
                        }

                        if (top > mScreenHeight / 2){ // 移动到屏幕的下半部分
                            btn_bottom.setVisibility(View.INVISIBLE);
                            btn_top.setVisibility(View.VISIBLE);
                        }else { // 移动到屏幕的上半部分
                            btn_bottom.setVisibility(View.VISIBLE);
                            btn_top.setVisibility(View.INVISIBLE);
                        }

                        // 2.告知移动的控件,根据计算出来的坐标去做展示
                        iv_drag.layout(left,top,right,bottom);

                        // 3.重置一次起始坐标
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        // 4.记录上次移动后的坐标位置
                        SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_X,iv_drag.getLeft());
                        SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_Y,iv_drag.getTop());
                        break;
                }
                // 在当前的情况下返回false表示不响应事件,返回true才表示响应时间
                return true;
            }
        });
    }
}

你可能感兴趣的:(Android)