说明:本人写博客一来是为了方便日后查看项目,二来是希望能够和广大的程序猿相互交流学习,文章布局简单,如有嫌弃,请绕行,如有错误,请指出,谢谢。
实验环境:安卓6.0 魅族手机
1、跟随手指的滑动而滑动(也可以用鼠标滑动)
2、在手指弹起的时候,悬浮窗会自动停靠在左右两侧
3、点击悬浮窗按钮可以返回到桌面
6.0权限问题:Google在6.0时加入权限管理机制,6.0之后,android需要动态获取权限,要使用权限,不仅要在manifest文件中定义,还要在代码中动态获取。点我了解权限问题
manifest中添加权限声明
MainActivity中代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Settings.canDrawOverlays(MainActivity.this)) {
Intent intent = new Intent(MainActivity.this, FloatViewService.class);
Toast.makeText(MainActivity.this, "已开启悬浮窗", Toast.LENGTH_SHORT).show();
startService(intent);
finish();
} else {
//若没有权限,提示获取.
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
Toast.makeText(MainActivity.this, "需要取得权限以使用悬浮窗", Toast.LENGTH_SHORT).show();
startActivity(intent);
finish();
}
}
}
代码说明:如果手机已授予该app使用悬浮窗的功能,界面会自动开启悬浮窗,MainActivity被finish,否则直接跳转到本手机开启悬浮窗权限的界面,亲测有效,比如魅族手机开启权限的界面如下图所示:
问题:只有在第一次安装app的时候才会跳转到打开权限的界面,之后打开app则不会跳转,这部分不太理解,有知悉的评论区见。
Android的窗口是基于WindowManager实现的,它面向的对象一端是屏幕,另一端就是View,比如我们之前使用的setContentView(R.layout.activity_main), 就是将view显示在屏幕上,代码的底层都是经过WindowManager实现的,整个系统只有一个WindowManager。点我了解界面绘制详解
当app没有被关闭时,悬浮窗同样可以运行,这时候就需要Service来实现后台运行。这里可自行百度Service具体实现的过程,本篇不做解释。
说明:需要监听手势,所以设置了setOnTouchListener,识别按下、移动、弹起三个动作,移动的过程需要动态获取触摸的坐标,所以首先要在按下的过程中获取按下的坐标,rawX = event.getRawX(); rawY = event.getRawY(),在移动的过程中进行刷新, wmParams.x = wmParams.x - distanceX;wmParams.y = wmParams.y - distanceY。
// 设置监听浮动窗口的触摸移动
go_mainhome.setOnTouchListener(new View.OnTouchListener() {
private float rawX;
private float rawY;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
mFloatLayout.setAlpha(1.0f);//设置其透明度
myCountDownTimer.cancel();//取消计时
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
// getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
int distanceX = (int) (event.getRawX() - rawX);
int distanceY = (int) (event.getRawY() - rawY);
//mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
wmParams.x = wmParams.x - distanceX;
wmParams.y = wmParams.y - distanceY;
// 刷新
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
myCountDownTimer.start();//重新开始计时
if (wmParams.x < screenWidth / 2) {
//在屏幕右侧
wmParams.x = 0;
wmParams.y = wmParams.y - 0;
} else {
//在屏幕左侧
wmParams.x = screenWidth;
wmParams.y = wmParams.y - 0;
}
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
break;
}
return false;//此处必须返回false,否则OnClickListener获取不到监听
}
});
获取屏幕大小
尝试了好几种获取屏幕大小的代码,此方法亲测有效。
Display display = mWindowManager.getDefaultDisplay();
Point point = new Point();
display.getRealSize(point);
screenWidth = point.x;
screenHeight = point.y;
Log.i("qqq", "screenWidth------: " + screenWidth + "\n" + "screenHeight---" + screenHeight);
说明:当手指滑动到屏幕中央右侧时,比如在图中的A点(x,y),最终悬浮窗将会停靠在图中的B点,A点向右平移到B点,纵坐标不变,横坐标为0,在屏幕左侧同理,可详见代码case MotionEvent.ACTION_UP部分,前提是需要设置 wmParams.gravity = Gravity.RIGHT | Gravity.BOTTOM,可以滑动最下面看详细代码。
其实就是对按钮设置了监听setOnClickListener,点击之后跳转到桌面的main。
package com.lightingcontour.toucher;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toast;
public class FloatViewService extends Service {
private static final String TAG = "FloatViewService";
// 定义浮动窗口布局
private LinearLayout mFloatLayout;
private WindowManager.LayoutParams wmParams;
// 创建浮动窗口设置布局参数的对象
private WindowManager mWindowManager;
private ImageButton go_mainhome;
private ImageButton go_back;
// private LinearLayout toucher_layout;
private int screenHeight;
private int screenWidth;
private MyCountDownTimer myCountDownTimer;
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate");
createFloatView();
myCountDownTimer = new MyCountDownTimer(2500, 1000); //设置计时2.5s
myCountDownTimer.start();
}
@SuppressWarnings("static-access")
@SuppressLint("InflateParams")
private void createFloatView() {
wmParams = new WindowManager.LayoutParams();
// 通过getApplication获取的是WindowManagerImpl.CompatModeWrapper
mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);
Display display = mWindowManager.getDefaultDisplay();
Point point = new Point();
display.getRealSize(point);
screenWidth = point.x;
screenHeight = point.y;
Log.i("qqq", "screenWidth------: " + screenWidth + "\n" + "screenHeight---" + screenHeight);
// 设置window type
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
// 设置图片格式,效果为背景透明
wmParams.format = PixelFormat.RGBA_8888;
// 设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
// 调整悬浮窗显示的停靠位置为右侧底部
wmParams.gravity = Gravity.RIGHT | Gravity.BOTTOM;
// 以屏幕左上角为原点,设置x、y初始值,相对于gravity
wmParams.x = 0;
wmParams.y = 0;
// 设置悬浮窗口长宽数据
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
LayoutInflater inflater = LayoutInflater.from(getApplication());
// 获取浮动窗口视图所在布局
mFloatLayout = (LinearLayout) inflater.inflate(R.layout.toucherlayout, null);
// 添加mFloatLayout
mWindowManager.addView(mFloatLayout, wmParams);
// 浮动窗口按钮
go_mainhome = (ImageButton) mFloatLayout.findViewById(R.id.go_mainhome);
go_back = (ImageButton) mFloatLayout.findViewById(R.id.go_back);
//UNSPECIFIED是未指定模式
mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
// 设置监听浮动窗口的触摸移动
go_mainhome.setOnTouchListener(new View.OnTouchListener() {
private float rawX;
private float rawY;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
mFloatLayout.setAlpha(1.0f);//设置其透明度
myCountDownTimer.cancel();//取消计时
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
// getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
int distanceX = (int) (event.getRawX() - rawX);
int distanceY = (int) (event.getRawY() - rawY);
//mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
wmParams.x = wmParams.x - distanceX;
wmParams.y = wmParams.y - distanceY;
// 刷新
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
myCountDownTimer.start();//重新开始计时
if (wmParams.x < screenWidth / 2) {
//在屏幕右侧
wmParams.x = 0;
wmParams.y = wmParams.y - 0;
} else {
wmParams.x = screenWidth;
wmParams.y = wmParams.y - 0;
}
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
break;
}
return false;//此处必须返回false,否则OnClickListener获取不到监听
}
});
// 设置监听浮动窗口的触摸移动
go_back.setOnTouchListener(new View.OnTouchListener() {
private float rawX;
private float rawY;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
mFloatLayout.setAlpha(1.0f);//设置其透明度
myCountDownTimer.cancel();//取消计时
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
// getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
int distanceX = (int) (event.getRawX() - rawX);
int distanceY = (int) (event.getRawY() - rawY);
//mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
wmParams.x = wmParams.x - distanceX;
wmParams.y = wmParams.y - distanceY;
// 刷新
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
rawX = event.getRawX();
rawY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
myCountDownTimer.start();//重新开始计时
if (wmParams.x < screenWidth / 2) {
//在屏幕右侧
wmParams.x = 0;
wmParams.y = wmParams.y - 0;
} else {
wmParams.x = screenWidth;
wmParams.y = wmParams.y - 0;
}
mWindowManager.updateViewLayout(mFloatLayout, wmParams);
break;
}
return false;//此处必须返回false,否则OnClickListener获取不到监听
}
});
go_mainhome.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FloatViewService.this, "返回到桌面",
Toast.LENGTH_SHORT).show();
Intent intent = new Intent();
// 为Intent设置Action、Category属性
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);// "android.intent.action.MAIN"
intent.addCategory(Intent.CATEGORY_HOME); //"android.intent.category.HOME"CATEGORY_HOME 目标Activity是HOME Activity,即手机开机启动后显示的Activity,或按下HOME键后显示的Activity
startActivity(intent);
}
});
go_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FloatViewService.this, "返回",
Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
if (mFloatLayout != null) {
// 移除悬浮窗口
mWindowManager.removeView(mFloatLayout);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public class MyCountDownTimer extends CountDownTimer {
public MyCountDownTimer(long millisInFuture, long countDownInterval) {
super(millisInFuture, countDownInterval);
}
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
mFloatLayout.setAlpha(0.4f);
}
}
}