android bottomSheet can not update child size programmatically - android

it's hard to me to explain this problem, but you can see the below layout code,
First i have the layout look like this:
yeah, this is the call screen using webrtc, when i have the video, put it into main_render, the change the size when i have delegate for video size:
main_render.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
main_render.init(rootEglBase.eglBaseContext, object : RendererCommon.RendererEvents{
override fun onFirstFrameRendered() {
Log.e(TAG, "onFirstFrameRendered")
}
override fun onFrameResolutionChanged(i: Int, i1: Int, i2: Int) {
Log.e(TAG, "onFrameResolutionChanged: $i - $i1")
runOnUiThread {
val newParams = main_render.layoutParams as FrameLayout.LayoutParams
newParams.width = dm.widthPixels
newParams.height = i1 * dm.widthPixels / i
main_render.layoutParams = newParams
main_render.requestLayout()
main_layout.updateViewLayout(main_render, newParams)
main_layout.requestLayout()
}
}
})
But the problem is the size does not changed, i have to press to hide sheet, press again to show sheet then now the size is change ( i have onclick to hide and show collapse sheet)
Can someone help me know this problem, when remove sheet and using main_layout it's work normally, but when using sheet the size can not changed immediately

Try to set state of BottomSheet after update viewlayout if it helps:
val behavior = bottomSheetDialog.behavior
behavior.state = BottomSheetBehavior.STATE_EXPANDED

Related

Reverting Window Insets on fragment change

I have three fragments. I want to apply a transparent status bar on just one fragment. For that purpose, I am calling the following hide method on the setOnItemSelectedListener method of the bottom navigation bar. Also added an image of what I am getting right now
private fun hideStatusBar() {
window.statusBarColor = ContextCompat.getColor(this#SuperActivity, R.color.transparent)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = insets.left
rightMargin = insets.right
bottomMargin = insets.bottom
}
WindowInsetsCompat.CONSUMED
}
}
private fun showStatusBar() {
window.statusBarColor = ContextCompat.getColor(this#SuperActivity, R.color.transparent)
WindowCompat.setDecorFitsSystemWindows(window, true)
}
I am getting the appropriate behavior on fragment calling hide method.
But when I tap on another fragment (the one that needs to show the status bar), I get the following behaviour:
The bottom margin by default is 0 (or the designated value in the root layout "binding.root")
So, you need to reset the bottom margin again; if it's already 0; then you can:
private fun showStatusBar() {
window.statusBarColor = ContextCompat.getColor(this#SuperActivity, R.color.transparent)
WindowCompat.setDecorFitsSystemWindows(window, true)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0 // reset the margin
}
WindowInsetsCompat.CONSUMED
}
}
}
Or if it's something else; then you need to convert that from dp to pixels and set it to the bottomMargin
The same thing applies if you have some designated margin values in binding.root; but I think you didn't as the issue only appears at the bottom.
UPDATE:
The method setOnApplyWindowInsetsListener is not called inside showStatusBar method. Because in this, the Window Insets are not changed. Since, we added margin in hideStatusBar method, so this space that you see below navigation bar is from hideStatusBar method.
Although the listener should be triggered, but you can update the root directly:
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
But notice that the setDecorFitsSystemWindows can take some time to update, so updateLayoutParams wouldn't have the effect, so, you might need a little delay for that:
Handler(Looper.getMainLooper()).postDelayed( {
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
}, 0.1.toLong())

LazyList composable inside ViewPager2 with height fixation

