@@ -24,6 +24,7 @@ import android.os.Handler
2424import android.os.IBinder
2525import android.os.Looper
2626import android.util.DisplayMetrics
27+ import android.util.Log
2728import android.view.Gravity
2829import android.view.MotionEvent
2930import 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