Last active
February 18, 2025 09:59
-
-
Save MFlisar/bb886f0e4a3a2f0e55dc1423495eade3 to your computer and use it in GitHub Desktop.
Compose AppWidgetHostView example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----------------- | |
// 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