@momo-kits/native-kits 0.162.1-debug → 0.162.1-gif.1-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compose/build.gradle.kts +5 -1
- package/compose/build.gradle.kts.backup +63 -59
- package/compose/src/androidMain/kotlin/vn/momo/kits/fpsmonitor/FPSGraphView.kt +250 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/fpsmonitor/FPSMonitor.kt +249 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/fpsmonitor/FPSOverlayView.kt +99 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +33 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +28 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +46 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/gradle/libs.versions.toml +5 -0
- package/gradle.properties +1 -1
- package/ios/FPSMonitor/FPSMonitor.swift +562 -0
- package/package.json +1 -1
- package/publish.sh +2 -2
- package/example/ios/Example.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -46
- package/local.properties +0 -8
package/compose/build.gradle.kts
CHANGED
|
@@ -40,7 +40,7 @@ kotlin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
cocoapods {
|
|
43
|
-
version = "0.
|
|
43
|
+
version = "0.162.1-gif.1-debug"
|
|
44
44
|
summary = "IOS Shared module"
|
|
45
45
|
homepage = "https://momo.vn"
|
|
46
46
|
ios.deploymentTarget = "15.0"
|
|
@@ -74,6 +74,10 @@ kotlin {
|
|
|
74
74
|
implementation(libs.kotlinx.datetime)
|
|
75
75
|
api(libs.native.max.api)
|
|
76
76
|
implementation(libs.compottie)
|
|
77
|
+
implementation(libs.sketch.compose)
|
|
78
|
+
implementation(libs.sketch.http)
|
|
79
|
+
implementation(libs.sketch.animated.core)
|
|
80
|
+
implementation(libs.sketch.animated.gif)
|
|
77
81
|
}
|
|
78
82
|
androidMain.dependencies {
|
|
79
83
|
implementation(libs.ktor.client.okhttp)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import com.vanniktech.maven.publish.JavadocJar
|
|
2
|
+
import com.vanniktech.maven.publish.KotlinMultiplatform
|
|
3
3
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
4
4
|
|
|
5
5
|
plugins {
|
|
@@ -7,7 +7,7 @@ plugins {
|
|
|
7
7
|
alias(libs.plugins.android.library)
|
|
8
8
|
alias(libs.plugins.compose)
|
|
9
9
|
alias(libs.plugins.kotlin.compose.compiler)
|
|
10
|
-
|
|
10
|
+
alias(libs.plugins.vanniktech.mavenPublish)
|
|
11
11
|
id(libs.plugins.jetbrains.kotlin.cocoapods.get().pluginId)
|
|
12
12
|
kotlin("plugin.serialization")
|
|
13
13
|
}
|
|
@@ -40,7 +40,7 @@ kotlin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
cocoapods {
|
|
43
|
-
version =
|
|
43
|
+
version = gitlabVersion
|
|
44
44
|
summary = "IOS Shared module"
|
|
45
45
|
homepage = "https://momo.vn"
|
|
46
46
|
ios.deploymentTarget = "15.0"
|
|
@@ -74,6 +74,10 @@ kotlin {
|
|
|
74
74
|
implementation(libs.kotlinx.datetime)
|
|
75
75
|
api(libs.native.max.api)
|
|
76
76
|
implementation(libs.compottie)
|
|
77
|
+
implementation(libs.sketch.compose)
|
|
78
|
+
implementation(libs.sketch.http)
|
|
79
|
+
implementation(libs.sketch.animated.core)
|
|
80
|
+
implementation(libs.sketch.animated.gif)
|
|
77
81
|
}
|
|
78
82
|
androidMain.dependencies {
|
|
79
83
|
implementation(libs.ktor.client.okhttp)
|
|
@@ -86,14 +90,14 @@ kotlin {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
android {
|
|
89
|
-
namespace = "
|
|
90
|
-
compileSdk =
|
|
93
|
+
namespace = "$gitlabGroup.$gitlabArtifactId"
|
|
94
|
+
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
|
91
95
|
compileOptions {
|
|
92
96
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
93
97
|
targetCompatibility = JavaVersion.VERSION_17
|
|
94
98
|
}
|
|
95
99
|
defaultConfig {
|
|
96
|
-
minSdk =
|
|
100
|
+
minSdk = libs.versions.android.minSdk.get().toInt()
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -132,55 +136,55 @@ compose {
|
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
139
|
+
publishing {
|
|
140
|
+
repositories {
|
|
141
|
+
maven {
|
|
142
|
+
name = "${gitlabRepo}Packages"
|
|
143
|
+
credentials {
|
|
144
|
+
username = gitlabUser
|
|
145
|
+
password = gitlabKey
|
|
146
|
+
}
|
|
147
|
+
url = uri("$gitlabUrl")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mavenPublishing {
|
|
153
|
+
configure(
|
|
154
|
+
KotlinMultiplatform(
|
|
155
|
+
javadocJar = JavadocJar.Empty(),
|
|
156
|
+
sourcesJar = true,
|
|
157
|
+
androidVariantsToPublish = listOf("release")
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
repositories {
|
|
162
|
+
maven {
|
|
163
|
+
name = "${gitlabRepo}Packages"
|
|
164
|
+
credentials { username = gitlabUser; password = gitlabKey }
|
|
165
|
+
url = uri("$gitlabUrl")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
coordinates(gitlabGroup.toString(), gitlabArtifactId, gitlabVersion.toString())
|
|
170
|
+
|
|
171
|
+
pom {
|
|
172
|
+
name = "$gitlabName Klib"
|
|
173
|
+
description = "$gitlabName"
|
|
174
|
+
url = "https://gitlab.mservice.com.vn"
|
|
175
|
+
licenses {
|
|
176
|
+
license {
|
|
177
|
+
name = "The Apache License, Version 2.0"
|
|
178
|
+
url = "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
|
179
|
+
distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
developers {
|
|
183
|
+
developer {
|
|
184
|
+
id = "kotlin-hands-on"
|
|
185
|
+
name = "Kotlin Developer Advocate"
|
|
186
|
+
url = "https://github.com/kotlin-hands-on/"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
package vn.momo.kits.fpsmonitor
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Rect
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.view.View
|
|
10
|
+
import kotlin.math.max
|
|
11
|
+
import kotlin.math.min
|
|
12
|
+
|
|
13
|
+
class FPSGraphView(context: Context) : View(context) {
|
|
14
|
+
private val fpsValues = mutableListOf<Float>()
|
|
15
|
+
private val cpuTimeValues = mutableListOf<Float>()
|
|
16
|
+
private val gpuTimeValues = mutableListOf<Float>()
|
|
17
|
+
private val maxPoints = 100
|
|
18
|
+
|
|
19
|
+
private var currentMetrics: FPSMetrics? = null
|
|
20
|
+
private var collapsed = false
|
|
21
|
+
private val density = resources.displayMetrics.density
|
|
22
|
+
private val scaledDensity = resources.displayMetrics.density * resources.configuration.fontScale
|
|
23
|
+
|
|
24
|
+
private val fpsColor = Color.rgb(0, 255, 0)
|
|
25
|
+
private val cpuColor = Color.rgb(255, 165, 0)
|
|
26
|
+
private val gpuColor = Color.rgb(0, 191, 255)
|
|
27
|
+
private val memColor = Color.rgb(255, 105, 180)
|
|
28
|
+
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
29
|
+
color = Color.argb(153, 0, 0, 0)
|
|
30
|
+
}
|
|
31
|
+
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
32
|
+
color = Color.WHITE
|
|
33
|
+
textSize = 12.sp
|
|
34
|
+
}
|
|
35
|
+
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
36
|
+
strokeWidth = 1.25f.dp
|
|
37
|
+
style = Paint.Style.STROKE
|
|
38
|
+
}
|
|
39
|
+
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
40
|
+
color = Color.argb(25, 255, 255, 255)
|
|
41
|
+
strokeWidth = 0.5f.dp
|
|
42
|
+
style = Paint.Style.STROKE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
init {
|
|
46
|
+
setLayerType(LAYER_TYPE_SOFTWARE, null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun toggleCollapse() {
|
|
50
|
+
collapsed = !collapsed
|
|
51
|
+
requestLayout()
|
|
52
|
+
invalidate()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun updateMetrics(metrics: FPSMetrics) {
|
|
56
|
+
currentMetrics = metrics
|
|
57
|
+
append(fpsValues, metrics.fps)
|
|
58
|
+
append(cpuTimeValues, metrics.estCPUTimePerFrameMs)
|
|
59
|
+
append(gpuTimeValues, metrics.estGPUTimePerFrameMs)
|
|
60
|
+
requestLayout()
|
|
61
|
+
invalidate()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
65
|
+
if (collapsed) {
|
|
66
|
+
setMeasuredDimension(72.dp.toInt(), 44.dp.toInt())
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
textPaint.textSize = 12.sp
|
|
71
|
+
val metrics = currentMetrics
|
|
72
|
+
val deviceInfo = metrics?.deviceInfo
|
|
73
|
+
val lines = listOf(
|
|
74
|
+
"${deviceInfo?.deviceName ?: "Device"} [${deviceInfo?.resolution ?: "0x0"}]",
|
|
75
|
+
"${String.format("%.1f", deviceInfo?.density ?: 1f)}x ${deviceInfo?.refreshRate?.toInt() ?: 60}Hz",
|
|
76
|
+
"Pre: 99.99ms [99.99 99.99]",
|
|
77
|
+
"GPU: 99.99ms [99.99 99.99]",
|
|
78
|
+
"Mem: 999.9MB",
|
|
79
|
+
"FPS: 99.99 [99.99 99.99]",
|
|
80
|
+
)
|
|
81
|
+
val desiredWidth = max(220.dp, lines.maxOf { textPaint.measureText(it) } + 16.dp).toInt()
|
|
82
|
+
val desiredHeight = 156.dp.toInt()
|
|
83
|
+
|
|
84
|
+
setMeasuredDimension(
|
|
85
|
+
resolveSize(desiredWidth, widthMeasureSpec),
|
|
86
|
+
resolveSize(desiredHeight, heightMeasureSpec),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onDraw(canvas: Canvas) {
|
|
91
|
+
super.onDraw(canvas)
|
|
92
|
+
canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), 8.dp, 8.dp, backgroundPaint)
|
|
93
|
+
|
|
94
|
+
if (collapsed) {
|
|
95
|
+
drawCollapsed(canvas)
|
|
96
|
+
} else {
|
|
97
|
+
drawExpanded(canvas)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun drawCollapsed(canvas: Canvas) {
|
|
102
|
+
val fpsText = String.format("%.0f", currentMetrics?.fps ?: 0f)
|
|
103
|
+
textPaint.color = Color.WHITE
|
|
104
|
+
textPaint.textSize = 18.sp
|
|
105
|
+
val bounds = Rect()
|
|
106
|
+
textPaint.getTextBounds(fpsText, 0, fpsText.length, bounds)
|
|
107
|
+
canvas.drawText(
|
|
108
|
+
fpsText,
|
|
109
|
+
(width - bounds.width()) / 2f,
|
|
110
|
+
(height + bounds.height()) / 2f,
|
|
111
|
+
textPaint,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun drawExpanded(canvas: Canvas) {
|
|
116
|
+
val metrics = currentMetrics
|
|
117
|
+
val margin = 8.dp
|
|
118
|
+
var y = margin + 12.sp
|
|
119
|
+
|
|
120
|
+
textPaint.textSize = 12.sp
|
|
121
|
+
textPaint.color = Color.WHITE
|
|
122
|
+
|
|
123
|
+
val deviceInfo = metrics?.deviceInfo
|
|
124
|
+
drawSplit(canvas, deviceInfo?.deviceName ?: "Device", "[${deviceInfo?.resolution ?: "0x0"}]", y, Color.WHITE)
|
|
125
|
+
y += 15.sp
|
|
126
|
+
drawSplit(
|
|
127
|
+
canvas,
|
|
128
|
+
"${String.format("%.1f", deviceInfo?.density ?: 1f)}x",
|
|
129
|
+
"${deviceInfo?.refreshRate?.toInt() ?: 60}Hz",
|
|
130
|
+
y,
|
|
131
|
+
Color.WHITE,
|
|
132
|
+
)
|
|
133
|
+
y += 18.sp
|
|
134
|
+
|
|
135
|
+
val cpuMinMax = minMax(cpuTimeValues, metrics?.estCPUTimePerFrameMs ?: 0f)
|
|
136
|
+
val gpuMinMax = minMax(gpuTimeValues, metrics?.estGPUTimePerFrameMs ?: 0f)
|
|
137
|
+
val fpsMinMax = minMax(fpsValues, metrics?.fps ?: 0f)
|
|
138
|
+
|
|
139
|
+
drawSplit(
|
|
140
|
+
canvas,
|
|
141
|
+
"Pre: ${String.format("%.2f", metrics?.estCPUTimePerFrameMs ?: 0f)}ms",
|
|
142
|
+
"[${String.format("%.2f", cpuMinMax.first)} ${String.format("%.2f", cpuMinMax.second)}]",
|
|
143
|
+
y,
|
|
144
|
+
cpuColor,
|
|
145
|
+
)
|
|
146
|
+
y += 15.sp
|
|
147
|
+
|
|
148
|
+
drawSplit(
|
|
149
|
+
canvas,
|
|
150
|
+
"GPU: ${String.format("%.2f", metrics?.estGPUTimePerFrameMs ?: 0f)}ms",
|
|
151
|
+
"[${String.format("%.2f", gpuMinMax.first)} ${String.format("%.2f", gpuMinMax.second)}]",
|
|
152
|
+
y,
|
|
153
|
+
gpuColor,
|
|
154
|
+
)
|
|
155
|
+
y += 15.sp
|
|
156
|
+
|
|
157
|
+
drawText(canvas, "Mem: ${String.format("%.1f", metrics?.appMemoryMB ?: 0f)}MB", margin, y, memColor)
|
|
158
|
+
y += 15.sp
|
|
159
|
+
|
|
160
|
+
drawSplit(
|
|
161
|
+
canvas,
|
|
162
|
+
"FPS: ${String.format("%.2f", metrics?.fps ?: 0f)}",
|
|
163
|
+
"[${String.format("%.2f", fpsMinMax.first)} ${String.format("%.2f", fpsMinMax.second)}]",
|
|
164
|
+
y,
|
|
165
|
+
fpsColor,
|
|
166
|
+
)
|
|
167
|
+
y += 17.sp
|
|
168
|
+
|
|
169
|
+
val graphHeight = height - y - margin
|
|
170
|
+
if (graphHeight > 24.dp) {
|
|
171
|
+
drawGraph(canvas, margin, y, width - margin * 2, graphHeight)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun drawSplit(canvas: Canvas, left: String, right: String, y: Float, color: Int) {
|
|
176
|
+
val margin = 8.dp
|
|
177
|
+
drawText(canvas, left, margin, y, color)
|
|
178
|
+
textPaint.color = color
|
|
179
|
+
canvas.drawText(right, width - margin - textPaint.measureText(right), y, textPaint)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun drawText(canvas: Canvas, text: String, x: Float, y: Float, color: Int) {
|
|
183
|
+
textPaint.color = color
|
|
184
|
+
canvas.drawText(text, x, y, textPaint)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun drawGraph(canvas: Canvas, x: Float, y: Float, graphWidth: Float, graphHeight: Float) {
|
|
188
|
+
canvas.drawRect(x, y, x + graphWidth, y + graphHeight, gridPaint)
|
|
189
|
+
for (index in 1..3) {
|
|
190
|
+
val gridY = y + index * (graphHeight / 4f)
|
|
191
|
+
canvas.drawLine(x + 2.dp, gridY, x + graphWidth - 2.dp, gridY, gridPaint)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var gridX = x + 16.dp
|
|
195
|
+
while (gridX < x + graphWidth) {
|
|
196
|
+
canvas.drawLine(gridX, y, gridX, y + graphHeight, gridPaint)
|
|
197
|
+
gridX += 16.dp
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
linePaint.color = fpsColor
|
|
201
|
+
drawDataLine(canvas, fpsValues, x, y, graphWidth, graphHeight, 60f)
|
|
202
|
+
linePaint.color = cpuColor
|
|
203
|
+
drawDataLine(canvas, cpuTimeValues, x, y, graphWidth, graphHeight, 50f)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private fun drawDataLine(
|
|
207
|
+
canvas: Canvas,
|
|
208
|
+
values: List<Float>,
|
|
209
|
+
x: Float,
|
|
210
|
+
y: Float,
|
|
211
|
+
graphWidth: Float,
|
|
212
|
+
graphHeight: Float,
|
|
213
|
+
maxScale: Float,
|
|
214
|
+
) {
|
|
215
|
+
if (values.size < 2) return
|
|
216
|
+
val visibleValues = values.takeLast(max((graphWidth / 2.dp).toInt(), 2))
|
|
217
|
+
val step = (graphWidth - 4.dp) / (visibleValues.size - 1).coerceAtLeast(1)
|
|
218
|
+
var previousX = x + 2.dp
|
|
219
|
+
var previousY = y + graphHeight * (1f - visibleValues.first().coerceIn(0f, maxScale) / maxScale)
|
|
220
|
+
|
|
221
|
+
visibleValues.drop(1).forEachIndexed { index, value ->
|
|
222
|
+
val pointX = x + 2.dp + (index + 1) * step
|
|
223
|
+
val pointY = y + graphHeight * (1f - min(max(value / maxScale, 0f), 1f))
|
|
224
|
+
canvas.drawLine(previousX, previousY, pointX, pointY, linePaint)
|
|
225
|
+
previousX = pointX
|
|
226
|
+
previousY = pointY
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private fun append(values: MutableList<Float>, value: Float) {
|
|
231
|
+
if (values.size >= maxPoints) values.removeAt(0)
|
|
232
|
+
values.add(value)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private fun minMax(values: List<Float>, fallback: Float): Pair<Float, Float> {
|
|
236
|
+
return Pair(values.minOrNull() ?: fallback, values.maxOrNull() ?: fallback)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private val Int.dp: Float
|
|
240
|
+
get() = toFloat().dp
|
|
241
|
+
|
|
242
|
+
private val Float.dp: Float
|
|
243
|
+
get() = this * density
|
|
244
|
+
|
|
245
|
+
private val Int.sp: Float
|
|
246
|
+
get() = toFloat().sp
|
|
247
|
+
|
|
248
|
+
private val Float.sp: Float
|
|
249
|
+
get() = this * scaledDensity
|
|
250
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package vn.momo.kits.fpsmonitor
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import android.util.DisplayMetrics
|
|
9
|
+
import android.view.Choreographer
|
|
10
|
+
import android.view.ViewGroup
|
|
11
|
+
import java.io.BufferedReader
|
|
12
|
+
import java.io.FileReader
|
|
13
|
+
|
|
14
|
+
data class FPSMetrics(
|
|
15
|
+
val fps: Float,
|
|
16
|
+
val avgFrameTimeMs: Float,
|
|
17
|
+
val estCPUTimePerFrameMs: Float,
|
|
18
|
+
val estGPUTimePerFrameMs: Float,
|
|
19
|
+
val appMemoryMB: Float,
|
|
20
|
+
val cpuUsagePercent: Float,
|
|
21
|
+
val deviceInfo: FPSDeviceInfo,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
data class FPSDeviceInfo(
|
|
25
|
+
val deviceName: String,
|
|
26
|
+
val resolution: String,
|
|
27
|
+
val density: Float,
|
|
28
|
+
val refreshRate: Float,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
object FPSMonitor {
|
|
32
|
+
var updateIntervalMs: Long = 200L
|
|
33
|
+
var fpsThreshold: Float = 30f
|
|
34
|
+
var ramThresholdMB: Float = 500f
|
|
35
|
+
var onFPSLow: ((Float) -> Unit)? = null
|
|
36
|
+
var onRamOverload: ((Float) -> Unit)? = null
|
|
37
|
+
|
|
38
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
39
|
+
private var appContext: Context? = null
|
|
40
|
+
private var choreographer: Choreographer? = null
|
|
41
|
+
private var frameCallback: Choreographer.FrameCallback? = null
|
|
42
|
+
private var overlayView: FPSOverlayView? = null
|
|
43
|
+
private var overlayParent: ViewGroup? = null
|
|
44
|
+
private var initialized = false
|
|
45
|
+
private var running = false
|
|
46
|
+
private var visible = false
|
|
47
|
+
private var lastTimestampMs = 0L
|
|
48
|
+
private var frameCount = 0
|
|
49
|
+
private var lastFPSAlertMs = 0L
|
|
50
|
+
private var cachedDeviceInfo: FPSDeviceInfo? = null
|
|
51
|
+
private var latestMetrics: FPSMetrics? = null
|
|
52
|
+
|
|
53
|
+
@JvmStatic
|
|
54
|
+
fun init(context: Context) {
|
|
55
|
+
if (initialized) return
|
|
56
|
+
appContext = context.applicationContext
|
|
57
|
+
ramThresholdMB = if (isEmulator()) 1500f else 500f
|
|
58
|
+
cachedDeviceInfo = readDeviceInfo(context)
|
|
59
|
+
initialized = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@JvmStatic
|
|
63
|
+
fun startOverlay(activity: Activity) {
|
|
64
|
+
init(activity)
|
|
65
|
+
start()
|
|
66
|
+
showUI(activity)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@JvmStatic
|
|
70
|
+
fun start() {
|
|
71
|
+
ensureInit()
|
|
72
|
+
if (running) return
|
|
73
|
+
running = true
|
|
74
|
+
runOnMain { startChoreographer() }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@JvmStatic
|
|
78
|
+
fun stop() {
|
|
79
|
+
runOnMain {
|
|
80
|
+
hideUI()
|
|
81
|
+
frameCallback?.let { choreographer?.removeFrameCallback(it) }
|
|
82
|
+
frameCallback = null
|
|
83
|
+
choreographer = null
|
|
84
|
+
lastTimestampMs = 0L
|
|
85
|
+
frameCount = 0
|
|
86
|
+
latestMetrics = null
|
|
87
|
+
lastFPSAlertMs = 0L
|
|
88
|
+
running = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@JvmStatic
|
|
93
|
+
fun showUI(activity: Activity) {
|
|
94
|
+
init(activity)
|
|
95
|
+
runOnMain {
|
|
96
|
+
val parent = activity.findViewById<ViewGroup>(android.R.id.content) ?: return@runOnMain
|
|
97
|
+
val view = overlayView ?: FPSOverlayView(activity).also { overlayView = it }
|
|
98
|
+
|
|
99
|
+
if (view.parent !== parent) {
|
|
100
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
101
|
+
parent.addView(view, FPSOverlayView.defaultLayoutParams())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
overlayParent = parent
|
|
105
|
+
view.visibility = android.view.View.VISIBLE
|
|
106
|
+
latestMetrics?.let { view.updateMetrics(it) }
|
|
107
|
+
visible = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@JvmStatic
|
|
112
|
+
fun hideUI() {
|
|
113
|
+
runOnMain {
|
|
114
|
+
overlayView?.let { view ->
|
|
115
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
116
|
+
view.visibility = android.view.View.GONE
|
|
117
|
+
}
|
|
118
|
+
overlayParent = null
|
|
119
|
+
visible = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@JvmStatic
|
|
124
|
+
fun metricsSnapshot(): FPSMetrics? = latestMetrics
|
|
125
|
+
|
|
126
|
+
@JvmStatic
|
|
127
|
+
fun isRunning(): Boolean = running
|
|
128
|
+
|
|
129
|
+
private fun startChoreographer() {
|
|
130
|
+
choreographer = Choreographer.getInstance()
|
|
131
|
+
frameCallback = object : Choreographer.FrameCallback {
|
|
132
|
+
override fun doFrame(frameTimeNanos: Long) {
|
|
133
|
+
val now = System.currentTimeMillis()
|
|
134
|
+
if (lastTimestampMs == 0L) {
|
|
135
|
+
lastTimestampMs = now
|
|
136
|
+
choreographer?.postFrameCallback(this)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
frameCount++
|
|
141
|
+
val delta = now - lastTimestampMs
|
|
142
|
+
if (delta >= updateIntervalMs) {
|
|
143
|
+
publishMetrics(now, delta)
|
|
144
|
+
lastTimestampMs = now
|
|
145
|
+
frameCount = 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
choreographer?.postFrameCallback(this)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
choreographer?.postFrameCallback(frameCallback!!)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun publishMetrics(now: Long, delta: Long) {
|
|
155
|
+
val fps = frameCount * 1000f / delta.coerceAtLeast(1)
|
|
156
|
+
val frameTimeMs = delta.toFloat() / frameCount.coerceAtLeast(1)
|
|
157
|
+
val fpsVariation = (60f - fps.coerceAtMost(60f)) / 60f
|
|
158
|
+
val gpuRatio = 0.4f + fpsVariation * 0.3f
|
|
159
|
+
val cpuPercent = getCPUUsagePercent()
|
|
160
|
+
val memoryMB = getAppMemoryUsageMB()
|
|
161
|
+
|
|
162
|
+
val metrics = FPSMetrics(
|
|
163
|
+
fps = fps,
|
|
164
|
+
avgFrameTimeMs = frameTimeMs,
|
|
165
|
+
estCPUTimePerFrameMs = frameTimeMs * cpuPercent.coerceIn(0f, 100f) / 100f,
|
|
166
|
+
estGPUTimePerFrameMs = frameTimeMs * gpuRatio,
|
|
167
|
+
appMemoryMB = memoryMB,
|
|
168
|
+
cpuUsagePercent = cpuPercent,
|
|
169
|
+
deviceInfo = cachedDeviceInfo ?: readDeviceInfo(appContext ?: return),
|
|
170
|
+
)
|
|
171
|
+
latestMetrics = metrics
|
|
172
|
+
|
|
173
|
+
if (visible) overlayView?.updateMetrics(metrics)
|
|
174
|
+
|
|
175
|
+
if (memoryMB > ramThresholdMB) {
|
|
176
|
+
ramThresholdMB += 100f
|
|
177
|
+
onRamOverload?.invoke(memoryMB)
|
|
178
|
+
}
|
|
179
|
+
if (fps < fpsThreshold && now - lastFPSAlertMs >= 1000L) {
|
|
180
|
+
onFPSLow?.invoke(fps)
|
|
181
|
+
lastFPSAlertMs = now
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun getAppMemoryUsageMB(): Float {
|
|
186
|
+
val runtime = Runtime.getRuntime()
|
|
187
|
+
val used = runtime.totalMemory() - runtime.freeMemory()
|
|
188
|
+
return used / (1024f * 1024f)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun getCPUUsagePercent(): Float {
|
|
192
|
+
return try {
|
|
193
|
+
BufferedReader(FileReader("/proc/stat")).use { reader ->
|
|
194
|
+
val parts = reader.readLine()
|
|
195
|
+
?.split(Regex("\\s+"))
|
|
196
|
+
?.filter { it.isNotBlank() }
|
|
197
|
+
?: return 0f
|
|
198
|
+
val idle = parts.getOrNull(4)?.toLongOrNull() ?: return 0f
|
|
199
|
+
val total = parts.drop(1).take(7).sumOf { it.toLongOrNull() ?: 0L }
|
|
200
|
+
if (total <= 0) 0f else ((total - idle) * 100f) / total
|
|
201
|
+
}
|
|
202
|
+
} catch (_: Exception) {
|
|
203
|
+
0f
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private fun readDeviceInfo(context: Context): FPSDeviceInfo {
|
|
208
|
+
val displayMetrics = context.resources.displayMetrics
|
|
209
|
+
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as? android.view.WindowManager
|
|
210
|
+
val metrics = DisplayMetrics()
|
|
211
|
+
@Suppress("DEPRECATION")
|
|
212
|
+
windowManager?.defaultDisplay?.getMetrics(metrics)
|
|
213
|
+
|
|
214
|
+
val width = if (metrics.widthPixels > 0) metrics.widthPixels else displayMetrics.widthPixels
|
|
215
|
+
val height = if (metrics.heightPixels > 0) metrics.heightPixels else displayMetrics.heightPixels
|
|
216
|
+
@Suppress("DEPRECATION")
|
|
217
|
+
val refreshRate = windowManager?.defaultDisplay?.refreshRate ?: 60f
|
|
218
|
+
|
|
219
|
+
return FPSDeviceInfo(
|
|
220
|
+
deviceName = "${Build.MANUFACTURER} ${Build.MODEL}",
|
|
221
|
+
resolution = "${width}x${height}",
|
|
222
|
+
density = displayMetrics.density,
|
|
223
|
+
refreshRate = refreshRate,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun isEmulator(): Boolean {
|
|
228
|
+
return Build.FINGERPRINT.startsWith("generic") ||
|
|
229
|
+
Build.FINGERPRINT.startsWith("unknown") ||
|
|
230
|
+
Build.MODEL.contains("google_sdk", ignoreCase = true) ||
|
|
231
|
+
Build.MODEL.contains("Emulator", ignoreCase = true) ||
|
|
232
|
+
Build.MODEL.contains("Android SDK built for x86", ignoreCase = true) ||
|
|
233
|
+
Build.MANUFACTURER.contains("Genymotion", ignoreCase = true) ||
|
|
234
|
+
(Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) ||
|
|
235
|
+
Build.PRODUCT == "google_sdk"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private fun ensureInit() {
|
|
239
|
+
check(initialized) { "FPSMonitor.init(context) must be called before use." }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private fun runOnMain(block: () -> Unit) {
|
|
243
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
244
|
+
block()
|
|
245
|
+
} else {
|
|
246
|
+
mainHandler.post(block)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|