I want to create with Jetpack Compose a dinamic header that should resize when user scrolls on the main screen content section. The dynamic header works fine, but I have some problem with a WebView.: with Compose when the WebView is rendered, it is stretched and any popup shown to the user are out of the main viewport.
This is an implementation using the old-style XML layout:
We have on the top the fake green dynamic header. The webview is loaded with an initial popup centered into the available screen, as expected.
With compose, the result is the following:
On the top there is the fake gray dynamic header, but the webview is "stretched", and the initial popup is not centered.
This happens also with a stand-alone WebView, without the dynamic header on top, so this is not an issue created by the dynamic header.
The implementation of the WebView is pretty simple
#SuppressLint("SetJavaScriptEnabled")
#Composable
fun PageBodyWithWebView(
scrollState: ScrollState,
) {
val context = LocalContext.current
AndroidView(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxWidth()
.fillMaxHeight(),
factory = {
WebView(context).apply {
settings.javaScriptEnabled = true
loadUrl(pageUrl)
}
})
}
So what is wrong with this? How we can center the webview, so the user can see all the popup as expected?
The base issue is that you're not reading the scroll from the WebView, and the scroll modifier on the AndroidView composable is not doing what you think it does. A solution is to hoist an event when the webview is scrolled and respond to that: https://gist.github.com/rock3r/65642c73de8602ac40a340c84e20b16a
Related
Background
I have created a Composable that contains a TopAppBar and a WebView which recreates the enterAlways collapsing-toolbar behavior that Xml has.
The way it works is by applying Modifier.offset {..} to the composables. The offset is calculated whenever the WebView is scrolled.
For example, if I'm at the top of the page, and I scroll towards the bottom of the page (swipe from bottom to top), the Y offset for both the WebView & TopAppBar decreases at the same time.
Problem
The problem is that it is very jittery whenever the TopAppBar is expanding or collapsing. I think this is because the offset for the WebView is changing while I'm still scrolling. In the previous example, the WebView moves up to take the place of the collapsing-toolbar, but my finger is still pressed on the screen while that happens, which may cause the device to interpret that as an upward scroll (swiping from top to bottom).
Basic Outline of Code:
Column {
TopAppBar(
Modifier.offset { IntOffset(0, state.toolbarOffsetHeightPx) },
...
)
WebView(
Modifier.offset { IntOffset(0, state.toolbarOffsetHeightPx) },
onWebPageScrolled = /** Update state.toolbarOffsetHeightPx */
...
)
}
What I tried
One potential solution I thought of involves reading the scrolls (like I currently am) and also consuming them.
When the toolbar is collapsing or expanding, I realized that I should probably only adjust the offset during that time, and I should ensure that the WebView does not scroll its content.
Since the WebView's height is the screen height, when the TopAppBar is collapsing, the part of the WebView which was hidden at the bottom of the screen now comes into view, and to the user, the offset of the WebView changing would make it look like they are scrolling. Therefore, I should force the WebView to not scroll during this time.
--
The problem with this approach is that I'm getting the data about WebView scrolls from the onScrollChangedListener, so if I disable WebView scrolling while the TopAppBar is expanding or collapsing, then I also lose the ability to keep expanding/collapsing the TopAppBar...
Some solutions I came up for this are:
Have a wrapper Composable around the WebView which can receive the scrolls.
I could not figure out how to implement this. It looks like there is a nestedScrolling Modifier which allows capturing scrolls, so I wrapped my WebView in a Box which had that Modifier, but even though I was consuming the scrolls in the NestedScrollConnection the WebView was still receiving scrolls normally..
Use onTouchListener to receive scroll events rather than onScrollListener.
This gets a little complicated because onTouchListener is not only called for scrolls. I tried only running my TopAppBar/WebView offset logic if the onTouchListener was called for MotionEvent.ACTION_DOWN, but the results were very strange.
I did manage to replicate the original expanding/collapsing w/ the TopAppBar & WebView, but rather than expanding/collapsing while I'm scrolling, the entire expanding/collapsing would complete before I even started dragging my finger - just pressing my finger and twisting it up or down completely expanded/collapsed the TopAppBar.
The desired behavior is that the expanding/collapsing should happen linearly with the webpage scrolling, so this did not work.
I really appreciate any help. I've created a simple app on a GitHub repo if you would like to test the behavior.
I have LazyColumn that has verticalScroll modifier, and each child row also has a horizontal swipeable modifier. Everything actually works pretty well, but there is a bit of a problem when trying to scroll vertically -- if there's even a very slight horizontal movement, then the vertical scroll doesn't get triggered, and the horizontal swipe is triggered instead.
Is there any way to set a 'preference' for the vertical scroll, so that if there's any vertical movement, then the swipe shouldn't happen?
Edit:
I should clarify that I'm using the swipeable modifier on each row for swipe-to-dismiss functionality. It's got some custom functionality, which is why I'm not using the SwipeToDismiss() composable, but I did try running it using SwipeToDismiss(), and it has the same problem
I suggest using HorizontalPager() inside your verticalScroll Column.
Column(
Modifier
.verticalScroll(scrollState)
) {
HorizontalPager(/*...*/)
}
It is smooth by default but you can even modify the gesture of it by changing flingBehavour as described here:
Swipe sensitivity for HorizontalPager in Compose
I want to achieve this using jetpack compose.
A is scrollable list of row items. When A is smaller than screen(or parent) size, B(footer) should be placed bellow the last row. When A + B are bigger than screen size, then B becomes fixed at the bottom and A content is scrollable. I'm wondering if there is easy way to achieve this, using compose ConstraintLayout.
I found solution for it. I had to use Modifier.weight(1f, false) at A.
There are many ways to do this but the most efficient way is to use the bottomBar property of Scaffold View.
For example:
#Composable
fun RenderContent() {
Scaffold(
topBar = { /* Your app bar goes here */ },
bottomBar = { /* Anything you place in here will be stick to the bottom. */ }
) {
// ... Rest of the content
// Benefits: If you make the content scrollable and
// the `bottomBar` composable remain on top of the content
}
}
If I understood you correctly you are trying to add a footer view to a LazyColmn. The LazyColumn in JetpackCompose is behaving like the native ListView. I will give an example of a LazyColumn that has a view(composable) at the bottom that is shown when you scroll at the end of the list:
LazyColumn() {
item {
//If you want the first item to stay always on top as you scroll use stickyHeader for this section.
Text("This is the first item")
}
items(listItems.size) {
Text("This is a normal list item")
}
item {
Text("This item will behave as a footer and display at the end of the list")
}
}
Hope this helps somebody.
Thanks
I do have a requirement in jetpack compose where I need a bottom bar in my scaffold also a bottom sheet with adjustable height. I can achieve only any one of these.
I had to sacrifice the bottom bar with below code:
BottomSheetScaffold(sheetContent = {//anything here})
{ innerPadding -> Box(){}
}
I have no option to use bottom bar here whereas I have the freedom to adjust the height of the sheet. The second solution is using ModalBottomSheetLayout where I can add scaffold inside it and add bottom bar but won't be able to adjust the height of sheet.
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {//anything here}
){
Scaffold(
topBar = {
//
},
bottomBar = {
//
},
){
innerPadding -> Box(){}
}
}
Your problem can be possibly solved with BottomSheetScaffold. You can adjust its height in closed position via sheetPeekHeight. To have a bottombar element when the bottom sheet is collapsed, you should read this article and take a look at the related sample. The principle of this solution is to animate the contents of the bottom sheet to bottom bar when it's collapsed, and to animate to the content you want in your bottom sheet when it's opened.
the article that explains the solution, and the sample.
If that doesn't work for you, then you should probably look at your own composable realisation. The requirements for implementing bottom sheet-like functionality are: nestedScroll, swipeable and offset Modifier extensions. How to deal with them you can find here: article about nested scrolling and bottom sheets.
Is there a way to automatically animate composition changes in a Jetpack Compose #Composable? For instance, if a previously shown widget is removed in a recomposition, can a fade-out animation be applied automatically? I'm thinking something similar to Android View's animateLayoutChanges.
Yes, you can use:
val visible by remember { mutableStateOf(false) }
AnimatedVisibility(visible = visible) {
// Composables Here
}
Or for the specific animation you asked for, surround the Composable (s) with CrossFade