I have a ViewPager2 inside a BottomSheetDialog in which I load a Fragment that contains a ComposeView. Inside this view I populate a LazyList with items as soon as they're loaded.
Now this works all fine, except that the ViewPager2 makes no height adaptions when it's inner contents change, so naturally I adapted the peekHeight at first and then added a GlobalLayoutListener to give the pager the height of the inner, currently displayed fragment view, like so:
val myPager = ...
myPager.registerOnPageChangeCallback(AdaptChildHeightOnPageChange(myPager))
...
internal class AdaptChildHeightOnPageChange(private val viewPager: ViewPager2) : ViewPager2.OnPageChangeCallback() {
private val otherViews = mutableSetOf<View>()
private fun getViewAtPosition(position: Int): View =
(viewPager.getChildAt(0) as RecyclerView).layoutManager?.findViewByPosition(position)
?: error("No layout manager set or no view found at position $position")
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val itemView = getViewAtPosition(position)
val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
itemView.updatePagerHeightForChild()
}
// remove the global layout listener from other views
otherViews.forEach { it.viewTreeObserver.removeOnGlobalLayoutListener(it.tag as ViewTreeObserver.OnGlobalLayoutListener) }
itemView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
itemView.tag = layoutListener
otherViews.add(itemView)
}
private fun View.updatePagerHeightForChild() {
post {
val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams as ViewGroup.LayoutParams)
.also { lp -> lp.height = measuredHeight }
}
}
}
}
(taken and adapted from https://stackoverflow.com/a/58632613/305532)
Now while this works fantastically with regular compose content, as soon as I switch my compose view to the LazyList implementation (or anything that uses Modifier.verticalScroll(...)), I receive the following exception:
Nesting scrollable in the same direction layouts like LazyColumn and \
Column(Modifier.verticalScroll()) is not allowed (Scroll.kt:370)
But I don't get this really, because I haven't nested any vertical-scolling compose elements that could trigger this exception. My only guess is that because of the height constraint I give to the ViewPager2 this internally triggers the enablement of vertical scrolling, making the inner LazyList unable to take over.
How can I solve this issue?
Ok, the crash seem to have stem from an issue with the GlobalLayoutListener. This constantly fired updates and kicked of relayouts, even though I tried to remove the listener explicitely before setting a new height to the surrounding pager.

Animation on a view with ConstraintLayout.LayoutParams ends instantly

I'm currently trying to change the matchConstraintPercentWidth from 2 to 0 of a view using Animation() when starting my activity (in method onWindowFocusChanged() to make sure that all the views have been drawn correctly). The problem is the animation ends instanlty (and th view has now the new params - seems like the duration of the animation is 0 ms), no matter the duration I set...
Here is my code (in Kotlin) :
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
val gradient = findViewById<ImageView>(R.id.black_gradient)
val animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
val params = gradient.layoutParams as ConstraintLayout.LayoutParams
params.matchConstraintPercentWidth = 0f
gradient.layoutParams = params
}
}
animation.duration = 2000L // in ms
gradient.startAnimation(animation) //also tried animation.start() without effect
//animation.hasStarted() is always false here
}
}
Any help is welcome ;)
That applyTransformation method is where you're meant to calculate the current state of the animation, based on interpolatedTime (which is between 0.0 and 1.0). You're just setting your constraint value to 0, so it's not actually changing a value over time and animating anything.
Honestly you probably don't want to touch any of that if you can help it, Android has some helper classes that abstract a lot of that detail away, so you can just easily animate a thing.ValueAnimator is probably a good shout, you could just do
ValueAnimator.ofFloat(0f, 100f).apply {
addUpdateListener { anim ->
val params = (gradient.layoutParams as ConstraintLayout.LayoutParams)
params.matchConstraintPercentWidth = anim.animatedValue as Float
}
duration = 1000
start()
}
and that should be the equivalent of what you're doing. There's also ObjectAnimator at that link too, but that requires a setter method and there isn't one for that layout parameter (ConstraintProperties has some, but not for that one as far as I can see)
Code looks fine to me, try removing the condition of hasFocus, because there might some views which might be getting the focus before the this particular the thing you should do to diagnose is
try to log hasFocus if it's not getting focus then change the code like code below, also just a tip you should always initialize the views outside of callbacks.
override fun onWindowFocusChanged(hasFocus: Boolean) {
val gradient = findViewById<ImageView>(R.id.black_gradient)
val animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
val params = gradient.layoutParams as ConstraintLayout.LayoutParams
params.matchConstraintPercentWidth = 0f
gradient.layoutParams = params
}
}
animation.duration = 2000L // in ms
gradient.startAnimation(animation) //also tried animation.start() without effect
//animation.hasStarted() is always false here
}

How to add many views to GridLayout without blocking UI?

