Android 实现一个系统级的悬浮秒表

前言

由于项目需要将手机录屏和时间日志对应起来,一般的手机录屏只能看到分钟,但是APP的日志输出通常都是秒级别的,于是决定自己手撸一个悬浮秒表(有拖拽效果)。

效果如下

具体实现

大致的实现思路:

创建一个悬浮窗口,并在该窗口中显示当前时间。它继承自 Service 类,并使用 WindowManager 来管理悬浮窗口的布局和显示。该类还处理悬浮窗口的触摸事件,使用户可以拖动窗口

FloatingImageDisplayService 代码如下:

java 复制代码
//继承service类,开启服务通过服务来显示悬浮窗
public class FloatingImageDisplayService extends Service {
    public static boolean isStarted = false;

    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private View displayView;

    @Override
    public void onCreate() {
        super.onCreate();
        // 标记服务已启动
        isStarted = true;
        // 获取WindowManager服务
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        // 初始化WindowManager.LayoutParams对象
        layoutParams = new WindowManager.LayoutParams();
        // 根据Android版本设置窗口类型
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }

        // 设置窗口的格式
        layoutParams.format = PixelFormat.RGBA_8888;
        // 设置窗口的位置和对齐方式
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        // 设置窗口的标志,窗口不获取焦点且不拦截触摸事件
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        // 设置窗口的宽度和高度
        layoutParams.width = 400;
        layoutParams.height = 200;
        // 设置窗口的初始位置
        layoutParams.x = 300;
        layoutParams.y = 300;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //开启服务后加载悬浮窗
        showFloatingWindow();
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @SuppressLint("SetTextI18n")
    private void showFloatingWindow() {
        // 检查是否有悬浮窗权限
        if (Settings.canDrawOverlays(this)) {
            // 获取LayoutInflater服务
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            // 加载悬浮窗布局
            displayView = layoutInflater.inflate(R.layout.image_display, null);
            // 设置触摸监听器,使悬浮窗可以拖动
            displayView.setOnTouchListener(new FloatingOnTouchListener());
            // 获取TextView控件
            TextView textView = displayView.findViewById(R.id.textView);

            // 创建Handler对象
            final Handler handler = new Handler();
            // 启动一个定时任务,每秒更新一次TextView的内容
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 获取当前时间
                    long currentTime = System.currentTimeMillis();
                    Date date = new Date(currentTime);
                    // 格式化时间戳精确到秒
                    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                    String formattedTime = sdf.format(date);
                    // 设置TextView的文本内容
                    textView.setText(formattedTime + "");
                    // 延迟1秒后再次执行
                    handler.postDelayed(this, 1000);
                }
            });
            // 将悬浮窗添加到窗口中
            windowManager.addView(displayView, layoutParams);

        }
    }

    //监听手势类,实现悬浮窗的拖动
    private class FloatingOnTouchListener implements View.OnTouchListener {
        private int x;
        private int y;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int nowX = (int) event.getRawX();
                    int nowY = (int) event.getRawY();
                    int movedX = nowX - x;
                    int movedY = nowY - y;
                    x = nowX;
                    y = nowY;
                    layoutParams.x = layoutParams.x + movedX;
                    layoutParams.y = layoutParams.y + movedY;
                    windowManager.updateViewLayout(view, layoutParams);
                    break;
                default:
                    break;
            }
            return false;
        }
    }
}

布局文件image_display.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="200dp"
        android:layout_height="60dp"
        android:background="#90000000"
        android:gravity="center"
        android:text="my is Float"
        android:textColor="@color/white"
        android:textSize="20dp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

服务和activity一样都是需要在AndroidManifest.xml中进行注册,并且对于这种系统级别的弹窗是需要申请系统权限的,

AndroidManifest.xml 进行声明

xml 复制代码
<!--    申请系统弹窗权限-->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <!--        注册服务-->
        <service android:name=".FloatingImageDisplayService"/>

到这里我们用来运行悬浮窗口的service就完成了。

如何运行

如何想要我们的悬浮秒表真正的运行起来还需要两步操作:
1.开启服务

service 作为四大组件之一,我们需要找一个中介去运行我们的服务,我这里依托一个activity

java 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //直接在Activity中开启服务
        startFloatingImageDisplayService()
    }
    }

2.如何申请权限

系统级别的悬浮窗是需要动态申请权限的,我们通过startActivityForResult 申请

java 复制代码
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 1)

并在onActivityResult处理权限结果

完整代码如下:

kotlin 复制代码
@Suppress("DEPRECATION")
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 启动悬浮图片显示服务
        startFloatingImageDisplayService()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // 检查请求码是否为1
        if (requestCode == 1) {
            // 检查是否有悬浮窗权限
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show()
                // 启动悬浮图片显示服务
                startService(Intent(this@MainActivity, FloatingImageDisplayService::class.java))
            }
        }
    }

    private fun startFloatingImageDisplayService() {
        // 如果服务已经启动,则返回
        if (FloatingImageDisplayService.isStarted) {
            return
        }
        // 检查是否有悬浮窗权限
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT).show()
            // 请求悬浮窗权限
            startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 1)
        } else {
            Toast.makeText(this, "启动服务", Toast.LENGTH_SHORT).show()
            // 启动悬浮图片显示服务
            startService(Intent(this@MainActivity, FloatingImageDisplayService::class.java))
        }
    }
}

end

以上便是实现一个安卓悬浮秒表的全部代码,由于是自己写着玩的,代码比较粗糙,需要的话可以优化下。需要源码的话也可以call我,CSDN这玩意不太好上传

相关推荐
花追雨8 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜82278 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android
氤氲息8 小时前
Android v4和v7冲突
android
KdanMin8 小时前
高通Android 12 Launcher应用名称太长显示完整
android
chenjk48 小时前
Android不可擦除分区写文件恢复出厂设置,无法读写问题
android
袁震8 小时前
Android-Glide缓存机制
android·缓存·移动开发·glide
工程师老罗8 小时前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite
User_undefined10 小时前
uniapp Native.js 调用安卓arr原生service
android·javascript·uni-app
安小牛10 小时前
android anr 处理
android
刘争Stanley13 小时前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