|
|
|
@@ -0,0 +1,135 @@
|
|
|
|
|
// Copyright (c) 2026 Hans Kokx.
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package com.hanskokx.custom_cursor
|
|
|
|
|
|
|
|
|
|
import android.app.Activity
|
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
|
import android.graphics.BitmapFactory
|
|
|
|
|
import android.os.Build
|
|
|
|
|
import android.view.View
|
|
|
|
|
import android.view.ViewGroup
|
|
|
|
|
import android.view.PointerIcon
|
|
|
|
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
|
|
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
|
|
|
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
|
|
|
|
import io.flutter.plugin.common.MethodCall
|
|
|
|
|
import io.flutter.plugin.common.MethodChannel
|
|
|
|
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
|
|
|
|
import io.flutter.plugin.common.MethodChannel.Result
|
|
|
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
|
|
|
|
/** CustomCursorPlugin */
|
|
|
|
|
class CustomCursorPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
|
|
|
|
|
private var channel: MethodChannel? = null
|
|
|
|
|
private var activity: Activity? = null
|
|
|
|
|
private val cursors = mutableMapOf<String, PointerIcon>()
|
|
|
|
|
|
|
|
|
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
|
|
|
channel = MethodChannel(binding.binaryMessenger, CHANNEL)
|
|
|
|
|
channel?.setMethodCallHandler(this)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
|
|
|
channel?.setMethodCallHandler(null)
|
|
|
|
|
channel = null
|
|
|
|
|
cursors.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
|
|
|
|
activity = binding.activity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDetachedFromActivity() {
|
|
|
|
|
activity = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
|
|
|
|
onAttachedToActivity(binding)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDetachedFromActivityForConfigChanges() {
|
|
|
|
|
onDetachedFromActivity()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
|
|
|
|
when (call.method) {
|
|
|
|
|
"registerCursor" -> handleRegisterCursor(call)
|
|
|
|
|
"setCursor" -> handleSetCursor(call)
|
|
|
|
|
"systemCursorScale" -> result.success(1.0)
|
|
|
|
|
"registerScaleObserver" -> result.success(null)
|
|
|
|
|
"systemDirection" -> result.success("ltr")
|
|
|
|
|
"registerDirectionObserver" -> result.success(null)
|
|
|
|
|
"resetCursor" -> handleResetCursor()
|
|
|
|
|
else -> result.notImplemented()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleRegisterCursor(call: MethodCall) {
|
|
|
|
|
val args = call.arguments as? Map<*, *> ?: return
|
|
|
|
|
val name = args["name"] as? String ?: return
|
|
|
|
|
val pngBytes = args["png"] as? ByteArray ?: return
|
|
|
|
|
val hotspotX = (args["hotspotX"] as? Number)?.toInt() ?: 0
|
|
|
|
|
val hotspotY = (args["hotspotY"] as? Number)?.toInt() ?: 0
|
|
|
|
|
|
|
|
|
|
val original = BitmapFactory.decodeByteArray(pngBytes, 0, pngBytes.size) ?: return
|
|
|
|
|
val density = activity?.resources?.displayMetrics?.density ?: 1.0f
|
|
|
|
|
val bitmap = scaleByDensity(original, density)
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
|
|
|
cursors[name] =
|
|
|
|
|
PointerIcon.create(
|
|
|
|
|
bitmap,
|
|
|
|
|
(hotspotX * (bitmap.width.toFloat() / original.width)).toFloat(),
|
|
|
|
|
(hotspotY * (bitmap.height.toFloat() / original.height)).toFloat(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleSetCursor(call: MethodCall) {
|
|
|
|
|
val args = call.arguments as? Map<*, *> ?: return
|
|
|
|
|
val name = args["name"] as? String ?: return
|
|
|
|
|
val icon = cursors[name]
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
|
|
|
setPointerIcon(icon)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleResetCursor() {
|
|
|
|
|
cursors.clear()
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
|
|
|
setPointerIcon(null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun setPointerIcon(icon: PointerIcon?) {
|
|
|
|
|
val views = mutableListOf<View>()
|
|
|
|
|
val decor = activity?.window?.decorView ?: return
|
|
|
|
|
collectHostViews(decor, views)
|
|
|
|
|
for (view in views) {
|
|
|
|
|
view.pointerIcon = icon
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun collectHostViews(view: View, out: MutableList<View>) {
|
|
|
|
|
val name = view.javaClass.name
|
|
|
|
|
if (name == "io.flutter.embedding.android.FlutterView" || name.endsWith("SurfaceView")) {
|
|
|
|
|
out.add(view)
|
|
|
|
|
}
|
|
|
|
|
if (view is ViewGroup) {
|
|
|
|
|
for (i in 0 until view.childCount) {
|
|
|
|
|
collectHostViews(view.getChildAt(i), out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun scaleByDensity(bitmap: Bitmap, density: Float): Bitmap {
|
|
|
|
|
val width = (bitmap.width * density).roundToInt()
|
|
|
|
|
val height = (bitmap.height * density).roundToInt()
|
|
|
|
|
if (width == bitmap.width && height == bitmap.height) return bitmap
|
|
|
|
|
return Bitmap.createScaledBitmap(bitmap, width, height, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
private const val CHANNEL = "custom_cursor"
|
|
|
|
|
}
|
|
|
|
|
}
|