I have problem with any type of animations. I want to make material banner behavior, but with other animations. Actually I got the result, but the problem is that view is blinking after the animation. My code:
First example:
val anim = TranslateAnimation(1f, 1f, 1f, 0f)
anim.duration = 300
banner.startAnimation(anim)
banner.visibility = View.INVISIBLE
Second example
val mTransition = Slide(Gravity.END)
mTransition.setDuration(300)
mTransition.addTarget(banner)
TransitionManager.beginDelayedTransition(banner, mTransition)
banner.setVisibility(View.GONE)
Can someone explain how to avoid blinking of the view and why it is happening.
The problem is on the code banner.visibility = View.INVISIBLE and banner.setVisibility(View.GONE). Try to remove it.
If you want to the banner is gone after the animation ended. Try to add a listener on the animation and hide the banner after the animation ended:
val anim = TranslateAnimation(1f, 1f, 1f, 0f)
anim.duration = 300
anim.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
// banner.visibility = View.INVISIBLE
// or
// banner.setVisibility(View.GONE)
}
override fun onAnimationStart(animation: Animation?) {
}
})
I solved problem of blinking of the view by animating it on other way. I used following strategy. First of all I used Guideline component of the ConstraintLayout. I constraint my banner to the top of it and place parameter layout_constraintGuide_begin = "0dp". After that I used ValueAnimator in order to get animated value for my Guideline and changed the guidebegin params of it(see the code).
val params: ConstraintLayout.LayoutParams = guideline2.layoutParams as ConstraintLayout.LayoutParams
animBanner = ValueAnimator.ofInt(0, banner.height + toolbar.height)
animBanner!!.addUpdateListener {
params.guideBegin = it.getAnimatedValue() as Int
guideline2.layoutParams = params
}
This is the declaration of animation. At the end it is enough to use animBanner.start() for starting the animation and animBanner.reverse() for reverse animation (hiding banner).
I understand what was my problem with the help of #John Lee, but it solution does not was suitable for me, so I used Guideline component with AnimatedValue. My solution:
params = view.layoutParams as ConstraintLayout.LayoutParams
anim = ValueAnimator.ofInt(fromY, toY)
anim.addUpdateListener {
params.guideBegin = it.animatedValue as Int
view.layoutParams = params
}
So, here fromY value is 0, toY value is height of banner and view is Guideline, which could received by view.height. I should mention that first my banner is constrained to top of Guideline, which is placed at constraintGuide_begin=0. Then I animated this guideline with help of code above and using anim.start(), anim.reverse() methods.
Related
This question may seem a little bit odd, but let me explain my problem.
I have a BottomSheetDialogFragment with rounded top corners. When I fully expand my dialog those corners will be filled automatically. I want the height to be more or less fixed, so isFitToContents is set to false.
Preferably I want my expanded state to have a slight margin to the top and still transparent corners, so the user is able to see a bit of the underlying layout. STATE_HALF_EXPANDED with a halfExpandedRatio of something like 0.95 is pretty much it.
But then the user is still able to switch to STATE_EXPANDED with a swipe up, which is weird, because there is almost no difference in height between both states, so this seems unnecessary.
Is there a way to make STATE_HALF_EXPANDED the maximum (disable STATE_EXPANDED) or, as an alternative, can I make STATE_EXPANDED behave as described and skip STATE_HALF_EXPANDED instead?
It seems like a really small thing, but I didn't find a way to achieve this behavior yet.
(I'm using XML layouts if this is relevant.)
This is what I currently apply to the dialog in the onShowListener:
isFitToContents = false
halfExpandedRatio = 0.95f
state = BottomSheetBehavior.STATE_HALF_EXPANDED
skipCollapsed = true
And in onViewCreated I ensure the parent layout's height is MATCH_PARENT, so the area below my inflated layout is not transparent:
val parentLayout = dialog?.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
parentLayout?.layoutParams?.height = ViewGroup.LayoutParams.MATCH_PARENT
This is the maximum state that I want
This is the state I want to disable
You can use following function in your bottom sheet, this may helpful for your problem.
override onCreateDialog()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener { dialogInterface: DialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
setupFullHeight(bottomSheetDialog)
}
return dialog
}
copy this function :
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
val bottomSheet = bottomSheetDialog.findViewById<FrameLayout>(R.id.design_bottom_sheet)
//BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
if (bottomSheet != null) {
val layoutParams = bottomSheet.layoutParams
val windowHeight = windowHeight
if (layoutParams != null) {
layoutParams.height = windowHeight
}
bottomSheet.layoutParams = layoutParams
BottomSheetBehavior.from(bottomSheet).setPeekHeight((windowHeight * 0.8).toInt(), _do)
}
}
EDITED - Missed variable updated.
private val windowHeight: Int
get() = if (activity != null && !requireActivity().isDestroyed && !requireActivity().isFinishing) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = requireActivity().windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
windowMetrics.bounds.height() - insets.top - insets.bottom
} else {
val displayMetrics = DisplayMetrics()
#Suppress("DEPRECATION")
requireActivity().windowManager
.defaultDisplay.getMetrics(displayMetrics)
displayMetrics.heightPixels
}
} else 0
I found a soulution which uses STATE_EXPANDED as the only available state, since I finally managed to keep the rounded corners clear/transparent in that state.
Originally I was just cutting out the corners using this ShapeAppearance,
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">32dp</item>
</style>
which I then applied to the BottomSheet.Modal style. Instead I now set the whole background of the BottomSheetDialogFragment transparent, what I couldn't accomplish before. But now it works using the following styles:
<style name="BottomSheetDialogThemeOverlay" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="bottomSheetStyle">#style/BottomSheetDialogStyle</item>
</style>
<style name="BottomSheetDialogStyle" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="backgroundTint">#color/transparent</item>
</style>
Obviously you need to apply them to you AppTheme using this line:
<item name="bottomSheetDialogTheme">#style/BottomSheetDialogThemeOverlay</item>
Then I was able to add a proper drawable with rounded corners as background inside the dialog's layout.
Final behavior setup
To make STATE_EXPANDED the initial and only available state to the dialog I use the following code inside onCreateView:
dialog?.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
bottomSheetDialog.behavior.apply {
isFitToContents = false
skipCollapsed = true
expandedOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics).toInt()
// Just using Int (pixel value) would be fine here, but I prefer dp
state = BottomSheetBehavior.STATE_EXPANDED
}
}
And additionally I need to override onViewCreated to achieve match_parent in my dialog root layout's height behaving as it should:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val parent = dialog?.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
parent?.layoutParams?.height = ViewGroup.LayoutParams.MATCH_PARENT
}
That's all I needed to achieve the behavior described in my question.
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())
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.
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
}
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