Skip to content

Instantly share code, notes, and snippets.

@MFlisar
Last active February 18, 2025 09:59
Show Gist options
  • Save MFlisar/bb886f0e4a3a2f0e55dc1423495eade3 to your computer and use it in GitHub Desktop.
Save MFlisar/bb886f0e4a3a2f0e55dc1423495eade3 to your computer and use it in GitHub Desktop.
Compose AppWidgetHostView example
// -----------------
// AppWidgetHostView
// -----------------
class LauncherAppWidgetHostView(context: Context) : AppWidgetHostView(context) {
private var hasPerformedLongPress: Boolean = false
private var pointDown = PointF()
private val pendingCheckForLongPress = CheckForLongPress()
private val pendingCheckTouched = CheckTouched()
private val threshold: Int = 5.dpToPx
var onLongPress: (() -> Unit)? = null
var onIsTouched: ((touched: Boolean) -> Unit)? = null
init {
// Hardware Acceleration - deactivate on android O or higher
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// setLayerType(View.LAYER_TYPE_SOFTWARE, null)
//}
}
override fun updateAppWidget(remoteViews: RemoteViews?) {
// can happen, no idea why (maybe if the widget itself has a bug?)... we better catch it to avoid that a widget can crash the whole app
try {
println("widget - updateAppWidget: $remoteViews | ${remoteViews?.`package`}")
super.updateAppWidget(remoteViews)
} catch (e: Exception) {
L.e(e)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// Consume any touch events for ourselves after longpress is triggered
if (hasPerformedLongPress) {
onIsTouched?.invoke(false)
hasPerformedLongPress = false
println("widget - hasPerformedLongPress was true!")
return true
}
//L.d { "onInterceptTouchEvent: ev = ${ev.action} | x = ${ev.x} | y = ${ev.y}" }
// Watch for longpress events at this level to make sure
// users can always pick up this widget
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
pointDown.set(ev.x, ev.y)
onIsTouched?.invoke(true)
postCheckForLongClick()
println("widget - action down")
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
hasPerformedLongPress = false
removeCallbacks(pendingCheckForLongPress)
onIsTouched?.invoke(false)
println("widget - action up/cancel")
}
MotionEvent.ACTION_MOVE -> {
val diffX = Math.abs(pointDown.x - ev.x)
val diffY = Math.abs(pointDown.y - ev.y)
//L.d { "onInterceptTouchEvent: diffX = $diffX | diffY = $diffY | mThreshold = $mThreshold" }
if (diffX >= threshold || diffY >= threshold) {
hasPerformedLongPress = false
removeCallbacks(pendingCheckForLongPress)
}
onIsTouched?.invoke(true)
postCheckTouched()
println("widget - action move | $ev")
}
else -> {
println("widget - action else")
}
}
// Otherwise continue letting touch events fall through to children
return false
}
internal inner class CheckForLongPress : Runnable {
private var originalWindowAttachCount: Int = 0
override fun run() {
println("widget - CheckForLongPress-run: $parent | $windowAttachCount | $originalWindowAttachCount | $hasPerformedLongPress")
if (parent != null
// hasWindowFocus()
&& originalWindowAttachCount == windowAttachCount
&& !hasPerformedLongPress
) {
println("widget - before performLongClick...")
//if (performLongClick()) {
onLongPress?.invoke()
println("widget - onLongPress")
hasPerformedLongPress = true
//}
}
}
fun rememberWindowAttachCount() {
originalWindowAttachCount = windowAttachCount
}
}
internal inner class CheckTouched : Runnable {
private var originalWindowAttachCount: Int = 0
override fun run() {
if (parent != null
// hasWindowFocus()
&& originalWindowAttachCount == windowAttachCount
) {
onIsTouched?.invoke(false)
}
}
fun rememberWindowAttachCount() {
originalWindowAttachCount = windowAttachCount
}
}
private fun postCheckTouched() {
removeCallbacks(pendingCheckTouched)
pendingCheckTouched.rememberWindowAttachCount()
postDelayed(pendingCheckTouched, 500)
}
private fun postCheckForLongClick() {
removeCallbacks(pendingCheckForLongPress)
hasPerformedLongPress = false
if (onLongPress == null) {
return
}
pendingCheckForLongPress.rememberWindowAttachCount()
postDelayed(pendingCheckForLongPress, ViewConfiguration.getLongPressTimeout().toLong())
}
override fun cancelLongPress() {
super.cancelLongPress()
hasPerformedLongPress = false
removeCallbacks(pendingCheckForLongPress)
}
//override fun getDescendantFocusability() = ViewGroup.FOCUS_BLOCK_DESCENDANTS
}
// -----------------
// Usage
// -----------------
val info : AppWidgetProviderInfo = ...
val widgetView : LauncherAppWidgetHostView = ...
val widgetRows: Int =
AndroidView(
modifier = Modifier
.fillMaxWidth()
.sizeIn(
minHeight = info.minHeight.pxToDp.dp,
minWidth = info.minWidth.pxToDp.dp
)
.height(heightInDp?.let { with(LocalDensity.current) { it.toDp() } } ?: (102.dp * widgetRows))
//.sizeIn(
// maxHeight = widget.appWidgetInfo.maxResizeHeight.pxToDp.dp,
// maxWidth = widget.appWidgetInfo.maxResizeWidth.pxToDp.dp
//)
.onSizeChanged {
widgetView.updateSize(it.width.pxToDp, it.height.pxToDp)
},
factory = { context -> widgetView },
update = { view ->
view.onLongPress = onLongPress
view.onIsTouched = { touched ->
// TODO: enable/disable scrolling in your parent container depending on this state
}
},
//onReset = { view ->
// view.onLongPress = null
// view.onIsTouched = null
//},
//onRelease = { view ->
// view.onLongPress = null
// view.onIsTouched = null
//}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment