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

相关推荐
烬奇小云4 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野16 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO16 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他16 小时前
Android ANR分析总结
android
PenguinLetsGo18 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博20 小时前
音频格式转换
android·音视频
音视频牛哥1 天前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.1 天前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android