Jetpack Compose : How to overlay Composable with AndroidView? - android

I'm new to Jetpack Compose and trying to figure out how to solve next task:
I need to create a simple transparent AndroidView. And it needs to be used as an overlay for Composable functions.
The problem is that an overlay should be the same size as a compose view under it.
I had some-kind of successful attempt with this:
#Composable
fun BugseeOverlayView() {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
View(ctx).apply {
layoutParams = LinearLayout.LayoutParams(200, 200) //Hardcoded size
alpha = 0.0F
}
}, update = {
Bugsee.addSecureView(it) // 3rd party I need to use
}
)
}
And then I used it like:
Box {
Box(Modifier.fillMaxSize()) {
BugseeOverlayView()
}
Text("Hide me") // or some 'CustomComposableView(param)'
}
This works, but the size is hardcoded.
PS. I need an AndroidView because of third-party tool which accepts android.view.View as a parameter.

You can get size of a Composable in various ways.
1- Modifier.onSizeChanged{intSize->} will return Composable size in pixels you can convert this to dp using LocalDensity.current.run{}. With this approach the size you set will change and there needs to be another recomposition. You can also get size of a Composable from Modifier.onGloballyPositioned either.
val density = LocalDensity.current
var dpSize: DpSize by remember{ mutableStateOf(DpSize.Zero) }
Modifier.onSizeChanged { size: IntSize ->
density.run { dpSize = DpSize(size.width.toDp(), size.height.toDp()) }
}
Modifier.onGloballyPositioned {layoutCoordinates: LayoutCoordinates ->
val size = layoutCoordinates.size
density.run { dpSize = DpSize(size.width.toDp(), size.height.toDp()) }
}
2- If the Composable has fixed size or covers screen you can use
BoxWithConstraints {
SomeComposable()
AndroidView(modifier=Modifier.size(maxWidth, maxHeight)
}
3- If you don't have chance to get Composable size and don't want to have another recomposition you can use SubcomposeLayout. Detailed answer is available here how to create a SubcomposeLayout to get exact size of a Composable without recomposition.
When you are able to get size of Composable you can set same size to AndroidView and set layout params to match parent. If that's not what you wish you can still set Modifier.fillMaxSize while using methods above to set layout params

Related

Jetpack Compose: Save composable size before configuration changes, then use that size after configuration changes

I am saving the size of a composable using the onSizeChanged modifier. I want to know what the size of the composable was during the previous configuration so that when the configuration changes, I can do a calculation with that size. However, I want to wait until my calculation finishes before I start saving the size again based on the new configuration.
Right now I'm passing the size to MyComposable. MyComposable is the only one who cares about what the size was during the last configuration (landscape or portrait), so I thought I would save the size within its scope. I have no idea when the configuration change happens, so I keep updating a variable oldSize whenever there is a new size to hopefully save the most recent value before the configuration changes.
Below is the code of how I manage the size within MyComposable. It seems to work, but is there a more straightforward way of doing this?
#Composable
fun MyComposable(
size: () -> IntSize,
) {
// save size across configurations
var oldSize by rememberSaveable(stateSaver = IntSizeSaver) {
mutableStateOf(IntSize.Zero)
}
var updateSize by remember { mutableStateOf(false) }
if (updateSize) {
LaunchedEffect(size()) { oldSize = size() }
}
LaunchedEffect(true) {
// do stuff with oldSize...
myFunction(oldSize)
// allow oldSize to be updated again
updateSize = true
}
}
I am guessing if I wanted to put some of this logic into some kind of composable function, it would look something like this. I used rememberUpdatedState as a guide.
#Composable
fun <T> rememberUpdatedStateSaveable(
newValue: T,
stateSaver: Saver<T, out Any>,
enableUpdates: Boolean
): State<T> = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(newValue)
}.apply { value = if (enableUpdates) newValue else value }

Jetpack Compose: Make full-screen (absolutely positioned) component

