mirror of
https://github.com/gkd-kit/gkd.git
synced 2026-06-03 21:01:49 +08:00
This commit is contained in:
parent
27382b7de4
commit
fa7eaac256
@ -1,31 +1,22 @@
|
||||
package li.songe.gkd.service
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.view.Gravity
|
||||
import android.view.Surface
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
@ -33,10 +24,11 @@ import androidx.savedstate.SavedStateRegistryController
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.notif.StopServiceReceiver
|
||||
@ -45,9 +37,10 @@ import li.songe.gkd.shizuku.casted
|
||||
import li.songe.gkd.util.AndroidTarget
|
||||
import li.songe.gkd.util.OnSimpleLife
|
||||
import li.songe.gkd.util.ScreenUtils
|
||||
import li.songe.gkd.util.runMainPost
|
||||
import li.songe.gkd.util.startForegroundServiceByClass
|
||||
import li.songe.gkd.util.stopServiceByClass
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
class TrackService : LifecycleService(), SavedStateRegistryOwner, OnSimpleLife {
|
||||
override fun onCreate() {
|
||||
@ -73,177 +66,272 @@ class TrackService : LifecycleService(), SavedStateRegistryOwner, OnSimpleLife {
|
||||
lifecycleScope.launch { resizeFlow.emit(Unit) }
|
||||
}
|
||||
|
||||
val curRotationFlow = MutableStateFlow(app.compatDisplay.rotation).apply {
|
||||
lifecycleScope.launch {
|
||||
resizeFlow.collect { update { app.compatDisplay.rotation } }
|
||||
}
|
||||
}
|
||||
val strokeWidth = 2f
|
||||
val lineMargin = 75f
|
||||
val pointSize = ScreenUtils.getScreenSize().let { min(it.width, it.height) } * 0.1f
|
||||
val pointRadius = pointSize / 2
|
||||
|
||||
private fun DrawScope.drawTrackPoint(center: Offset) {
|
||||
|
||||
drawLine(
|
||||
color = Color.Yellow,
|
||||
start = Offset(center.x, center.y - lineMargin),
|
||||
end = Offset(center.x, center.y + lineMargin),
|
||||
start = Offset(center.x, center.y - pointRadius),
|
||||
end = Offset(center.x, center.y + pointRadius),
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
drawLine(
|
||||
color = Color.Yellow,
|
||||
start = Offset(center.x - lineMargin, center.y),
|
||||
end = Offset(center.x + lineMargin, center.y),
|
||||
start = Offset(center.x - pointRadius, center.y),
|
||||
end = Offset(center.x + pointRadius, center.y),
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
drawCircle(
|
||||
color = Color.Red,
|
||||
radius = 10f,
|
||||
center = center,
|
||||
)
|
||||
drawCircle(
|
||||
color = Color.Red,
|
||||
radius = 20f,
|
||||
center = center,
|
||||
style = Stroke(4f)
|
||||
)
|
||||
drawCircle(
|
||||
color = Color.Red,
|
||||
radius = 30f,
|
||||
center = center,
|
||||
style = Stroke(4f)
|
||||
)
|
||||
val ringSize = 3
|
||||
repeat(ringSize) { i ->
|
||||
drawCircle(
|
||||
color = Color.Red,
|
||||
radius = pointRadius * 0.8f / ringSize * (i + 1),
|
||||
center = center,
|
||||
style = Stroke(strokeWidth)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwipePointCanvas(swipePoint: SwipeTrackPoint, curRotation: Int) {
|
||||
val progress = remember { Animatable(0f) }
|
||||
LaunchedEffect(null) {
|
||||
// 匀速直线
|
||||
progress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(swipePoint.duration.toInt(), easing = LinearEasing),
|
||||
)
|
||||
delayRemovePosition(swipePoint.id)
|
||||
private abstract inner class FloatLayer {
|
||||
private val view = ComposeView(this@TrackService).apply {
|
||||
setViewTreeSavedStateRegistryOwner(this@TrackService)
|
||||
setViewTreeLifecycleOwner(this@TrackService)
|
||||
setContent(::ComposeContent)
|
||||
}
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val startCenter = swipePoint.start.getCenter(size, curRotation)
|
||||
drawTrackPoint(startCenter)
|
||||
val endCenter = swipePoint.end.getCenter(size, curRotation)
|
||||
val midCenter = Offset(
|
||||
lerp(startCenter.x, endCenter.x, progress.value),
|
||||
lerp(startCenter.y, endCenter.y, progress.value)
|
||||
private val layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT,
|
||||
).apply {
|
||||
gravity = Gravity.START or Gravity.TOP
|
||||
if (AndroidTarget.S) {
|
||||
alpha = app.inputManager.maximumObscuringOpacityForTouch
|
||||
}
|
||||
}
|
||||
private val subScope = MainScope()
|
||||
private val boundsRect = Rect()
|
||||
protected var connected = false
|
||||
protected var removed = false
|
||||
|
||||
@Composable
|
||||
abstract fun ComposeContent()
|
||||
abstract fun syncRotation()
|
||||
|
||||
fun removeView() {
|
||||
subScope.cancel()
|
||||
windowManager.removeView(view)
|
||||
removed = true
|
||||
}
|
||||
|
||||
fun getRect(): Rect? {
|
||||
if (!connected || removed) return null
|
||||
return boundsRect
|
||||
}
|
||||
|
||||
fun setAlpha(alpha: Float) {
|
||||
if (!connected || removed) return
|
||||
if (layoutParams.alpha == alpha) return
|
||||
layoutParams.alpha = alpha
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
}
|
||||
|
||||
fun updateViewLayout(
|
||||
x: Number,
|
||||
y: Number,
|
||||
width: Number = layoutParams.width,
|
||||
height: Number = layoutParams.height,
|
||||
) {
|
||||
layoutParams.x = x.toInt()
|
||||
layoutParams.y = y.toInt()
|
||||
layoutParams.width = width.toInt()
|
||||
layoutParams.height = height.toInt()
|
||||
boundsRect.set(
|
||||
layoutParams.x,
|
||||
layoutParams.y,
|
||||
layoutParams.x + layoutParams.width,
|
||||
layoutParams.y + layoutParams.height,
|
||||
)
|
||||
drawTrackPoint(midCenter)
|
||||
if (!connected) {
|
||||
connected = true
|
||||
windowManager.addView(view, layoutParams)
|
||||
subScope.launch { resizeFlow.collect { syncRotation() } }
|
||||
} else {
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
recalcOverlappingAlpha()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PointFloatLayer(val point: TrackPoint) : FloatLayer() {
|
||||
@Composable
|
||||
override fun ComposeContent() = Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawTrackPoint(Offset(pointRadius, pointRadius))
|
||||
}
|
||||
|
||||
override fun syncRotation() {
|
||||
val (x, y) = point.getCurCenter() - Offset(pointRadius, pointRadius)
|
||||
updateViewLayout(x, y)
|
||||
}
|
||||
|
||||
init {
|
||||
updateViewLayout(point.x - pointRadius, point.y - pointRadius, pointSize, pointSize)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SwipePointFloatLayer(val swipePoint: SwipeTrackPoint) : FloatLayer() {
|
||||
@Composable
|
||||
override fun ComposeContent() = Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val sc = swipePoint.start.getCurCenter()
|
||||
val ec = swipePoint.end.getCurCenter()
|
||||
val start = Offset(
|
||||
if (sc.x <= ec.x) pointRadius else size.width - pointRadius,
|
||||
if (sc.y <= ec.y) pointRadius else size.height - pointRadius
|
||||
)
|
||||
val end = Offset(
|
||||
if (sc.x <= ec.x) size.width - pointRadius else pointRadius,
|
||||
if (sc.y <= ec.y) size.height - pointRadius else pointRadius
|
||||
)
|
||||
drawTrackPoint(start)
|
||||
drawTrackPoint(end)
|
||||
drawLine(
|
||||
color = Color.Blue,
|
||||
start = startCenter,
|
||||
end = midCenter,
|
||||
start = start,
|
||||
end = end,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ComposeContent() {
|
||||
val curRotation by curRotationFlow.collectAsState()
|
||||
val positionList = pointListFlow.collectAsState().value
|
||||
Canvas(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
positionList.forEach { point ->
|
||||
drawTrackPoint(point.getCenter(size, curRotation))
|
||||
}
|
||||
override fun syncRotation() {
|
||||
val f = animator.animatedValue as Float
|
||||
val sc = swipePoint.start.getCurCenter()
|
||||
val ec = swipePoint.end.getCurCenter()
|
||||
val cur = Offset(sc.x + (ec.x - sc.x) * f, sc.y + (ec.y - sc.y) * f)
|
||||
updateViewLayout(
|
||||
minOf(sc.x, cur.x) - pointRadius,
|
||||
minOf(sc.y, cur.y) - pointRadius,
|
||||
maxOf(sc.x, cur.x) - minOf(sc.x, cur.x) + pointSize,
|
||||
maxOf(sc.y, cur.y) - minOf(sc.y, cur.y) + pointSize
|
||||
)
|
||||
}
|
||||
val swipePointList = swipePointListFlow.collectAsState().value
|
||||
swipePointList.forEach { swipePoint ->
|
||||
key(swipePoint.id) { SwipePointCanvas(swipePoint, curRotation) }
|
||||
}
|
||||
}
|
||||
|
||||
val view by lazy {
|
||||
ComposeView(this).apply {
|
||||
setViewTreeSavedStateRegistryOwner(this@TrackService)
|
||||
setViewTreeLifecycleOwner(this@TrackService)
|
||||
setContent {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ComposeContent()
|
||||
private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = swipePoint.duration
|
||||
addUpdateListener {
|
||||
if (removed) {
|
||||
cancel()
|
||||
} else {
|
||||
val f = it.animatedValue as Float
|
||||
val sc = swipePoint.start.getCurCenter()
|
||||
val ec = swipePoint.end.getCurCenter()
|
||||
val cur = Offset(sc.x + (ec.x - sc.x) * f, sc.y + (ec.y - sc.y) * f)
|
||||
updateViewLayout(
|
||||
minOf(sc.x, cur.x) - pointRadius,
|
||||
minOf(sc.y, cur.y) - pointRadius,
|
||||
maxOf(sc.x, cur.x) - minOf(sc.x, cur.x) + pointSize,
|
||||
maxOf(sc.y, cur.y) - minOf(sc.y, cur.y) + pointSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val sc = swipePoint.start.getCurCenter()
|
||||
updateViewLayout(sc.x - pointRadius, sc.y - pointRadius, pointSize, pointSize)
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
val layoutParams = WindowManager.LayoutParams(
|
||||
ScreenUtils.getScreenWidth(),
|
||||
ScreenUtils.getScreenHeight(),
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT,
|
||||
).apply {
|
||||
gravity = Gravity.START or Gravity.TOP
|
||||
if (AndroidTarget.S) {
|
||||
// fix #1325
|
||||
alpha = app.inputManager.maximumObscuringOpacityForTouch
|
||||
private val layerMap = hashMapOf<Int, FloatLayer>().apply {
|
||||
onDestroyed {
|
||||
forEach { it.value.removeView() }
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun recalcOverlappingAlpha() {
|
||||
if (!AndroidTarget.S) return
|
||||
val maxOpacity = app.inputManager.maximumObscuringOpacityForTouch
|
||||
val entries = layerMap.values.mapNotNull { layer ->
|
||||
layer.getRect()?.let { rect -> layer to rect }
|
||||
}
|
||||
for ((layer, rect) in entries) {
|
||||
var overlapCount = 1
|
||||
for ((other, otherRect) in entries) {
|
||||
if (other !== layer && Rect.intersects(rect, otherRect)) {
|
||||
overlapCount++
|
||||
}
|
||||
}
|
||||
val safeAlpha = if (overlapCount > 1) {
|
||||
1f - (1f - maxOpacity).toDouble().pow(1.0 / overlapCount).toFloat()
|
||||
} else {
|
||||
maxOpacity
|
||||
}
|
||||
layer.setAlpha(safeAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
val tapDelay = 100L
|
||||
val missDelay = 7500L
|
||||
|
||||
private fun addPoint(point: TrackPoint) {
|
||||
runScopePost(tapDelay) {
|
||||
layerMap[point.id] = PointFloatLayer(point)
|
||||
recalcOverlappingAlpha()
|
||||
}
|
||||
runScopePost(missDelay) {
|
||||
layerMap.remove(point.id)?.removeView()
|
||||
recalcOverlappingAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSwipePoint(swipePoint: SwipeTrackPoint) {
|
||||
runScopePost(tapDelay) {
|
||||
layerMap[swipePoint.id] = SwipePointFloatLayer(swipePoint)
|
||||
recalcOverlappingAlpha()
|
||||
}
|
||||
runScopePost(missDelay + swipePoint.duration) {
|
||||
layerMap.remove(swipePoint.id)?.removeView()
|
||||
recalcOverlappingAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
useLogLifecycle()
|
||||
onCreated { service = this }
|
||||
onDestroyed { service = null }
|
||||
useAliveFlow(isRunning)
|
||||
useAliveToast("轨迹提示")
|
||||
StopServiceReceiver.autoRegister()
|
||||
onCreated { trackNotif.notifyService() }
|
||||
onCreated { windowManager.addView(view, layoutParams) }
|
||||
onDestroyed { windowManager.removeView(view) }
|
||||
onDestroyed { clearPosition() }
|
||||
onCreated {
|
||||
scope.launch {
|
||||
resizeFlow.collect {
|
||||
layoutParams.width = ScreenUtils.getScreenWidth()
|
||||
layoutParams.height = ScreenUtils.getScreenHeight()
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val pointListFlow = MutableStateFlow<List<TrackPoint>>(emptyList())
|
||||
private val swipePointListFlow = MutableStateFlow<List<SwipeTrackPoint>>(emptyList())
|
||||
private fun clearPosition() {
|
||||
pointListFlow.value = emptyList()
|
||||
swipePointListFlow.value = emptyList()
|
||||
autoIncreaseId.value = 0
|
||||
}
|
||||
|
||||
private fun delayRemovePosition(id: Int) {
|
||||
runMainPost(7500) {
|
||||
pointListFlow.update { it.filter { v -> v.id != id } }
|
||||
swipePointListFlow.update { it.filter { v -> v.id != id } }
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var service: TrackService? = null
|
||||
val isRunning: StateFlow<Boolean>
|
||||
field = MutableStateFlow(false)
|
||||
|
||||
fun start() = startForegroundServiceByClass(TrackService::class)
|
||||
fun stop() = stopServiceByClass(TrackService::class)
|
||||
fun addA11yNodePosition(node: AccessibilityNodeInfo) {
|
||||
if (!isRunning.value) return
|
||||
addXyPosition(
|
||||
node.casted.boundsInScreen.centerX().toFloat(),
|
||||
node.casted.boundsInScreen.centerY().toFloat(),
|
||||
service?.addPoint(
|
||||
TrackPoint(
|
||||
node.casted.boundsInScreen.centerX().toFloat(),
|
||||
node.casted.boundsInScreen.centerY().toFloat(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun addXyPosition(x: Float, y: Float) {
|
||||
if (!isRunning.value) return
|
||||
val p = TrackPoint(x, y)
|
||||
pointListFlow.update { it + p }
|
||||
delayRemovePosition(p.id)
|
||||
service?.addPoint(TrackPoint(x, y))
|
||||
}
|
||||
|
||||
fun addSwipePosition(
|
||||
@ -253,13 +341,13 @@ class TrackService : LifecycleService(), SavedStateRegistryOwner, OnSimpleLife {
|
||||
endY: Float,
|
||||
duration: Long
|
||||
) {
|
||||
if (!isRunning.value) return
|
||||
val p = SwipeTrackPoint(
|
||||
start = TrackPoint(startX, startY),
|
||||
end = TrackPoint(endX, endY),
|
||||
duration = duration,
|
||||
service?.addSwipePoint(
|
||||
SwipeTrackPoint(
|
||||
TrackPoint(startX, startY),
|
||||
TrackPoint(endX, endY),
|
||||
duration
|
||||
)
|
||||
)
|
||||
swipePointListFlow.update { it + p }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -271,20 +359,19 @@ private data class TrackPoint(
|
||||
val y: Float,
|
||||
) {
|
||||
val id = autoIncreaseId.incrementAndGet()
|
||||
val screenWidth = ScreenUtils.getScreenWidth().toFloat()
|
||||
val screenHeight = ScreenUtils.getScreenHeight().toFloat()
|
||||
val screenSize = ScreenUtils.getScreenSize()
|
||||
val rotation = app.compatDisplay.rotation
|
||||
|
||||
fun getCenter(size: Size, curRotation: Int): Offset {
|
||||
val curWidth = size.width
|
||||
val curHeight = size.height
|
||||
val (physX, physY) = screenToPhysical(x, y, screenWidth, screenHeight, rotation)
|
||||
return physicalToScreen(physX, physY, curWidth, curHeight, curRotation)
|
||||
fun getCurCenter(): Offset {
|
||||
val curSize = ScreenUtils.getScreenSize()
|
||||
val curRotation = app.compatDisplay.rotation
|
||||
val (physX, physY) = screenToPhysical(x, y, screenSize.width, screenSize.height, rotation)
|
||||
return physicalToScreen(physX, physY, curSize.width, curSize.height, curRotation)
|
||||
}
|
||||
|
||||
private fun screenToPhysical(
|
||||
sx: Float, sy: Float,
|
||||
sw: Float, sh: Float,
|
||||
sw: Int, sh: Int,
|
||||
rot: Int,
|
||||
): Offset = when (rot) {
|
||||
Surface.ROTATION_0 -> Offset(sx, sy)
|
||||
@ -296,7 +383,7 @@ private data class TrackPoint(
|
||||
|
||||
private fun physicalToScreen(
|
||||
px: Float, py: Float,
|
||||
sw: Float, sh: Float,
|
||||
sw: Int, sh: Int,
|
||||
rot: Int,
|
||||
): Offset = when (rot) {
|
||||
Surface.ROTATION_0 -> Offset(px, py)
|
||||
|
||||
@ -421,6 +421,11 @@ fun useSettingsPage(): ScaffoldExt {
|
||||
checked = TrackService.isRunning.collectAsState().value,
|
||||
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
mainVm.dialogFlow.waitResult(
|
||||
title = "使用须知",
|
||||
text = "开启「轨迹提示」后点击或滑动后会在屏幕上使用悬浮窗绘制轨迹(一段时间后消失),如果新触摸事件恰好在悬浮窗区域内,可能会被目标应用拒绝,从而导致点击或滑动无响应",
|
||||
confirmText = "继续",
|
||||
)
|
||||
requiredPermission(context, foregroundServiceSpecialUseState)
|
||||
requiredPermission(context, notificationState)
|
||||
requiredPermission(context, canDrawOverlaysState)
|
||||
|
||||
@ -2,9 +2,12 @@ package li.songe.gkd.util
|
||||
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.loc.Loc
|
||||
import java.util.WeakHashMap
|
||||
|
||||
@ -61,6 +64,17 @@ interface OnSimpleLife {
|
||||
toast("${name}已关闭", loc = loc)
|
||||
}
|
||||
}
|
||||
|
||||
fun runScopePost(delayMillis: Long, r: Runnable) {
|
||||
if (delayMillis == 0L && isMainThread) {
|
||||
r.run()
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
delay(delayMillis)
|
||||
r.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OnA11yLife : OnSimpleLife {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user