When using classic Views it is easy to obtain a bitmap from a view without displaying it. I create the view class through a LayoutInflater, and then, since it hasn't been attached to a view, I measure it first.
I have the following extension function which measures it and draws the view on a bitmap:
fun View.toBitmap(width, height): Bitmap {
this.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
)
val bitmap = Bitmap.createBitmap(this.measuredWidth, this.measuredHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
this.layout(0, 0, this.measuredWidth, this.measuredHeight)
this.draw(canvas)
return bitmap
}
When using Composables I can't succeed in exporting a bitmap from a view.
I imagined something like this:
class MyComposableView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): AbstractComposeView(context, attrs, defStyleAttr) {
#Composable
override fun Content() {
MyComposable()
}
}
What I did is instancing a MyComposableView with the application context, and then I tried to obtain a bitmap with the extension function toBitmap.
The result is the following exception:
java.lang.IllegalStateException: Cannot locate windowRecomposer; View io.myapp.MyComposableView{f66ecdd V.E...... ......I. 0,0-0,0} is not attached to a window
What I can't understand is why the exception is thrown for the AbstractComposeView but is not thrown for the view obtained through the inflater.
EDIT:
on 09 Apr. 2022 it seems there's not a solution other than using a classic XML layout.
You can do that without xml layout like this by flowing below steps and impelement functions :
BitmapComposable(
onBitmapped = { bitmap ->
// Do your operation
},
intSize = IntSize(500, 700) // Pixel size for output bitmap
) {
// Composable that you want to convert it to bitmap
// This context is #composable
YourComposable()
}
1 - Copy this functions :
Note that this composable will not display anything on screen !
#Composable
fun BitmapComposable(
onBitmapped: (bitmap: Bitmap) -> Unit = { _ -> },
backgroundColor: Color = Color.Transparent,
dpSize : DpSize,
composable: #Composable () -> Unit
) {
Column(modifier = Modifier
.size(0.dp, 0.dp)
.verticalScroll(
rememberScrollState(), enabled = false
)
.horizontalScroll(
rememberScrollState(), enabled = false
)) {
Box(modifier = Modifier.size(dpSize)) {
AndroidView(factory = {
ComposeView(it).apply {
setContent {
Box(modifier = Modifier.background(backgroundColor).fillMaxSize()) {
composable()
}
}
}
}, modifier = Modifier.fillMaxSize(), update = {
it.run {
doOnLayout {
onBitmapped(drawToBitmap())
}
}
})
}
}
}
#Composable
fun BitmapComposable(
onBitmapped: (bitmap: Bitmap) -> Unit = { _ -> },
backgroundColor: Color = Color.Transparent,
intSize : IntSize, // Pixel size for output bitmap
composable: #Composable () -> Unit
) {
val renderComposableSize = LocalDensity.current.run { intSize.toSize().toDpSize() }
BitmapComposable(onBitmapped,backgroundColor,renderComposableSize,composable)
}
2 - And use like this :
BitmapComposable(
onBitmapped = { bitmap ->
// Do your operation
},
backgroundColor = Color.White,
intSize = IntSize(500, 700) // Pixel size for output bitmap
) {
// Composable that you want to convert it to bitmap
// This context is #composable
YourComposable()
}
Related
I have a lazy column with items where each item has its own custom Slider which is implemented using the AndroidView(). The problem is that the slider has its onDraw() Method overridden. Since the onDraw() method runs in the Main thread, during re-composition or scrolling, the UI starts to lag a lot. How can I tackle this?
LazyColumn(
modifier = Modifier.fillMaxWidth(), contentPadding = it
) {
itemsIndexed(items = historyState.asReversed())
{ index, history ->
HistoryItem(
name = history.groupName,
progress= history.progress)
}
}
#Composable
fun HistoryItem(name: String,progress:Float)
{
Column{
Text(text = name)
WaveFormCompose(
modifier = Modifier
.weight(0.7F)
.padding(8.dp),
progress = progress,
audioData = someData,
audioTrack = someByteBuffer,
onSeekStarted = {onSeekStarted(it)},
onSeekFinished = {},
)
}
}
The compose function WaveFormCompose() looks like this:
#Composable
fun WaveFormCompose(
modifier: Modifier,
progress: Float,
audioTrack: ReplayTrack?,
audioData: ByteBuffer?,
onSeekStarted: (Float) -> Unit,
onSeekFinished: (Float) -> Unit,
) {
AndroidView(modifier = modifier,
factory = { context ->
WaveformSlider (context).apply {
this.audioData = audioData
value = progress
},
update ={
it.value =progress
})
Now the class WaveFormSlider is a custom view that extends Google material Slider,and has its onDraw() method overridden.
Inside class
class WaveformSlider #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null){
Slider(context, attrs)
override fun onDraw(canvas: Canvas) {
if (heights == null) {
computeHeights()
}
// Draw waveform
for (i in 0 until (heights?.size ?: 0)) {
val height = heights?.get(i) ?: 0.0f
if (height > 0) {
drawWaveformLine(canvas, i, height)
}
}
super.onDraw(canvas)
}
}
The reversed operation is a heavy operation and should be done as follows:
val reversedList = remember(historyState) { historyState.asReversed() }
please check this and notify me
I am trying to solve this problem. Basically I'm creating a function in which I will put a webview.
Both above this function and below this function there will be other UI components.
when I scroll up the part of the UI above the webview shrinks to half of its height.
What I do is to record the page scroll events to make this animation.
Unfortunately, however, it happens that the webview does not ONLY occupy the space available on the screen but in height it is as high as all the contents of the WebView. Below you can see what I would like to achieve in the first image:
And here instead what I actually get:
This is a problem because when a page asks to accept cookies and the relevant popup appears, being the page so high I get an overlay that looks like an error but is simply given by the popup that asks you to accept cookies and which however is below, and on some devices it is not even visible
What I am trying to achieve, unsuccessfully, would be the XML equivalent of 0dp, in height.
To do this I thought I could use ConstraintLayouts for compose (implementation ("androidx.constraintlayout:constraintlayout-compose:1.0.0")) but they are not working. In fact, the result I get is as if I had "wrap content" and not 0dp and in fact a small page shrinks the box, a large page expands beyond the size of the screen.
Below is the code I am using. I started with the new compose feature whose example is well explained in this link
private val ToolbarHeight = 300.dp
#Composable
fun CollapsingToolbarComposeViewComposeNestedScrollInteropSample(content: #Composable () -> Unit) {
val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
logger.debug("Offset.Zero : ${Offset.Zero} .... $newOffset, $delta")
return Offset.Zero
}
}
}
// Compose Scrollable
Box(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
// Android View
AndroidView(
factory = { context -> AndroidViewWithCompose(context, content) },
modifier = Modifier.fillMaxWidth()
)
TopAppBar(
modifier = Modifier
.height(ToolbarHeight)
.offset {
IntOffset(
x = 0,
y = toolbarOffsetHeightPx.value
.coerceAtLeast(-200f)
.coerceAtMost(0f)
.roundToInt()
)
},
title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
)
}
}
private fun AndroidViewWithCompose(context: Context, content: #Composable () -> Unit): View {
return LayoutInflater.from(context)
.inflate(R.layout.three_fold_nested_scroll_interop, null).apply {
with(findViewById<ComposeView>(R.id.compose_view)) {
// Compose
setContent { LazyColumnWithNestedScrollInteropEnabled(content) }
}
}.also {
ViewCompat.setNestedScrollingEnabled(it, true)
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun LazyColumnWithNestedScrollInteropEnabled(content: #Composable () -> Unit) {
ConstraintLayout() {
val (wv) = createRefs()
LazyColumn(
modifier = Modifier
.constrainAs(wv) {
top.linkTo(parent.top)
bottom.linkTo(parent.top)
}
.nestedScroll(
rememberNestedScrollInteropConnection()
),
contentPadding = PaddingValues(top = ToolbarHeight)
) {
item {
Text("This is a Lazy Column")
}
items(1) { _ ->
Box(modifier = Modifier.fillMaxSize()
) {
content()
}
}
}
}
}
the content() is just my webView:
AndroidView(
factory = {
WebView(it).apply {
webViewClient = WebViewClient()
settings.javaScriptEnabled = true
loadUrl("https://stackoverflow.com/posts/72259954/edit")
}
}
)
I have VideoListScreen with LazyColumn and as my item I use VideoItem. This LazyColumn it's created with grid items to have lazy grid view with Category header. Tag is tag of category. Category details is information about category colors, title etc.:
#Composable
fun VideoItem(
videoPath: String,
brush: Brush,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val assetFileDescriptor = LocalContext.current.assets.open(videoPath)
Surface(
modifier = modifier
.padding(5.dp)
.aspectRatio(1f)
.clickable { onClick() },
shape = Shapes.small,
elevation = 1.dp
) {
GlideImage(
imageModel = assetFileDescriptor.readBytes(),
contentScale = ContentScale.Crop,
requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE),
shimmerParams = ShimmerParams(
baseColor = MaterialTheme.colors.background,
highlightColor = Blue200,
durationMillis = 650,
dropOff = 0.65f,
tilt = 20f
)
)
Box(modifier = Modifier
.background(brush)
.fillMaxSize() )
}
}
VideoListScreen:
#Composable
fun VideoListScreen(
navController: NavHostController,
tag: String
) {
val cells = 2
val context = LocalContext.current
val categoryDetails = getCategoryDetailsBy(tag)
val videos = fetchVideos(context, tag)
LazyColumn(contentPadding = PaddingValues(5.dp)) {
item {
CategoryElement(
categoryDetails = categoryDetails,
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(5.dp),
customTitle = "O kategorii"
)
}
gridItems(videos, cells) { assetFileName ->
val videoPath = "$tag/$assetFileName"
VideoItem(
videoPath = videoPath,
brush = categoryDetails.transparentBrush
) { navController.navigateToPlayer(videoPath) } //onClick function
}
}
}
private fun fetchVideos(context: Context, tag: String): List<String> {
return context.resources.assets.list("$tag/")?.toList() ?: listOf()
}
gridItems extension function:
fun <T> LazyListScope.gridItems(
data: List<T>,
cells: Int,
itemContent: #Composable BoxScope.(T) -> Unit,
) {
items(data.chunked(cells)) { row ->
Row(Modifier.fillMaxWidth()) {
for ((index, item) in row.withIndex()) {
Box(Modifier.fillMaxWidth(1f / (cells - index))) {
itemContent.invoke(this, item)
}
}
}
}
}
The problem is that when I try to apply clickability on this item (no matter where) the thumbnail loading (from the assets) becomes almost twice as slow. What's interesting when onClick function is empty, performance issue disappearing. In function called "navigateToPlayer(videoPath)" I navigate to another screen and send "videoPath" with navController.
If you have any questions, feel free to ask!
In compose you're creating UI with view builders. This function can be called many times, when you start using animations it even can be recomposed on each frame.
That's why you shouldn't perform any heavy work directly in composable function. And if you do, you need to store the result so you don't need recalculation on the next recomposition.
Both fetchVideos and assets.open are quite heavy operations, and even result of getCategoryDetailsBy(not sure what's that) should be cached. To do that you need to use remember or rememberSaveable. Check out how are these different and more about state in composables.
So update your declarations like this:
val categoryDetails = remember { getCategoryDetailsBy(tag) }
val videos = remember { fetchVideos(context, tag) }
val context = LocalContext.current
val assetFileDescriptor = remember { context.assets.open(videoPath) }
What is the Android Compose approach to tile an image to fill my background with a small pattern?
A naive approach for Bitmaps without rotation could be like this:
#Composable
fun TileImage() {
val pattern = ImageBitmap.imageResource(R.drawable.pattern_bitmap)
Canvas(modifier = Modifier.fillMaxSize()) {
// rotate(degrees = -15f) { // The rotation does not produce the desired effect
val totalWidth = size.width / pattern.width
val totalHeight = size.height / pattern.height
var x = 0f
var y = 0f
for (i in 0..totalHeight.toInt()) {
y = (i * pattern.height).toFloat()
for (j in 0..totalWidth.toInt()) {
x = (j * pattern.width).toFloat()
drawImage(
pattern,
colorFilter = giftColorFilter,
topLeft = Offset(x, y)
)
}
}
// }
}
}
In Android XML you can easily create XML to repeat a bitmap
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="#drawable/pattern_bitmap"
android:tileMode="repeat" />
Or if you need to tile a vector you can use a custom Drawable class to achieve your goal
TileDrawable(AppCompatResources.getDrawable(context, R.drawable.pattern_vector), Shader.TileMode.REPEAT)
class TileDrawable(drawable: Drawable, tileMode: Shader.TileMode, private val angle: Float? = null) : Drawable() {
private val paint: Paint = Paint().apply {
shader = BitmapShader(getBitmap(drawable), tileMode, tileMode)
}
override fun draw(canvas: Canvas) {
angle?.let {
canvas.rotate(it)
}
canvas.drawPaint(paint)
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun getOpacity() = PixelFormat.TRANSLUCENT
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
private fun getBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bmp = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val c = Canvas(bmp)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(c)
return bmp
}
}
If you want to use native canvas you can do something like this in jetpack compose.
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
shader = ImageShader(pattern, TileMode.Repeated, TileMode.Repeated)
}
drawIntoCanvas {
it.nativeCanvas.drawPaint(paint)
}
paint.reset()
}
And If you want to limit your repetition to a certain height and width you can use the clip modifier in canvas like below otherwise it will fill the entire screen.
Canvas(
modifier = Modifier
.width(300.dp)
.height(200.dp)
.clip(RectangleShape)
) {
----
}
Based on Rafiul's answer, I was able to come up with something a bit more succinct. Here's hoping Compose comes up with something built-in to make this simpler in the future.
val image = ImageBitmap.imageResource(R.drawable.my_image)
val brush = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) }
Box(Modifier
.fillMaxSize()
.background(brush)) {
}
I'm looking for native code and other libraries such as Coil, Glide and native code load image asynchronously.
/*
Load the image in background thread.
Until resource loading complete, this function returns deferred image resource
with PendingResource. Once the loading finishes, recompose is scheduled and this
function will return deferred image resource with LoadedResource or FailedResource.
*/
val deferredResource: DeferredResource<ImageBitmap> =
loadImageResource(id = R.drawable.landscape2)
deferredResource.resource.resource?.let { imageBitmap ->
// We need a MUTABLE Bitmap to draw on Canvas to not get IllegalArgumentException
val bitmap = imageBitmap.asAndroidBitmap().copy(Bitmap.Config.ARGB_8888, true)
val paint = Paint().apply {
style = PaintingStyle.Stroke
strokeWidth = 10f
color = Color(0xff29B6F6)
}
// We need a ImageBitmap for Jetpack Compose Canvas
val newImageBitmap = bitmap.asImageBitmap()
val canvas = Canvas(newImageBitmap)
canvas.drawRect(0f, 0f, 200f, 200f, paint = paint)
canvas.drawCircle(
Offset(
newImageBitmap.width / 2 - 75f,
newImageBitmap.height / 2 + 75f
), 150.0f, paint
)
Image(bitmap = newImageBitmap, contentDescription = null)
}
As of alpha-12 DeferredResource is deprecated
#Deprecated("DeferredResource has been deprecated. Use State
instead")
How can i use State for loading and do i need to dispose bitmap when this Composable is done?
With Glide it can be as
#Composable
fun ImageDownloadWithGlideExample() {
val url =
"https://avatars3.githubusercontent.com/u/35650605?s=400&u=058086fd5c263f50f2fbe98ed24b5fbb7d437a4e&v=4"
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
val sizeModifier = Modifier.fillMaxWidth().width(150.dp)
val context = LocalContext.current
val glide = Glide.with(context)
val target = object : CustomTarget<Bitmap>() {
override fun onLoadCleared(placeholder: Drawable?) {
imageBitmap = null
}
override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
imageBitmap = bitmap.asImageBitmap()
}
}
glide
.asBitmap()
.load(url)
.into(target)
Column(
modifier = sizeModifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
imageBitmap?.let { imgBitmap ->
// Image is a pre-defined composable that lays out and draws a given [ImageBitmap].
Image(bitmap = imgBitmap, contentDescription = null)
}
}
}
and i saw in some examples onCommit and onDispose functions from androidx.compose.runtime:runtime which i cannot access even though it's implemented, is used. Is it necessary to use these functions or does Glide handle it?