Lazy Column with Android View in it lags a lot when scrolling - android

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

Related

How to fill available space with WebView inside

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")
}
}
)

How to wait for the end of the animation correctly?

I know that I can track the moment when lottie animation is completed using progress.
But the problem is that I want to start a new screen at the moment when the animation is completely finished.
Here is the code of my animation
#Composable
fun AnimatedScreen(
modifier: Modifier = Modifier,
rawId: Int
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize()
) {
val compositionResult: LottieCompositionResult = rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(rawId)
)
AnimatedScreenAnimation(compositionResult = compositionResult)
}
}
#Composable
fun AnimatedScreenAnimation(compositionResult: LottieCompositionResult) {
val progress by animateLottieCompositionAsState(composition = compositionResult.value)
Column {
if (progress < 1) {
Text(text = "Progress: $progress")
} else {
Text(
modifier = Modifier.clickable { },
text = "Animation is done"
)
}
LottieAnimation(
composition = compositionResult.value,
progress = progress,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
}
And here is code of my screen where i want to wait for the end of the animation and then go to a new screen:
#Composable
fun SplashScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: SplashScreenViewModel = getViewModel()
) {
val resIdState = viewModel.splashScreenResId.collectAsState()
val resId = resIdState.value
if (resId != null) {
AnimatedScreen(modifier = modifier, rawId = resId)
}
LaunchedEffect(true) {
navigate("onboarding_route") {
popUpTo(0)
}
}
}
I used the progress & listened to it's updates & as soon as it reaches 1f I'll call my function.
Example:
#Composable
fun Splash() {
LottieTest {
// Do something here
}
}
#Composable
fun LottieTest(onComplete: () -> Unit) {
val composition: LottieCompositionResult =
rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.camera))
val progress by animateLottieCompositionAsState(
composition.value,
iterations = 1,
)
LaunchedEffect(progress) {
Log.d("MG-progress", "$progress")
if (progress >= 1f) {
onComplete()
}
}
LottieAnimation(
composition.value,
progress,
)
}
Note: This is just the way I did it. The best way is still unknown(to me atleast). I feel it lacks the samples for that.
Also, You can modify a lot from this & just concentrate on the core flow.

Composable to bitmap without displaying it

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()
}

Android Jetpack Compose (Composable) How to properly implement Swipe Refresh

I use the SwipeRefresh composable from the accompanist library, I checked the examples, but I could not find sample that matches my needs. I want to implement element that is hidden above the main UI, so when the user starts swiping the box is slowly shown up. I implemented this logic by setting the padding of the box that is below the hidden box. The obvious reason is that by changing the padding, all the composables are recreated and that leads to lags as seen from this report. Is there a way to fix it?
#Composable
fun HomeScreen(viewModel: CityWeatherViewModel) {
val scrollState = rememberScrollState()
val maxTopOffset = 200f
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
onRefresh = {
},
indicator = { state, triggerDp ->
if (state.isRefreshing) {
} else {
val triggerPx = with(LocalDensity.current) { triggerDp.toPx() }
val progress = (state.indicatorOffset / triggerPx).coerceIn(0f, 1f)
viewModel.apply {
rotateSwipeRefreshArrow(progress >= 0.9)
setSwipeRefreshTopPadding(progress * maxTopOffset)
}
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState, enabled = true)
.padding(top = viewModel.swipeRefreshPaddingTop.value.dp)
) {
HiddenSwipeRefreshBox(viewModel)
MainBox(viewModel)
}
}
}
#Composable
fun HiddenSwipeRefreshBox(viewModel: CityWeatherViewModel) {
}
#Composable
fun MainBox(viewModel: CityWeatherViewModel) {
}
#HiltViewModel
class CityWeatherViewModel #Inject constructor(
private val getCityWeather: GetCityWeather
) : ViewModel() {
private val _swipeRefreshPaddingTop = mutableStateOf(0f)
val swipeRefreshPaddingTop: State<Float> = _swipeRefreshPaddingTop
fun setSwipeRefreshTopPadding(padding: Float) {
_swipeRefreshPaddingTop.value = padding
}
}
I managed to fix it, by replacing the padding with offset, so the code for the Column is changed to:
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState, enabled = true)
.offset { IntOffset(0, viewModel.swipeRefreshPaddingTop.value.roundToInt()) }
) {
}
And now there is no more lagging, even on older devices with 2GB RAM! I have no idea how this is related to the padding lagging, but it works. I found the code from HERE

Jetpack Compose: Performance problem when element with GlideImage is clickable in LazyColumn

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) }

Categories

Resources