Skip to content

Commit f25236f

Browse files
committed
optimize bitmap usage
1 parent b23e9fe commit f25236f

File tree

1 file changed

+95
-45
lines changed

1 file changed

+95
-45
lines changed

app/src/main/java/io/github/codehasan/colorpicker/services/ColorPickerService.kt

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.os.Handler
2424
import android.os.IBinder
2525
import android.os.Looper
2626
import android.util.DisplayMetrics
27+
import android.util.Log
2728
import android.view.Gravity
2829
import android.view.MotionEvent
2930
import android.view.ViewGroup
@@ -76,6 +77,9 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
7677
private var bitmapWidth = 0
7778
private var bitmapHeight = 0
7879

80+
private var croppedBitmap: Bitmap? = null
81+
private var croppedBitmapSize = 0
82+
7983
// Logic Variables
8084
private var scanX = 0
8185
private var scanY = 0
@@ -143,9 +147,10 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
143147
}
144148

145149
if (resultCode == Activity.RESULT_OK && resultData != null) {
146-
ServiceState.setColorPickerRunning(true)
147150
setupWindows()
148151
startScreenCapture(resultCode, resultData)
152+
// Set state to true AFTER service is actually initialized
153+
ServiceState.setColorPickerRunning(true)
149154
} else {
150155
stopSelf()
151156
}
@@ -253,10 +258,12 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
253258
targetParams.x = initX + (event.rawX - touchX).toInt()
254259
targetParams.y = initY + (event.rawY - touchY).toInt()
255260

256-
checkDistanceAndRules()
261+
val magChanged = checkDistanceAndRules()
257262

258263
windowManager.updateViewLayout(targetLayout, targetParams)
259-
windowManager.updateViewLayout(magnifierLayout, magnifierParams)
264+
if (magChanged) {
265+
windowManager.updateViewLayout(magnifierLayout, magnifierParams)
266+
}
260267

261268
updateScanCoordinates()
262269
true
@@ -267,7 +274,7 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
267274
}
268275
}
269276

