android 编写简单可拖动 TextView 悬浮窗

121 阅读1分钟

在工作中,需要记录的某项测试的具体操作时间,可以在系统桌面上显示实时的具体时间,以便具体分析发生这个行为的日志

TextView 可拖动悬浮窗

package com.lujianfei.showtime

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.WindowManager
import android.widget.TextView
import java.text.SimpleDateFormat
import kotlin.math.abs


/**
 * Author: lujianfei
 * Date: 2025/1/23 16:25
 * Description:
 */

class ShowTimeWindow @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : TextView(context, attrs) {

    private var mWindowManager: WindowManager? = null
    private var mLayoutParams: WindowManager.LayoutParams? = null
    private val handler = Handler(Looper.getMainLooper(), this::handleMessage)
    private var showTime = false
    private var mTouchStartX = 0f
    private var mTouchStartY = 0f
    private var mX = 0f
    private var mY = 0f
    private val mOnTouchListener = OnTouchListener { v: View, event: MotionEvent ->
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 获取相对View的坐标,即以此View左上角为原点
                mTouchStartX = event.x
                mTouchStartY = event.y
                return@OnTouchListener true
            }

            MotionEvent.ACTION_MOVE -> {
                mX = event.rawX
                mY = event.rawY - getStatusBarHeight() // 系统状态栏的高度
                updateViewPosition()
                return@OnTouchListener true
            }

            MotionEvent.ACTION_UP -> {
                updateViewPosition()
                mTouchStartX = 0f
                mTouchStartY = 0f
                if (abs((event.rawX - mTouchStartX).toDouble()) < 10 && abs(
                        (event.rawY - mTouchStartY).toDouble()
                    ) < 10
                ) {
                    v.performClick()
                }
                return@OnTouchListener true
            }
        }
        false
    }



    init {
        mLayoutParams = WindowManager.LayoutParams()
        mLayoutParams?.width = WindowManager.LayoutParams.WRAP_CONTENT
        mLayoutParams?.height = WindowManager.LayoutParams.WRAP_CONTENT
        mLayoutParams?.type  = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        mLayoutParams?.flags  = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        mLayoutParams?.gravity = Gravity.START or Gravity.TOP
        val displayMetrics = resources.displayMetrics
        mLayoutParams?.x = displayMetrics.widthPixels / 2
        mLayoutParams?.y = displayMetrics.heightPixels / 2
        setBackgroundColor(0x80000000.toInt())
        setTextColor(0xFFFFFFFF.toInt())
        textSize = 30f
        setOnTouchListener(mOnTouchListener)
    }

    fun showWindow() {
        if (mWindowManager == null) {
            mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        }
        text = "显示时间"
        kotlin.runCatching {
            mWindowManager?.addView(this, mLayoutParams)
        }
        showTime = true
        handler.sendEmptyMessageDelayed(0, 1000)
    }

    fun hideWindow() {
        kotlin.runCatching {
            mWindowManager?.removeView(this)
        }
        showTime = false
        handler.removeMessages(0)
    }

    private fun handleMessage(msg: Message): Boolean {
        val timeStr = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(System.currentTimeMillis())
        text = "当前时间:${timeStr}"
        if (showTime) {
            handler.sendEmptyMessageDelayed(0, 10)
        }
        return true
    }


    private fun updateViewPosition() {
        // 更新浮动窗口位置参数
        mLayoutParams?.let {
            it.x = (mX - mTouchStartX).toInt()
            it.y = (mY - mTouchStartY).toInt()
            mWindowManager?.updateViewLayout(this, it)
        }
    }


    private fun getStatusBarHeight(): Int {
        var result = 0
        val resourceId: Int =
            resources.getIdentifier("status_bar_height", "dimen", "android")
        if (resourceId > 0) {
            result = resources.getDimensionPixelSize(resourceId)
        }
        return result
    }
}

使用方法

在使用的地方放两个按钮, 分别用于显示/隐藏

package com.lujianfei.showtime

import android.app.Activity
import android.os.Bundle
import android.view.View


/**
 * Author: lujianfei
 * Date: 2025/1/23 16:24
 * Description:
 */

class MainActivity:Activity() {

    private var showTimeWindow:ShowTimeWindow ?= null

    private fun checkShowTimeWindow() {
        if (showTimeWindow == null) {
            showTimeWindow = ShowTimeWindow(this)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        requestPermissions(arrayOf(android.Manifest.permission.SYSTEM_ALERT_WINDOW , "android.permission.SYSTEM_OVERLAY_WINDOW"), 100)
        findViewById<View>(R.id.showTimeButton).setOnClickListener {
            checkShowTimeWindow()
            showTimeWindow?.showWindow()
        }
        findViewById<View>(R.id.hideTimeButton).setOnClickListener {
            showTimeWindow?.hideWindow()
        }
    }
}