上面的小节中我们完成了包括提示框的拖曳在内的基本功能,现在需要完善提示框的双击事件,使提示框在双击时处于居中位置。
修改ToastLocationActivity,修改initUI(),完善提示框双击事件的业务逻辑。注意,为了让控件的setOnClickListener
和setOnTouchListener
两个监听器同时发挥效果,需要修改setOnTouchListener
的返回值结果,即最终返回值需要为false
,代码如下:
package com.example.mobilesafe.activity;
import android.app.Activity;
import android.os.Bundle;
import android.os.SystemClock;
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;
// 存储点击事件的数组,数组容量就是点击次数
private long[] mHits = new long[2];
@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);
}
// 监听某一个控件的双击事件
iv_drag.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.arraycopy(mHits,1,mHits,0,mHits.length - 1);
mHits[mHits.length - 1] = SystemClock.uptimeMillis();
if (mHits[mHits.length - 1] - mHits[0] < 500){ // 两次点击间隔0.5秒
// 满足双击事件后的调用
int left = mScreenWidth / 2 - iv_drag.getWidth() / 2;
int top = mScreenHeight / 2 - iv_drag.getHeight() / 2;
int right = mScreenWidth / 2 + iv_drag.getWidth() / 2;
int bottom = mScreenHeight / 2 + iv_drag.getHeight() / 2;
// 控件按以上提供的四个坐标来显示
iv_drag.layout(left,top,right,bottom);
// 存储最终位置
SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_X,iv_drag.getLeft());
SharedPreferencesUtil.putInt(getApplicationContext(), ConstantValue.LOCATION_Y,iv_drag.getTop());
}
}
});
// 监听某一个控件的拖拽过程(按下(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才表示响应事件
// 既要响应点击事件,又要响应拖拽过程,则此返回值结果需要修改为false
return false;
}
});
}
}
根据以上代码,可以做出简单总结:
onTouch()
方法返回了true,所以dispatchTouchEvent()
直接返回了true,随后响应事件;onTouch()
方法返回了false,而控件设置过点击事件,所以具有点击事件,在手势抬起时,就会进行响应,即onClick()
,然后dispatchTouchEvent()
返回的也是true。论证这段总结的dispatchTouchEvent()
伪代码如图所示:
上一节中我们处理好了Toast
提示框的位置处理信息,这一节需要将其与Service
联动起来,即读取sp中存储的左上角坐标值即可。
修改AddressService,修改showToast()方法,读取sp中存储的左上角坐标,并且将值赋给自定义(ViewToast)控件即可。除此之外,为其注册触摸监听器,使其可以随意拖动,代码如下:
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.MotionEvent;
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;
// ViewToast的X坐标
private int startX;
// ViewToast的Y坐标
private int startY;
// 窗体的宽度
private int mScreenWidth;
// 窗体的高度
private int mScreenHeight;
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);
mScreenHeight = mWindowsManager.getDefaultDisplay().getHeight();
mScreenWidth = mWindowsManager.getDefaultDisplay().getWidth();
}
// 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);
mViewToast.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;
// 赋值给自定义控件
params.x = params.x + disX;
params.y = params.y + disY;
// 容错处理
if (params.x < 0){
params.x = 0;
}
if (params.y < 0){
params.y = 0;
}
if (params.x > mScreenWidth - mViewToast.getWidth()){
params.x = mScreenWidth - mViewToast.getWidth();
}
if (params.y > mScreenHeight - mViewToast.getHeight() - 22){
params.y = mScreenHeight - mViewToast.getHeight() - 22;
}
// 根据手势移动,在窗体上去进行自定义控件位置的更新
mWindowsManager.updateViewLayout(mViewToast,params);
// 重置一次起始坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.LOCATION_X,params.x);
SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.LOCATION_Y,params.y);
break;
}
// 在当前的情况下返回false表示不响应事件,返回true才表示响应事件
// 既要响应点击事件,又要响应拖拽过程,则此返回值结果需要修改为false
return true;
}
});
// 读取sp中存储Toast左上角坐标值(x,y)
int localX = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_X, 0);
int localY = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_Y, 0);
// 将读取的坐标值赋给params(这里的坐标默认代表左上角)
params.x = localX;
params.y = localY;
// 从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);
}
}
}
之前完成了设置中心里三个条目的功能——开启来电归属地信息显示、设置显示样式、设置显示位置。接下来我们需要完成另一个常见的功能——清理内存。清理内存一般都由一个小火箭的动画来实现,实现这个动画需要注意以下几点:
Activity
之外,和Activity
的开启/关闭没有关系,说明需要写在Service
中;由于该功能并非主要功能,是可以选择性添加的功能,所以这里就单独将其拿出来进行讲解。
在res/drawable下新建一个名为rocket_bg.xml的资源,表示这是小火箭在喷射时的动画,代码如下:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:drawable="@drawable/desktop_rocket_launch_1"
android:duration="200"/>
<item android:drawable="@drawable/desktop_rocket_launch_2"
android:duration="200"/>
animation-list>
随后,在res/layout下新增名为rocket_view.xml的布局文件,代表小火箭的布局,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<ImageView
android:id="@+id/iv_rocket"
android:background="@drawable/rocket_bg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
LinearLayout>
接下来,创建RocketService,作为小火箭开启和关闭时的Service
,完善相应逻辑,注意在Service中开启Activity需要给intent设置一个参数,即FLAG_ACTIVITY_NEW_TASK
代码如下:
package com.example.rocketman;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.ImageView;
public class RocketService extends Service {
private WindowManager mWM;
private int mScreenHeight;
private int mScreenWidth;
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
private View mRocketView;
private Handler mHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
params.y = (Integer) msg.obj;
//告知窗体更新火箭view的所在位置
mWM.updateViewLayout(mRocketView, params);
};
};
private WindowManager.LayoutParams params; @Override
public void onCreate() {
//获取窗体对象
mWM = (WindowManager) getSystemService(WINDOW_SERVICE);
mScreenHeight = mWM.getDefaultDisplay().getHeight();
mScreenWidth = mWM.getDefaultDisplay().getWidth();
//开启火箭
showRocket();
super.onCreate();
}
private void showRocket() {
params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 默认能够被触摸
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
params.format = PixelFormat.TRANSLUCENT;
//在响铃的时候显示吐司,和电话类型一致
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.setTitle("Toast");
//指定吐司的所在位置(将吐司指定在左上角)
params.gravity = Gravity.LEFT+Gravity.TOP;
//定义吐司所在的布局,并且将其转换成view对象,添加至窗体(权限)
mRocketView = View.inflate(this, R.layout.rocket_view, null);
ImageView iv_rocket = (ImageView) mRocketView.findViewById(R.id.iv_rocket);
AnimationDrawable animationDrawable = (AnimationDrawable) iv_rocket.getBackground();
animationDrawable.start();
mWM.addView(mRocketView, params);
mRocketView.setOnTouchListener(new OnTouchListener() {
private int startX;
private int startY;
@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;
params.x = params.x+disX;
params.y = params.y+disY;
//容错处理
if(params.x<0){
params.x = 0;
}
if(params.y<0){
params.y=0;
}
if(params.x>mScreenWidth-mRocketView.getWidth()){
params.x = mScreenWidth-mRocketView.getWidth();
}
if(params.y>mScreenHeight-mRocketView.getHeight()-22){
params.y = mScreenHeight-mRocketView.getHeight()-22;
}
//告知窗体吐司需要按照手势的移动,去做位置的更新
mWM.updateViewLayout(mRocketView, params);
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
if(params.x>100 && params.x<200 && params.y>350){
//发射火箭
sendRocket();
//开启产生尾气的activity
Intent intent = new Intent(getApplicationContext(), BackgroundActivity.class);
//开启火箭后,关闭了唯一的activity对应的任务栈,所以在此次需要告知新开启的activity开辟一个新的任务栈
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
break;
}
return true;
}
});
}
protected void sendRocket() {
//在向上的移动过程中,一直去减少y轴的大小,直到减少为0为止
//在主线程中不能去睡眠,可能会导致主线程阻塞
new Thread(){
public void run() {
for(int i=0;i<11;i++){
int height = 350-i*35;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message msg = Message.obtain();
msg.obj = height;
mHandler.sendMessage(msg);
}
};
}.start();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
if(mWM!=null && mRocketView!=null){
mWM.removeView(mRocketView);
}
super.onDestroy();
}
}
随后再新建一个名为BackgroundActivity的透明样式的Activity,作为小火箭产生喷射尾气的Activity。其布局文件和代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/iv_bottom"
android:layout_alignParentBottom="true"
android:background="@drawable/desktop_smoke_m"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv_top"
android:layout_above="@id/iv_bottom"
android:background="@drawable/desktop_smoke_t"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
RelativeLayout>
package com.example.rocketman;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
public class BackgroundActivity extends Activity {
private Handler mHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
finish();
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_background);
ImageView iv_top = (ImageView) findViewById(R.id.iv_top);
ImageView iv_bottom = (ImageView) findViewById(R.id.iv_bottom);
AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
alphaAnimation.setDuration(500);
iv_top.startAnimation(alphaAnimation);
iv_bottom.startAnimation(alphaAnimation);
mHandler.sendEmptyMessageDelayed(0, 1000);
}
}