package com.rnwallpapers import android.content.Context import android.graphics.* import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.service.wallpaper.WallpaperService import android.view.SurfaceHolder import android.graphics.BitmapFactory import java.io.File class ParallaxWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine = ParallaxEngine() inner class ParallaxEngine : Engine(), SensorEventListener { private var sensorManager: SensorManager? = null private var gyroscope: Sensor? = null // Bitmaps private var bgBitmap: Bitmap? = null private var objBitmap: Bitmap? = null // Parallax offsets — gyroscope drives these private var offsetX = 0f private var offsetY = 0f private var targetOffsetX = 0f private var targetOffsetY = 0f // Object offsets — move more than background for depth parallax private var objOffsetX = 0f private var objOffsetY = 0f private var targetObjOffsetX = 0f private var targetObjOffsetY = 0f // Object transform state — rotation + skew for realism private var objRotation = 0f private val drawRunnable = Runnable { draw() } private val handler = android.os.Handler(android.os.Looper.getMainLooper()) private val prefs by lazy { applicationContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE) } // ── Lifecycle ──────────────────────────────────────────────────────── override fun onCreate(holder: SurfaceHolder) { super.onCreate(holder) loadBitmaps() setupSensor() } override fun onDestroy() { sensorManager?.unregisterListener(this) handler.removeCallbacks(drawRunnable) bgBitmap?.recycle() objBitmap?.recycle() bgBitmap = null objBitmap = null super.onDestroy() } override fun onVisibilityChanged(visible: Boolean) { if (visible) { gyroscope?.let { sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } scheduleDrawLoop() } else { sensorManager?.unregisterListener(this) handler.removeCallbacks(drawRunnable) } } override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { super.onSurfaceChanged(holder, format, width, height) loadBitmaps() draw() } // ── Bitmap loading ──────────────────────────────────────────────────── private fun loadBitmaps() { val sw = surfaceHolder.surfaceFrame.width().takeIf { it > 0 } ?: applicationContext.resources.displayMetrics.widthPixels val sh = surfaceHolder.surfaceFrame.height().takeIf { it > 0 } ?: applicationContext.resources.displayMetrics.heightPixels // Load background — wider than screen for scroll room val bgSource = prefs.getString("parallax_source", null) val intensity = prefs.getFloat("parallax_intensity", 0.5f) if (bgSource != null) { bgBitmap = try { val raw = decodeBitmap(bgSource) val w = (sw * (1f + intensity * 0.6f)).toInt() Bitmap.createScaledBitmap(raw, w, sh, true) } catch (e: Exception) { null } } // Load foreground object (optional) val objSource = prefs.getString("parallax_object", "").takeIf { !it.isNullOrBlank() } if (objSource != null) { objBitmap = try { val raw = decodeBitmap(objSource) val objW = (sw * 0.55f).toInt() val objH = (raw.height.toFloat() / raw.width * objW).toInt() Bitmap.createScaledBitmap(raw, objW, objH, true) } catch (e: Exception) { null } } } private fun decodeBitmap(path: String): Bitmap { val clean = path.removePrefix("file://") return BitmapFactory.decodeFile(clean) ?: throw Exception("Cannot decode: $clean") } // ── Sensor setup ────────────────────────────────────────────────────── private fun setupSensor() { sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) gyroscope?.let { sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } } // ── Sensor callback ─────────────────────────────────────────────────── override fun onSensorChanged(event: SensorEvent) { if (event.sensor.type != Sensor.TYPE_GYROSCOPE) return val intensity = prefs.getFloat("parallax_intensity", 0.5f) val maxOffset = prefs.getInt("parallax_max_offset", 40).toFloat() // Gyroscope values: [0]=x(pitch), [1]=y(roll), [2]=z(yaw) in rad/s val rollRate = event.values[1] // left-right tilt val pitchRate = event.values[0] // up-down tilt // Background moves subtly targetOffsetX += rollRate * intensity * 12f targetOffsetY += pitchRate * intensity * 6f targetOffsetX = targetOffsetX.coerceIn(-maxOffset, maxOffset) targetOffsetY = targetOffsetY.coerceIn(-maxOffset * 0.4f, maxOffset * 0.4f) // Foreground object moves 2.5x more for depth effect targetObjOffsetX += rollRate * intensity * 30f targetObjOffsetY += pitchRate * intensity * 15f targetObjOffsetX = targetObjOffsetX.coerceIn(-maxOffset * 2f, maxOffset * 2f) targetObjOffsetY = targetObjOffsetY.coerceIn(-maxOffset, maxOffset) // Rotate object slightly with roll for realism objRotation += rollRate * intensity * 0.8f objRotation = objRotation.coerceIn(-8f, 8f) } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} // ── Draw loop ───────────────────────────────────────────────────────── private fun scheduleDrawLoop() { handler.postDelayed(drawRunnable, 16) // ~60fps } private fun draw() { val holder = surfaceHolder val canvas = try { holder.lockCanvas() } catch (e: Exception) { null } ?: return try { val sw = canvas.width.toFloat() val sh = canvas.height.toFloat() // Smooth interpolation — ease toward target offsetX += (targetOffsetX - offsetX) * 0.07f offsetY += (targetOffsetY - offsetY) * 0.07f objOffsetX += (targetObjOffsetX - objOffsetX) * 0.05f objOffsetY += (targetObjOffsetY - objOffsetY) * 0.05f // Clear canvas.drawColor(Color.BLACK) // Draw background — centered + parallax offset bgBitmap?.let { bg -> val left = (sw - bg.width) / 2f + offsetX val top = (sh - bg.height) / 2f + offsetY canvas.drawBitmap(bg, left, top, null) } // Draw foreground object with transform objBitmap?.let { obj -> val cx = sw / 2f + objOffsetX val cy = sh * 0.35f + objOffsetY val matrix = Matrix() // Pivot from center of object matrix.setTranslate(-obj.width / 2f, -obj.height / 2f) // Apply slight skew for 3D perspective feel val skewMatrix = Matrix() skewMatrix.setSkew( rollRate() * 0.04f, pitchRate() * 0.02f ) matrix.postConcat(skewMatrix) // Apply rotation matrix.postRotate(objRotation) // Move to position matrix.postTranslate(cx, cy) val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) canvas.drawBitmap(obj, matrix, paint) } } finally { try { holder.unlockCanvasAndPost(canvas) } catch (e: Exception) {} } // Schedule next frame if (isVisible) scheduleDrawLoop() } private fun rollRate() = (targetObjOffsetX / 60f).coerceIn(-1f, 1f) private fun pitchRate() = (targetObjOffsetY / 60f).coerceIn(-1f, 1f) } }