I want to display a grid layout in a fragment with thousands (>1.000) grid cells. In order to save code lines, I want to add the grid cells programmatically when the fragment is created. The grid cells do not have to do more than just each of them individually displaying a certain colour.
The problem is, whenever the fragment is created, the UI is blocked for several seconds because the grid layout has to be setup first.
I tried making use of AsyncLayoutInflater but that doesn't really solve my problem since the xml layout itself is very small and is inflated without blocking the UI. Creating and adding thousands of views to the grid layout after the xml layout was inflated is what blocks the UI.
So my question is, how can I add all these grid cells to my grid layout in the background without blocking the UI?
My code:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.grid_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupGrid()
}
private fun setupGrid() {
// Row and column count is currently set to 60
for (yPos in 0 until gridLayout.rowCount) {
for (xPos in 0 until gridLayout.columnCount) {
val gridCell = ImageView(activity)
val params = GridLayout.LayoutParams(GridLayout.spec(yPos, 1f), GridLayout.spec(xPos, 1f))
gridCell.layoutParams = params
gridLayout.addView(gridCell)
}
}
}
My xml file:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<GridLayout
android:id="#+id/gridLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="240dp"
android:layout_marginBottom="240dp"
android:columnCount="60"
android:rowCount="60"
android:orientation="horizontal" />
</RelativeLayout>
Screenshot how it should look like:
Thanks a lot!
Okay, so as the Android docs say, one should never call any methods or constructors on any view outside the UI thread since it is not thread safe. It may compile and actually run but it is unsafe to use.
I came up with a solution/workaround which actually only delays the UI block with a higher chance of the user not taking notice of it. This may only work in my specific scenario since I populate my grid with thousands of views only after a network connection was established. Populating the view still blocks the UI but the user may not notice it. A little tweak how to reduce the blocking UI time is described below.
The key to that is making use of an OnLayoutChangedListener. Once I have added all my views to my gridLayout, I call gridLayout.addOnChangeListener and implemented the listener to take care of the grid cells' layout params.
Here's the code:
fun configureGridLayout(gridHeight: Int, gridWidth: Int) {
println("Setting grid dimensions to: ${gridHeight}x${gridWidth}")
runOnUiThread {
gridLayout.rowCount = gridHeight
gridLayout.columnCount = gridWidth
for (g in 0 until gridHeight * gridWidth) {
val gridCell = View(context!!)
gridLayout.addView(gridCell)
}
gridLayout.addOnLayoutChangeListener(LayoutChangeListener(gridLayout, this))
}
}
// This callback is fired when fragment was completely rendered in order to reduce UI blocking time
class LayoutChangeListener(private val gridLayout: GridLayout, private val gridLayoutConfiguredListener: GridLayoutConfiguredListener) : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
v?.removeOnLayoutChangeListener(this)
val gridRowCount = gridLayout.rowCount
val gridColumnCount = gridLayout.columnCount
val gridHeight = gridLayout.height
val gridWidth = gridLayout.width
val margin = 1
val h = gridHeight / gridRowCount
val w = gridWidth / gridColumnCount
for (yPos in 0 until gridRowCount) {
for (xPos in 0 until gridColumnCount) {
val params = GridLayout.LayoutParams()
params.height = h - 2 * margin
params.width = w - 2 * margin
params.setMargins(margin, margin, margin, margin)
// Use post to get rid of "requestView() was called twice" errors
gridLayout.getChildAt(yPos * gridColumnCount + xPos).post {
gridLayout.getChildAt(yPos * gridColumnCount + xPos).layoutParams = params
}
}
}
gridLayoutConfiguredListener.onGridLayoutConfigured()
}
}

MotionLayout inside the ScrollView does not update the height after the state change

I have a MotionLayout inside the NestedScrollView:
<androidx.core.widget.NestedScrollView
android:id="#+id/scroll_content"
android:layout_width="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="#+id/content_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
app:layoutDescription="#xml/main_scene">
<View 1>
<View 2>
<View 3>
</androidx.constraintlayout.motion.widget.MotionLayout>
My state 1 shows View 1 only.
My state 2 shows View 2 only.
My state 3 shows View 1 + View 2(below View 1) + View 3(below View 2)
Since state 3 appends multiple views vertically, it is the longest vertically.
However, I can only scroll down to the amount set for state 1 & state 2. It does not reset the height inside the scrollView.
Am I doing something wrong?
I tried following at onTransitionCompleted():
scroll_content.getChildAt(0).invalidate()
scroll_content.getChildAt(0).requestLayout()
scroll_content.invalidate()
scroll_content.requestLayout()
They did not solve my issue.
Adding motion:layoutDuringTransition="honorRequest" inside the <Transition> in your layoutDescription XML file fixes the issue.
This was added to ConstraintLayout in version 2.0.0-beta4
Unfortunately, I also encountered such a problem, but found a workaround
<ScrollView>
<LinearLayout>
<MotionLayout>
and after the animation is completed
override fun onTransitionCompleted(contentContainer: MotionLayout?, p1: Int) {
val field = contentContainer::class.java.getDeclaredField("mEndWrapHeight")
field.isAccessible = true
val newHeight = field.getInt(contentContainer)
contentContainer.requestNewSize(contentContainer.width, newHeight)
}
requestViewSize this is an extension function
internal fun View.requestNewSize(width: Int, height: Int) {
layoutParams.width = width
layoutParams.height = height
layoutParams = layoutParams
}
if you twitch when changing height, just add animateLayoutChanges into your main container and your MotionLayout.
Add if necessary in code
yourView.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
--------UPDATE--------
I think I found a more correct option for animating the change in height.
Just the first line in the method onTransitionEnd, insert scroll.fullScroll (ScrollView.FOCUS_UP). I added so that the code for changing the height is executed in 500 milliseconds
override fun onTransitionCompleted(contentContainer: MotionLayout?, currentState: Int) {
if (currentState == R.id.second_state) {
scroll.fullScroll(ScrollView.FOCUS_UP)
GlobalScope.doAfterDelay(500) {
if (currentState == R.id.second_state) {
val field = contentContainer::class.java.getDeclaredField("mEndWrapHeight")
field.isAccessible = true
val newHeight = field.getInt(contentContainer)
contentContainer.requestNewSize(contentContainer.width, newHeight)
}
}
}
}
doAfterDelay is a function extension for coroutine
fun GlobalScope.doAfterDelay(time: Long, code: () -> Unit) {
launch {
delay(time)
launch(Dispatchers.Main) { code() }
}
}
But you can use alitenative

Categories

Resources