How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?
Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.
I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?
One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.
From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.
We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.
What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.
Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.
#Composable
fun FullScreen(content: #Composable () -> Unit) {
val view = LocalView.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val id = rememberSaveable { UUID.randomUUID() }
val fullScreenLayout = remember {
FullScreenLayout(
view,
id
).apply {
setContent(parentComposition) {
currentContent()
}
}
}
DisposableEffect(fullScreenLayout) {
fullScreenLayout.show()
onDispose { fullScreenLayout.dismiss() }
}
}
#SuppressLint("ViewConstructor")
private class FullScreenLayout(
private val composeView: View,
uniqueId: UUID
) : AbstractComposeView(composeView.context) {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val params = createLayoutParams()
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
init {
id = android.R.id.content
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
}
private var content: #Composable () -> Unit by mutableStateOf({})
#Composable
override fun Content() {
content()
}
fun setContent(parent: CompositionContext, content: #Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
private fun createLayoutParams(): WindowManager.LayoutParams =
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = composeView.applicationWindowToken
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
fun show() {
windowManager.addView(this, params)
}
fun dismiss() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
windowManager.removeViewImmediate(this)
}
}
Here is an example how you can use it
#Composable
internal fun Screen() {
Column(
Modifier
.fillMaxSize()
.background(Color.Red)
) {
Text("Hello World")
Box(Modifier.size(100.dp).background(Color.Yellow)) {
DeeplyNestedComposable()
}
}
}
#Composable
fun DeeplyNestedComposable() {
var showFullScreenSomething by remember { mutableStateOf(false) }
TextButton(onClick = { showFullScreenSomething = true }) {
Text("Show full screen content")
}
if (showFullScreenSomething) {
FullScreen {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
) {
Text("Full screen text", Modifier.align(Alignment.Center))
TextButton(onClick = { showFullScreenSomething = false }) {
Text("Close")
}
}
}
}
}
The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.
Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.
Dialog(
onDismissRequest = { /* Do something when back button pressed */ },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
/* Your full screen content */
}
If I understand correctly you just don't want to navigate anywhere. Id something like this.
when (val viewType = viewModel.viewTypeGallery.get()) {
is GalleryViewModel.GalleryViewType.Gallery -> {
Gallery(viewModel, scope, installId, filePathModifier, fragment, setImageUploadType)
}
is GalleryViewModel.GalleryViewType.ImageViewer -> {
Row(Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxSize(),
painter = rememberCoilPainter(viewType.imgUrl),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
I just keep track of what type the view is meant to be. In my case I'm not displaying a dialog I'm removing my entire gallery and showing an image instead.
Alternatively you could just have an if(viewImage) condition below your call your and layer the 'dialog' on top of it.
After notice that, at least for now, we don't have any Composable to do "easy" fullscreen, I decided to implement mine one, mostly based on ideas from #foxtrotuniform6969 and #ntoskrnl. Also, I tried to do it most possible without to use platform dependent functions then I think this is very suiteable to Desktop/Android.
You can check the basic implementation in this GitHub repository.
By the way, the implementation idea was just:
Create a composable to wrap the target composables tree that can call an FullScreen composable;
Retrieve the full screen dimensions/size from a auxiliary Box matched to the root screen size using the .onGloballyPositioned() modifier;
Store the full screen size and all FullScreen composables created in the tree onto appropriated compositionLocalOf instances (see documentation).
I tried to use this in a Desktop project and seems to be working, however I didn't tested in Android yet. The repository also contains a example.
Feel free to navigate in the repository and sent a pull request if you can. :)

Problem trying to optimize for recomposition

I'm trying to port a rather complex Android View to Compose and I've managed to do a naive implementation by basically using a Canvas and moving the onDraw() code there. I've ran into issues when trying to optimize this to make it skip unneeded parts of the recomposition.
The view is a board for the game of GO (it would be the same for chess). I'm trying to get things such as the board's background to not redraw every time a move is made, as the background does not change. As my understanding of the docs is, if I pull the drawBackground() from the onDraw and just put it in an Image() composable, the Image() composable should not get recomposed unless its parameter (which is just the bitmap) changes. However, breakpoints show the method getting called every single time the position changes (e.g. the player makes a move). Am I doing something wrong? How could I take advantage of Compose here?
Code:
#Composable
fun Board(modifier: Modifier = Modifier, boardSize: Int, position: Position?, candidateMove: Point?, candidateMoveType: StoneType?, onTapMove: ((Point) -> Unit)? = null, onTapUp: ((Point) -> Unit)? = null) {
val background: ImageBitmap = imageResource(id = R.mipmap.texture)
Box(modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
Image(bitmap = background) // Expecting this to run only once, but gets run every time Board() gets recomposed!!!
var width by remember { mutableStateOf(0) }
val measurements = remember(width, boardSize) { doMeasurements(width, boardSize, drawCoordinates) }
var lastHotTrackedPoint: Point? by remember { mutableStateOf(null) }
Canvas(modifier = Modifier.fillMaxSize()) {
if (measurements.width == 0) {
return#Canvas
}
//... lots of draw code here
}
}
}
Any Jetpack Compose guru can help me understand why is it not skipping that recomposition?
Try putting Image(bitmap = background) in a separate composable function. Or moving Canvas to another function.

Jetpack Compose: Custom VectorAsset Icon object similar to built-in `Icons.Default`

It looks like the only way to go about loading custom icons from Android Vector Resources in the res folder is to do it within a #Composable function using the vectorResource(R.drawable.myVectorName) method.
This is great and all, but I like the syntax of fetching VectorAssets for the Icon(asset: VectorAsset) class, which looks like Icon(Icons.Default.Plus).
It looks like the vectorResource() method uses an internal method called loadVectorResource(), and the methods it uses to read the actual XML file composing the vector asset file are also internal.
How would I go about creating an object like MyAppIcons.Default.SomeIcon in Jetpack Compose?
EDIT
So, I have sort-of found a solution. However, it would be nice to make my own extension/overloading of the built-in Icon() function, but I'm not sure if there is a proper way to do this.
from Resources in Compose
Use the painterResource API to load either vector drawables or rasterized asset formats like PNGs. You don't need to know the type of the drawable, simply use painterResource in Image composables or paint modifiers.
// Files in res/drawable folders. For example:
// - res/drawable-nodpi/ic_logo.xml
// - res/drawable-xxhdpi/ic_logo.png
// In your Compose code
Icon(
painter = painterResource(id = R.drawable.ic_logo),
contentDescription = null // decorative element
)
Turns out I wasn't using my brain. The answer is pretty easy.
The gist is, Icon() is a composable function, meaning that of course vectorResource() can be used there.
So, the correct approach is no secret... it's to make your own MyAppIcon() component, call vectorResource() and then return a normal Icon(), like so:
Correct Way
#Composable
fun MyAppIcon(
resourceId: Int,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current
) {
Icon(
asset = vectorResource(id = resourceId),
modifier = modifier,
tint = tint
)
}
You can then create an object elsewhere, like so:
object MyAppIcons {
val SomeIcon = R.drawable.someIcon
val AnotherIcon = R.drawable.anotherIcon
}
When you put the two together, you can use it like this:
MyAppIcon(MyAppIcons.SomeIcon)
I'm hoping that Google just adds this override soon, allowing us to pass in resource IDs.
There is a way to load asset using Icon(Icons.Default.Plus). You need to make an extesion property
val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
get() {
}
but I don't see the way to get VectorAsset outside of composable function.
Of course you can do something like this
val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
get() {
return Assets.FiveG
}
object Assets {
lateinit var FiveG: VectorAsset
}
#Composable
fun initializeAssets() {
Assets.FiveG = vectorResource(R.drawable.ic_baseline_5g_24)
}
but it's a bad idea to have a composable with side effect. So i'm waiting for someone to find a way to convert SVG to VectorAsset Kotlin class or get VectorAsset object outside of composable function.
I went down the other route and extracted the logic from Jetpack Compose source code that turns an XML SVG path string into an ImageVector. In the end I came up with this:
fun makeIconFromXMLPath(
pathStr: String,
viewportWidth: Float = 24f,
viewportHeight: Float = 24f,
defaultWidth: Dp = 24.dp,
defaultHeight: Dp = 24.dp,
fillColor: Color = Color.White,
): ImageVector {
val fillBrush = SolidColor(fillColor)
val strokeBrush = SolidColor(fillColor)
return ImageVector.Builder(
defaultWidth = defaultWidth,
defaultHeight = defaultHeight,
viewportWidth = viewportWidth,
viewportHeight = viewportHeight,
).run {
addPath(
pathData = addPathNodes(pathStr),
name = "",
fill = fillBrush,
stroke = strokeBrush,
)
build()
}
}
All you have to do is call this function with pathStr set to the value of android:pathData from the drawable XML file. Here's an example:
val AppleIcon by lazy { makeAppleIcon() }
// by Austin Andrews, found on https://materialdesignicons.com/
private fun makeAppleIcon(): ImageVector {
return makeIconFromXMLPath(
pathStr = "M20,10C22,13 17,22 15,22C13,22 13,21 12,21C11,21 11,22 9,22C7,22 2,13 4,10C6,7 9,7 11,8V5C5.38,8.07 4.11,3.78 4.11,3.78C4.11,3.78 6.77,0.19 11,5V3H13V8C15,7 18,7 20,10Z"
)
}
#Preview
#Composable
fun AppleIconPreview() {
Surface {
Icon(AppleIcon, "Apple")
}
}

Does Compose ConstraintLayout provide same handling for INVISIBLE and GONE as view based ConstraintLayout?

Disclaimer: I know that Compose just entered alpha01 and thus I do not expect that every functionality
is available. However, layout and handling of specific layout cases is IMHO an important topic
that should be addressed early 😉.
The current view based ConstraintLayout provides some specific handling in case a child view is
marked as GONE, refer to the
ConstrainLayout documentation.
I checked the Compose ConstraintLayout documentation, the available modifiers and so on, however
did not find anything that points in this direction. I also could not find any hint regarding
INVISIBLE and how/if Compose ConstraintLayout handles it like the view based ConstraintLayout.
In general, the current view based layouts (LinearLayout for example) handle INVISIBLE and GONE in a
similar fashion:
if a view is in state INVISIBLE then the view is part of the layout with its sizes, just not
shown. The overall layout of other views does not change and they stay in their positions.
if a view is in state GONE its sizes a usually treated as 0 and the layout is recomputed and
changed, other views usually change their positions.
Here a simple Compose ConstraintLayout UI, just 4 buttons in a row, chained to have them nicely
spread.
// if dontShow is 0 then show all buttons, otherwise make the button with this number
// somehow INVISIBLE. This feature is not yet implemented.
#Composable
fun fourButtonsCL(dontShow: Int) {
ConstraintLayout(Modifier.fillMaxSize()) {
val (btn1, btn2, btn3, btn4) = createRefs()
TextButton(onClick = {}, Modifier.constrainAs(btn1) {}.background(teal200)) { Text("Button1") }
TextButton(onClick = {}, Modifier.constrainAs(btn2) {}.background(teal200)) { Text("Button2") }
TextButton(onClick = {}, Modifier.constrainAs(btn3) {}.background(teal200)) { Text("Button3") }
TextButton(onClick = {}, Modifier.constrainAs(btn4) {}.background(teal200)) { Text("Button4") }
createHorizontalChain(btn1, btn2, btn3, btn4)
}
}
#Preview(showBackground = true)
#Composable
fun previewThreeButtons() {
ComposeAppTheme {
fourButtonsCL()
}
}
Assume I would like to make Button3 invisible, but keep the other 3 buttons positioned where they
are. Thus just a hole between Button2 and Button4. How to achieve this without creating yet another
Composable or adding additional logic. While the logic in this simple case may be just a view lines
of code, more complex layouts would the need some more complex logic. In veiw based ConstraintLayout
wwe just need to modify the child view.
The other assumption: make Button3 disappear completely from the layout (GONE) and re-compute the
layout, remaining buttons become wider and evenly spread out. At a first glance this looks simple,
and in this very simple example it maybe easy. However in more complex layouts this could require
some or even a lot of re-wiring of constraints of the embedded Composables.
Thus the question is:
how does compose handles these cases for Column and Row layouts (like in view based LinearLayout)
and for ConstraintLayout in particular? However with the following restriction 😉: without defining
many new Composables and/or without adding complex layout logic inside the Composables (re-wiring
constraints for example).
Did I miss some modifier? Is this even planned or possible in the Composable layouts? What would be
the preferred way to solve such layout cases in Compose?
Based on #CommonsWare comment to the question I could solve the INVISIBLE
option, see code below.
Currently (in alpha-01) the implementation of ConstraintLayout seems to be incomplete, at least a few TODO comments in the code indicate this. This
seems to include the yet missing support of the GONE feature.
I saw some of these:
// TODO(popam, b/158069248): add parameter for gone margin
Also the chain feature does not yet perform layout rendering in the same way as
in the view based ConstraintLayout.
object FourElementsNoDSL {
const val elementA = "ElementA"
const val elementB = "ElementB"
const val elementC = "ElementC"
const val elementD = "ElementD"
private val noDSLConstraintSet = ConstraintSet {
// Create references with defines ids, here using a string as id. Could be an Int as well,
// actually it's defined as 'Any'
val elemA = createRefFor(elementA)
val elemB = createRefFor(elementB)
val elemC = createRefFor(elementC)
val elemD = createRefFor(elementD)
// Simple chain only. Instead of this simple chain we can use (for example):
// constrain(elemA) {start.linkTo(parent.start) }
// to set a constraint as known in XML
// constrain(elemA) {start.linkTo(parent.start, 16.dp) }
// constrain(elemB) {start.linkTo(elemA.end) }
// constrain(elemC) {start.linkTo(elemB.end) }
// constrain(elemD) {end.linkTo(parent.end) }
createHorizontalChain(elemA, elemB, elemC, elemD)
}
#Composable
fun fourButtonsCLNoDSL(doNotShow: List<String>) {
ConstraintLayout(constraintSet = noDSLConstraintSet, modifier = Modifier.fillMaxSize()) {
// This block contains the children
Text(text = "A",
modifier = Modifier.layoutId(elementA)
.drawOpacity(if (doNotShow.contains(elementA)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "B",
modifier = Modifier.layoutId(elementB)
.drawOpacity(if (doNotShow.contains(elementB)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "C",
modifier = Modifier.layoutId(elementC)
.drawOpacity(if (doNotShow.contains(elementC)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "D",
modifier = Modifier.layoutId(elementD)
.drawOpacity(if (doNotShow.contains(elementD)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp))
}
}
}
#Preview(showBackground = true)
#Composable
fun previewFourFieldsNoDSL() {
val noShow = listOf(FourElementsNoDSL.elementC)
PlaygroundTheme {
FourElementsNoDSL.fourButtonsCLNoDSL(noShow)
}
}
The object FourElementsNoDSL defines the layout, provides element ids and so on.
This is roughly comparable to an XML file that contains such layout.
noDSL means, that this layout does not use the Compose ConstraintLayout's Kotlin
DSL. Currently the DSL does not provide a mechanism to setup element references
(used in layoutId) with defined ids as its done in this example.

Categories

Resources