import android.os.Bundle | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.LifecycleRegistry | |
import androidx.savedstate.SavedStateRegistry | |
import androidx.savedstate.SavedStateRegistryController | |
import androidx.savedstate.SavedStateRegistryOwner | |
internal class MyLifecycleOwner : SavedStateRegistryOwner { | |
private var mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) | |
private var mSavedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this) | |
/** | |
* @return True if the Lifecycle has been initialized. | |
*/ | |
val isInitialized: Boolean | |
get() = true | |
override fun getLifecycle(): Lifecycle { | |
return mLifecycleRegistry | |
} | |
fun setCurrentState(state: Lifecycle.State) { | |
mLifecycleRegistry.currentState = state | |
} | |
fun handleLifecycleEvent(event: Lifecycle.Event) { | |
mLifecycleRegistry.handleLifecycleEvent(event) | |
} | |
override fun getSavedStateRegistry(): SavedStateRegistry { | |
return mSavedStateRegistryController.savedStateRegistry | |
} | |
fun performRestore(savedState: Bundle?) { | |
mSavedStateRegistryController.performRestore(savedState) | |
} | |
fun performSave(outBundle: Bundle) { | |
mSavedStateRegistryController.performSave(outBundle) | |
} | |
} |
import android.app.AlertDialog | |
import android.app.Service | |
import android.content.Intent | |
import android.graphics.PixelFormat | |
import android.os.Build | |
import android.os.IBinder | |
import android.util.TypedValue | |
import android.view.Window | |
import android.view.WindowManager | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.wrapContentSize | |
import androidx.compose.material.Text | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.platform.ComposeView | |
import androidx.compose.ui.unit.sp | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.ViewModelStore | |
import androidx.lifecycle.ViewTreeLifecycleOwner | |
import androidx.lifecycle.ViewTreeViewModelStoreOwner | |
import androidx.savedstate.ViewTreeSavedStateRegistryOwner | |
import com.viatek.fitnation.echelon_android.R | |
class OverlayService : Service() { | |
val windowManager get() = getSystemService(WINDOW_SERVICE) as WindowManager | |
override fun onCreate() { | |
super.onCreate() | |
setTheme(R.style.ThemeOverlay_AppCompat_Light) | |
showOverlay() | |
} | |
private fun showOverlay() { | |
val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY | |
} else { | |
WindowManager.LayoutParams.TYPE_PHONE | |
} | |
val params = WindowManager.LayoutParams( | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
layoutFlag, | |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | |
PixelFormat.TRANSLUCENT | |
) | |
val composeView = ComposeView(this) | |
composeView.setContent { | |
Text( | |
text = "Hello", | |
color = Color.Black, | |
fontSize = 50.sp, | |
modifier = Modifier | |
.wrapContentSize() | |
.background(Color.Green) | |
) | |
} | |
// Trick The ComposeView into thinking we are tracking lifecycle | |
val viewModelStore = ViewModelStore() | |
val lifecycleOwner = MyLifecycleOwner() | |
lifecycleOwner.performRestore(null) | |
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) | |
ViewTreeLifecycleOwner.set(composeView, lifecycleOwner) | |
ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore } | |
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner) | |
windowManager.addView(composeView, params) | |
} | |
override fun onBind(intent: Intent): IBinder? { | |
return null | |
} | |
} |
Thank you for this! :)
I dont know if you should provide a proper LifecycleOwner
such as LifecycleService in androidx.lifecycle:lifecycle-service:2.4.0
, thats what I ended up doing at least!
Thanks @MathewSachin! I just got back to this project, added your code, and it worked again now with Compose 1.0.5!
@zoltish - I can take a look at that proper lifecycle owner for the service. Thank you!!!
I pulled in LifecycleService and extended from it. Do you have an example of what is different @zoltish? Thanks!
@handstandsam Awesome! I havent digged too deep into it really - but considering that the service goes through a series of lifecycle-events, and compose-view requires a proper lifecycle-owner... It seems like a good idea to do it!
Thank you for this! :)
Thanks for this, I used this code to create a floating timer https://github.com/tberghuis/FloatingCountdownTimer
Awesome!!!
The UI doesn't restructure when my mutableState changes, how to deal with this?
I've reviewed a bit you're implementation to be more generic, with a dedicated ui context (needed to access window manger from a background context for API 12) and using the lifecycle of the service, you can check it out here : https://gist.github.com/KONFeature/2f84436e1c0a1926505cac934d470f90
@KONFeature That looks nice, although Im curious about the dedicated ui context. I dont use that in my solution, yet it works perfectly fine on Android 12. Why would that be? Im just using the services (this) context.
@zoltish It will work, but not really perfectly, if you enable strict mode logging you will see that the system isn't happy to have a non UI context as context when building the view. The fact that it's a background thread will prevent the component (composable, window manager, layout inflater etc), to access UI constants ressources.
Extract of the strict mode error log with non ui context when building the ComposeView :
E/ViewConfiguration: Tried to access UI constants from a non-visual Context:...VideoCallOverlayService@8a2596aUI constants, such as display metrics or window metrics, must be accessed from Activity or other visual Context. Use an Activity or a Context created with Context#createWindowContext(int, Bundle), which are adjusted to the configuration and visual bounds of an area on screen
java.lang.IllegalArgumentException: Tried to access UI constants from a non-visual Context:...VideoCallOverlayService@8a2596a
at android.view.ViewConfiguration.get(ViewConfiguration.java:510)
at android.view.View.<init>(View.java:5317)
at android.view.View.<init>(View.java:5467)
at android.view.ViewGroup.<init>(ViewGroup.java:697)
at android.view.ViewGroup.<init>(ViewGroup.java:693)
at android.view.ViewGroup.<init>(ViewGroup.java:689)
at android.view.ViewGroup.<init>(ViewGroup.java:685)
at androidx.compose.ui.platform.AndroidComposeView.<init>(AndroidComposeView.android.kt:154)
You will get this log for the composable view creation, and and you will have a similar one for the window manager :)
@KONFeature Interesting! Thanks for letting me know about it :) Noteworthy: createWindowContext
is available on R and above.
Since lifecycle-viewmodel-savedstate 2.5.0, the line
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
needs to be changed to
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
They did some Kotlin refactoring.
In newer libraries versions , we have to replace
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
by
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
That's working for me! But the shadow is missing while I show my composable. I've tried adding extra padding in the parent of my composable, or in ComposeView
. The shadow works well when I put them into activities. Could anyone help me?
Here is my code:
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Overlay(
modifier: Modifier = Modifier,
onDrag: (offsetX: Float, offsetY: Float) -> Unit,
onClick: () -> Unit,
) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val drag = Modifier.pointerInput(Unit) {
detectDragGestures(onDragStart = {}, onDrag = { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
onDrag(offsetX, offsetY)
}, onDragEnd = {})
}
Box(
Modifier
.background(Color.White)
.zIndex(0f)
.padding(50.dp)
) {
Surface(
elevation = 10.dp,
shape = CircleShape,
onClick = onClick,
modifier = modifier.then(drag),
) {
Image(
painter = painterResource(id = R.drawable.avatar_green),
contentDescription = "assistant",
)
}
}
}
Nice work !!!
I am getting error: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@6c83604 -- permission denied for window type 2038.
Does anyone know how to fix?
@mairs8 Add <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
to your manifest
The UI doesn't restructure when my mutableState changes, how to deal with this?
@wilinz I have the same same problem. Have you solved it?
The UI doesn't restructure when my mutableState changes, how to deal with this?
@wilinz I have the same same problem. Have you solved it?
not
@wilinz , I have found the solution and created the question and answer on stackoverflow to help others that cannot find this info: https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused
I am getting error: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@6c83604 -- permission denied for window type 2038.
Does anyone know how to fix?
@mairs8
You have to add the permission to use Window Manager
https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused
That's working for me! But the shadow is missing while I show my composable. I've tried adding extra padding in the parent of my composable, or in
ComposeView
. The shadow works well when I put them into activities. Could anyone help me? Here is my code:@OptIn(ExperimentalMaterialApi::class) @Composable fun Overlay( modifier: Modifier = Modifier, onDrag: (offsetX: Float, offsetY: Float) -> Unit, onClick: () -> Unit, ) { var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } val drag = Modifier.pointerInput(Unit) { detectDragGestures(onDragStart = {}, onDrag = { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y onDrag(offsetX, offsetY) }, onDragEnd = {}) } Box( Modifier .background(Color.White) .zIndex(0f) .padding(50.dp) ) { Surface( elevation = 10.dp, shape = CircleShape, onClick = onClick, modifier = modifier.then(drag), ) { Image( painter = painterResource(id = R.drawable.avatar_green), contentDescription = "assistant", ) } } }
@NamekMaster Have you tried to add recomposer? https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused
Amazing works great!!
ViewTreeLifecycleOwner.set(composeView, lifecycleOwner)
ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore }
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
I can not import this by lifecycle version 2.6.1. Anyone fix it ?
@tungthanhss the api changed to extension functions on View
composeView.setViewTreeLifecycleOwner(lifecycleOwner)
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
composeView.setViewTreeViewModelStoreOwner(viewModelStoreOwner)
viewModelStoreOwner
thank you but how i can get viewModelStoreOwner
val viewModelStoreOwner = ViewModelStoreOwner { viewModelStore } not working now
val viewModelStore = ViewModelStore()
val viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore: ViewModelStore
get() = viewModelStore
}
Beta01 also needs a
compositionContext
.Adding this block of code seems to work for me for now:
Technically, we should also cancel the
runRecomposeScope
when the view is removed.