270-
private fun checkDistanceAndRules() {
277+
private fun checkDistanceAndRules(): Boolean {
271278
val tSize = targetLayout.width
272279
val mSize = magnifierLayout.width // Assuming square
273280

@@ -287,6 +294,9 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
287294
val mRadius = mSize / 2f
288295
val currentGap = centerDistance - tRadius - mRadius
289296

297+
val oldMagX = magnifierParams.x
298+
val oldMagY = magnifierParams.y
299+
290300
// RULE 1: TOWING (If gap > max, pull magnifier closer)
291301
if (currentGap > maxGapBetweenEdges) {
292302
val angle = atan2(dy, dx)
@@ -306,6 +316,8 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
306316
if (currentGap < minGapBetweenEdges) {
307317
repositionMagnifier(tx, ty, tRadius, mSize)
308318
}
319+
320+
return magnifierParams.x != oldMagX || magnifierParams.y != oldMagY
309321
}
310322

311323
private fun repositionMagnifier(tx: Float, ty: Float, tRadius: Float, mSize: Int) {
@@ -365,8 +377,11 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
365377

366378
@SuppressLint("ClickableViewAccessibility")
367379
private fun addMagnifierFineTuneListener() {
368-
var touchX = 0f;
380+
var touchX = 0f
369381
var touchY = 0f
382+
// Accumulate fractional movement for sub-pixel precision
383+
var accumulatedX = 0f
384+
var accumulatedY = 0f
370385

371386
magnifierView.setOnTouchListener { view, event ->
372387
view.onTouchEvent(event) // Allow clicking buttons
@@ -375,24 +390,39 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
375390
MotionEvent.ACTION_DOWN -> {
376391
touchX = event.rawX
377392
touchY = event.rawY
393+
accumulatedX = 0f
394+
accumulatedY = 0f
378395
true
379396
}
380397

381398
MotionEvent.ACTION_MOVE -> {
382399
val dx = event.rawX - touchX
383400
val dy = event.rawY - touchY
384401

385-
targetParams.x += (dx * 0.1f).toInt()
386-
targetParams.y += (dy * 0.1f).toInt()
402+
// Accumulate fractional movement
403+
accumulatedX += dx * 0.1f
404+
accumulatedY += dy * 0.1f
405+
406+
// Apply integer portion, keep fractional part
407+
val moveX = accumulatedX.toInt()
408+
val moveY = accumulatedY.toInt()
409+
accumulatedX -= moveX
410+
accumulatedY -= moveY
411+
412+
targetParams.x += moveX
413+
targetParams.y += moveY
387414

388415
// Check rules even during fine tuning to keep constraints valid
389-
checkDistanceAndRules()
416+
val magChanged = checkDistanceAndRules()
390417

391418
windowManager.updateViewLayout(targetLayout, targetParams)
392-
windowManager.updateViewLayout(
393-
magnifierLayout,
394-
magnifierParams
395-
) // Update mag if towed
419+
// Only update magnifier if its position actually changed
420+
if (magChanged) {
421+
windowManager.updateViewLayout(
422+
magnifierLayout,
423+
magnifierParams
424+
) // Update mag if towed
425+
}
396426
updateScanCoordinates()
397427

398428
touchX = event.rawX
@@ -466,51 +496,68 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
466496

467497
private fun captureLoop() {
468498
if (!isCapturing) return
469-
imageReader?.acquireLatestImage()?.let { processImage(it); it.close() }
499+
try {
500+
imageReader?.acquireLatestImage()?.let { processImage(it) }
501+
} catch (e: Exception) {
502+
Log.e("ColorPickerService", "Failed to process image", e)
503+
}
470504
handler.postDelayed({ captureLoop() }, captureDelayMs)
471505
}
472506

473507
private fun processImage(image: Image) {
474-
val planes = image.planes
475-
val buffer = planes[0].buffer
476-
val pixelStride = planes[0].pixelStride
477-
val rowStride = planes[0].rowStride
478-
val rowPadding = rowStride - pixelStride * image.width
479-
480-
val requiredWidth = image.width + rowPadding / pixelStride
481-
val requiredHeight = image.height
482-
483-
var bitmap = screenBitmap
484-
if (bitmap == null || bitmap.width != requiredWidth || bitmap.height != requiredHeight) {
485-
bitmap = createBitmap(requiredWidth, requiredHeight).also {
486-
screenBitmap?.recycle()
487-
screenBitmap = it
488-
bitmapWidth = requiredWidth
489-
bitmapHeight = requiredHeight
508+
try {
509+
val planes = image.planes
510+
val buffer = planes[0].buffer
511+
val pixelStride = planes[0].pixelStride
512+
val rowStride = planes[0].rowStride
513+
val rowPadding = rowStride - pixelStride * image.width
514+
515+
val requiredWidth = image.width + rowPadding / pixelStride
516+
val requiredHeight = image.height
517+
518+
var bitmap = screenBitmap
519+
if (bitmap == null || bitmap.width != requiredWidth || bitmap.height != requiredHeight) {
520+
bitmap = createBitmap(requiredWidth, requiredHeight).also {
521+
screenBitmap?.recycle()
522+
screenBitmap = it
523+
bitmapWidth = requiredWidth
524+
bitmapHeight = requiredHeight
525+
}
490526
}
491-
}
492527

493-
bitmap.copyPixelsFromBuffer(buffer)
528+
bitmap.copyPixelsFromBuffer(buffer)
494529

495-
val safeX = scanX.coerceIn(0, bitmap.width - 1)
496-
val safeY = scanY.coerceIn(0, bitmap.height - 1)
530+
val safeX = scanX.coerceIn(0, bitmap.width - 1)
531+
val safeY = scanY.coerceIn(0, bitmap.height - 1)
497532

498-
val pixelColor = bitmap[safeX, safeY]
499-
val hexColor = String.format("#%06X", (0xFFFFFF and pixelColor))
533+
val pixelColor = bitmap[safeX, safeY]
534+
val hexColor = String.format("#%06X", (0xFFFFFF and pixelColor))
500535

501-
val cropSize = targetView.getSafeCropSize()
536+
val cropSize = targetView.getSafeCropSize()
502537

503-
val cropX = (safeX - cropSize / 2).coerceIn(0, bitmap.width - cropSize)
504-
val cropY = (safeY - cropSize / 2).coerceIn(0, bitmap.height - cropSize)
538+
val cropX = (safeX - cropSize / 2).coerceIn(0, bitmap.width - cropSize)
539+
val cropY = (safeY - cropSize / 2).coerceIn(0, bitmap.height - cropSize)
505540

506-
val croppedBitmap = Bitmap.createBitmap(
507-
bitmap,
508-
cropX, cropY,
509-
cropSize, cropSize
510-
)
541+
// Reuse cropped bitmap to avoid allocation on every frame
542+
var crop = croppedBitmap
543+
if (crop == null || crop.width != cropSize || crop.height != cropSize) {
544+
crop = createBitmap(cropSize, cropSize).also {
545+
croppedBitmap?.recycle()
546+
croppedBitmap = it
547+
croppedBitmapSize = cropSize
548+
}
549+
}
550+
551+
// Extract just the crop region
552+
val pixels = IntArray(cropSize * cropSize)
553+
bitmap.getPixels(pixels, 0, cropSize, cropX, cropY, cropSize, cropSize)
554+
crop.setPixels(pixels, 0, cropSize, 0, 0, cropSize, cropSize)
511555

512-
handler.post {
513-
magnifierView.updateContent(croppedBitmap, hexColor, safeX, safeY)
556+
handler.post {
557+
magnifierView.updateContent(crop, hexColor, safeX, safeY)
558+
}
559+
} finally {
560+
image.close()
514561
}
515562
}
516563

@@ -541,6 +588,9 @@ class ColorPickerService : Service(), MagnifierView.OnInteractionListener {
541588
screenBitmap?.recycle()
542589
screenBitmap = null
543590

591+
croppedBitmap?.recycle()
592+
croppedBitmap = null
593+
544594
if (::targetLayout.isInitialized) windowManager.removeView(targetLayout)
545595
if (::magnifierLayout.isInitialized) windowManager.removeView(magnifierLayout)
546596

0 commit comments

Comments
 (0)