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这玩意不太好上传

相关推荐
阿巴斯甜2 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker2 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95273 小时前
Andorid Google 登录接入文档
android
黄林晴4 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab17 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿20 小时前
Android MediaPlayer 笔记
android
Jony_20 小时前
Android 启动优化方案
android
阿巴斯甜20 小时前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇20 小时前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android