现在很多安全类的软件,比如360手机助手,百度手机助手等等,都有一个悬浮窗,可以飘浮在桌面上,方便用户使用一些常用的操作。今天这篇文章,就是介绍如何实现桌面悬浮窗效果的。
首先,看一下效果图。
悬浮窗一共分为两个部分,一个是平常显示的小窗口,另外一个是点击小窗口显示出来的二级悬浮窗口。
首先,先看一下这个项目的目录结构。
最关键的就是红框内的四个类。
首先,FloatWindowService是一个后台的服务类,主要负责在后台不断的刷新桌面上的小悬浮窗口,否则会导致更换界面之后,悬浮窗口也会随之消失,因此需要不断的刷新。下面是实现代码。
- package com.qust.floatwindow;
- import java.util.Timer;
- import java.util.TimerTask;
- import android.app.Service;
- import android.content.Context;
- import android.content.Intent;
- import android.os.Handler;
- import android.os.IBinder;
- /**
- * 悬浮窗后台服务
- *
- * @author zhaokaiqiang
- *
- */
- public class FloatWindowService extends Service {
- public static final String LAYOUT_RES_ID = "layoutResId";
- public static final String ROOT_LAYOUT_ID = "rootLayoutId";
- // 用于在线程中创建/移除/更新悬浮窗
- private Handler handler = new Handler();
- private Context context;
- private Timer timer;
- // 小窗口布局资源id
- private int layoutResId;
- // 布局根布局id
- private int rootLayoutId;
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- context = this;
- layoutResId = intent.getIntExtra(LAYOUT_RES_ID, 0);
- rootLayoutId = intent.getIntExtra(ROOT_LAYOUT_ID, 0);
- if (layoutResId == 0 || rootLayoutId == 0) {
- throw new IllegalArgumentException(
- "layoutResId or rootLayoutId is illegal");
- }
- if (timer == null) {
- timer = new Timer();
- // 每500毫秒就执行一次刷新任务
- timer.scheduleAtFixedRate(new RefreshTask(), 0, 500);
- }
- return super.onStartCommand(intent, flags, startId);
- }
- @Override
- public void onDestroy() {
- super.onDestroy();
- // Service被终止的同时也停止定时器继续运行
- timer.cancel();
- timer = null;
- }
- private class RefreshTask extends TimerTask {
- @Override
- public void run() {
- // 当前界面没有悬浮窗显示,则创建悬浮
- if (!FloatWindowManager.getInstance(context).isWindowShowing()) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- FloatWindowManager.getInstance(context)
- .createSmallWindow(context, layoutResId,
- rootLayoutId);
- }
- });
- }
- }
- }
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
- }
除了后台服务之外,我们还需要两个自定义的布局,分别是FloatWindowSmallView和FloatWindowBigView,这两个自定义的布局,主要负责悬浮窗的前台显示,我们分别看一下代码实现。
首先是FloatWindowSmallView类的实现。
- package com.qust.floatwindow;
- import java.lang.reflect.Field;
- import android.annotation.SuppressLint;
- import android.content.Context;
- import android.graphics.PixelFormat;
- import android.view.Gravity;
- import android.view.LayoutInflater;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.WindowManager;
- import android.widget.LinearLayout;
- import android.widget.TextView;
- import com.qust.demo.ScreenUtils;
- import com.qust.floatingwindow.R;
- /**
- * 小悬浮窗,用于初始显示
- *
- * @author zhaokaiqiang
- *
- */
- public class FloatWindowSmallView extends LinearLayout {
- // 小悬浮窗的宽
- public int viewWidth;
- // 小悬浮窗的高
- public int viewHeight;
- // 系统状态栏的高度
- private static int statusBarHeight;
- // 用于更新小悬浮窗的位置
- private WindowManager windowManager;
- // 小悬浮窗的布局参数
- public WindowManager.LayoutParams smallWindowParams;
- // 记录当前手指位置在屏幕上的横坐标
- private float xInScreen;
- // 记录当前手指位置在屏幕上的纵坐标
- private float yInScreen;
- // 记录手指按下时在屏幕上的横坐标,用来判断单击事件
- private float xDownInScreen;
- // 记录手指按下时在屏幕上的纵坐标,用来判断单击事件
- private float yDownInScreen;
- // 记录手指按下时在小悬浮窗的View上的横坐标
- private float xInView;
- // 记录手指按下时在小悬浮窗的View上的纵坐标
- private float yInView;
- // 单击接口
- private OnClickListener listener;
- /**
- * 构造函数
- *
- * @param context
- * 上下文对象
- * @param layoutResId
- * 布局资源id
- * @param rootLayoutId
- * 根布局id
- */
- public FloatWindowSmallView(Context context, int layoutResId,
- int rootLayoutId) {
- super(context);
- windowManager = (WindowManager) context
- .getSystemService(Context.WINDOW_SERVICE);
- LayoutInflater.from(context).inflate(layoutResId, this);
- View view = findViewById(rootLayoutId);
- viewWidth = view.getLayoutParams().width;
- viewHeight = view.getLayoutParams().height;
- statusBarHeight = getStatusBarHeight();
- TextView percentView = (TextView) findViewById(R.id.percent);
- percentView.setText("悬浮窗");
- smallWindowParams = new WindowManager.LayoutParams();
- // 设置显示类型为phone
- smallWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
- // 显示图片格式
- smallWindowParams.format = PixelFormat.RGBA_8888;
- // 设置交互模式
- smallWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- // 设置对齐方式为左上
- smallWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
- smallWindowParams.width = viewWidth;
- smallWindowParams.height = viewHeight;
- smallWindowParams.x = ScreenUtils.getScreenWidth(context);
- smallWindowParams.y = ScreenUtils.getScreenHeight(context) / 2;
- }
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- // 手指按下时记录必要的数据,纵坐标的值都减去状态栏的高度
- case MotionEvent.ACTION_DOWN:
- // 获取相对与小悬浮窗的坐标
- xInView = event.getX();
- yInView = event.getY();
- // 按下时的坐标位置,只记录一次
- xDownInScreen = event.getRawX();
- yDownInScreen = event.getRawY() - statusBarHeight;
- break;
- case MotionEvent.ACTION_MOVE:
- // 时时的更新当前手指在屏幕上的位置
- xInScreen = event.getRawX();
- yInScreen = event.getRawY() - statusBarHeight;
- // 手指移动的时候更新小悬浮窗的位置
- updateViewPosition();
- break;
- case MotionEvent.ACTION_UP:
- // 如果手指离开屏幕时,按下坐标与当前坐标相等,则视为触发了单击事件
- if (xDownInScreen == event.getRawX()
- && yDownInScreen == (event.getRawY() - getStatusBarHeight())) {
- if (listener != null) {
- listener.click();
- }
- }
- break;
- }
- return true;
- }
- /**
- * 设置单击事件的回调接口
- */
- public void setOnClickListener(OnClickListener listener) {
- this.listener = listener;
- }
- /**
- * 更新小悬浮窗在屏幕中的位置
- */
- private void updateViewPosition() {
- smallWindowParams.x = (int) (xInScreen - xInView);
- smallWindowParams.y = (int) (yInScreen - yInView);
- windowManager.updateViewLayout(this, smallWindowParams);
- }
- /**
- * 获取状态栏的高度
- *
- * @return
- */
- private int getStatusBarHeight() {
- try {
- Class<?> c = Class.forName("com.android.internal.R$dimen");
- Object o = c.newInstance();
- Field field = c.getField("status_bar_height");
- int x = (Integer) field.get(o);
- return getResources().getDimensionPixelSize(x);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return 0;
- }
- /**
- * 单击接口
- *
- * @author zhaokaiqiang
- *
- */
- public interface OnClickListener {
- public void click();
- }
- }
在这个类里面,主要的工作是实现悬浮窗口在桌面前端的实现,还有就是位置的移动和单击事件的判断以及处理。这里使用的是主要是WindowManager类的一些方法和属性,下一篇会详细说明,这篇只说实现。
除了小悬浮窗之外,点击之后弹出的二级悬浮窗也是类似的方式添加到桌面上,下面是二级悬浮窗的代码。
- package com.qust.floatwindow;
- import android.content.Context;
- import android.graphics.PixelFormat;
- import android.view.Gravity;
- import android.view.LayoutInflater;
- import android.view.View;
- import android.view.WindowManager;
- import android.widget.LinearLayout;
- import android.widget.TextView;
- import com.qust.demo.ScreenUtils;
- import com.qust.floatingwindow.R;
- public class FloatWindowBigView extends LinearLayout {
- // 记录大悬浮窗的宽
- public int viewWidth;
- // 记录大悬浮窗的高
- public int viewHeight;
- public WindowManager.LayoutParams bigWindowParams;
- private Context context;
- public FloatWindowBigView(Context context) {
- super(context);
- this.context = context;
- LayoutInflater.from(context).inflate(R.layout.float_window_big, this);
- View view = findViewById(R.id.big_window_layout);
- viewWidth = view.getLayoutParams().width;
- viewHeight = view.getLayoutParams().height;
- bigWindowParams = new WindowManager.LayoutParams();
- // 设置显示的位置,默认的是屏幕中心
- bigWindowParams.x = ScreenUtils.getScreenWidth(context) / 2 - viewWidth
- / 2;
- bigWindowParams.y = ScreenUtils.getScreenHeight(context) / 2
- - viewHeight / 2;
- bigWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
- bigWindowParams.format = PixelFormat.RGBA_8888;
- // 设置交互模式
- bigWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- bigWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
- bigWindowParams.width = viewWidth;
- bigWindowParams.height = viewHeight;
- initView();
- }
- private void initView() {
- TextView tv_back = (TextView) findViewById(R.id.tv_back);
- tv_back.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- FloatWindowManager.getInstance(context).removeBigWindow();
- }
- });
- }
- }
这些基本的类建立起来之后,剩下的就是最重要的类FloatWindowManager的实现。这个类实现的就是对悬浮窗的操作。
- package com.qust.floatwindow;
- import android.content.Context;
- import android.content.Intent;
- import android.view.WindowManager;
- /**
- * 悬浮窗管理器
- *
- * @author zhaokaiqiang
- *
- */
- public class FloatWindowManager {
- // 小悬浮窗对象
- private FloatWindowSmallView smallWindow;
- // 大悬浮窗对象
- private FloatWindowBigView bigWindow;
- // 用于控制在屏幕上添加或移除悬浮窗
- private WindowManager mWindowManager;
- // FloatWindowManager的单例
- private static FloatWindowManager floatWindowManager;
- // 上下文对象
- private Context context;
- private FloatWindowManager(Context context) {
- this.context = context;
- }
- public static FloatWindowManager getInstance(Context context) {
- if (floatWindowManager == null) {
- floatWindowManager = new FloatWindowManager(context);
- }
- return floatWindowManager;
- }
- /**
- * 创建小悬浮窗
- *
- * @param context
- * 必须为应用程序的Context.
- */
- public void createSmallWindow(Context context, int layoutResId,
- int rootLayoutId) {
- WindowManager windowManager = getWindowManager();
- if (smallWindow == null) {
- smallWindow = new FloatWindowSmallView(context, layoutResId,
- rootLayoutId);
- windowManager.addView(smallWindow, smallWindow.smallWindowParams);
- }
- }
- /**
- * 将小悬浮窗从屏幕上移除
- *
- * @param context
- */
- public void removeSmallWindow() {
- if (smallWindow != null) {
- WindowManager windowManager = getWindowManager();
- windowManager.removeView(smallWindow);
- smallWindow = null;
- }
- }
- public void setOnClickListener(FloatWindowSmallView.OnClickListener listener) {
- if (smallWindow != null) {
- smallWindow.setOnClickListener(listener);
- }
- }
- /**
- * 创建大悬浮窗
- *
- * @param context
- * 必须为应用程序的Context.
- */
- public void createBigWindow(Context context) {
- WindowManager windowManager = getWindowManager();
- if (bigWindow == null) {
- bigWindow = new FloatWindowBigView(context);
- windowManager.addView(bigWindow, bigWindow.bigWindowParams);
- }
- }
- /**
- * 将大悬浮窗从屏幕上移除
- *
- * @param context
- */
- public void removeBigWindow() {
- if (bigWindow != null) {
- WindowManager windowManager = getWindowManager();
- windowManager.removeView(bigWindow);
- bigWindow = null;
- }
- }
- public void removeAll() {
- context.stopService(new Intent(context, FloatWindowService.class));
- removeSmallWindow();
- removeBigWindow();
- }
- /**
- * 是否有悬浮窗显示(包括小悬浮窗和大悬浮)
- *
- * @return 有悬浮窗显示在桌面上返回true,没有的话返回false
- */
- public boolean isWindowShowing() {
- return smallWindow != null || bigWindow != null;
- }
- /**
- * 如果WindowManager还未创建,则创建新的WindowManager返回。否则返回当前已创建的WindowManager
- *
- * @param context
- * @return
- */
- private WindowManager getWindowManager() {
- if (mWindowManager == null) {
- mWindowManager = (WindowManager) context
- .getSystemService(Context.WINDOW_SERVICE);
- }
- return mWindowManager;
- }
- }
还有个获取屏幕宽高的帮助类。
- package com.qust.demo;
- import android.content.Context;
- import android.view.WindowManager;
- /**
- * 屏幕帮助类
- *
- * @author zhaokaiqiang
- *
- */
- public class ScreenUtils {
- /**
- * 获取屏幕宽度
- *
- * @return
- */
- @SuppressWarnings("deprecation")
- public static int getScreenWidth(Context context) {
- return ((WindowManager) context
- .getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
- .getWidth();
- }
- /**
- * 获取屏幕宽度
- *
- * @return
- */
- @SuppressWarnings("deprecation")
- public static int getScreenHeight(Context context) {
- return ((WindowManager) context
- .getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
- .getHeight();
- }
- }
完成这些,我们就可以直接用了。
- package com.qust.demo;
- import android.app.Activity;
- import android.content.Context;
- import android.content.Intent;
- import android.os.Bundle;
- import android.view.KeyEvent;
- import android.view.View;
- import com.qust.floatingwindow.R;
- import com.qust.floatwindow.FloatWindowManager;
- import com.qust.floatwindow.FloatWindowService;
- import com.qust.floatwindow.FloatWindowSmallView.OnClickListener;
- /**
- * 示例
- *
- * @ClassName: com.qust.demo.MainActivity
- * @Description:
- * @author zhaokaiqiang
- * @date 2014-10-23 下午11:30:13
- *
- */
- public class MainActivity extends Activity {
- private FloatWindowManager floatWindowManager;
- private Context context;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- context = this;
- floatWindowManager = FloatWindowManager.getInstance(context);
- }
- /**
- * 显示小窗口
- *
- * @param view
- */
- public void show(View view) {
- // 需要传递小悬浮窗布局,以及根布局的id,启动后台服务
- Intent intent = new Intent(context, FloatWindowService.class);
- intent.putExtra(FloatWindowService.LAYOUT_RES_ID,
- R.layout.float_window_small);
- intent.putExtra(FloatWindowService.ROOT_LAYOUT_ID,
- R.id.small_window_layout);
- startService(intent);
- }
- /**
- * 显示二级悬浮窗
- *
- * @param view
- */
- public void showBig(View view) {
- // 设置小悬浮窗的单击事件
- floatWindowManager.setOnClickListener(new OnClickListener() {
- @Override
- public void click() {
- floatWindowManager.createBigWindow(context);
- }
- });
- }
- /**
- * 移除所有的悬浮窗
- *
- * @param view
- */
- public void remove(View view) {
- floatWindowManager.removeAll();
- }
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- // 返回键移除二级悬浮窗
- if (keyCode == KeyEvent.KEYCODE_BACK
- && event.getAction() == KeyEvent.ACTION_DOWN) {
- floatWindowManager.removeBigWindow();
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
- }
项目下载地址:https://github.com/ZhaoKaiQiang/FloatWindow