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
}
Related
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
First: I created a sample project showing this problem. By now I begin to think that this is a bug in either RecyclerView or MotionLayout.
https://github.com/muetzenflo/SampleRecyclerView
This project is set up a little bit different than what is described below: It uses data binding to toggle between the MotionLayout states. But the outcome is the same. Just play around with toggling the state and swiping between the items. Sooner than later you'll come upon a ViewHolder with the wrong MotionLayout state.
So the main problem is:
ViewHolders outside of the screen are not updated correctly when transition from one MotionLayout state to another.
So here is the problem / What I've found so far:
I am using a RecyclerView.
It has only 1 item type which is a MotionLayout (so every item of the RV is a MotionLayout).
This MotionLayout has 2 states, let's call them State big and State small
All items should always have the same State. So whenever the state is switched for example from big => small then ALL items should be in small from then on.
But what happens is that the state changes to small and most(!) of the items are also updated correctly. But one or two items are always left with the old State. I am pretty sure it has to do with recycled ViewHolders. These steps produce the issue reliably when using the adapter code below (not in the sample project):
swipe from item 1 to the right to item 2
change from big to small
change back from small to big
swipe from item 2 to the left to item 1
=> item 1 is now in the small state, but should be in the big state
Additional findings:
After step 4 if I continue swiping to the left, there comes 1 more item in the small state (probably the recycled ViewHolder from step 4). After that no other item is wrong.
Starting from step 4, I continue swiping for a few items (let's say 10) and then swipe all the way back, no item is in the wrong small state anymore. The faulty recycled ViewHolder seems to be corrected then.
What did I try?
I tried to call notifyDataSetChanged() whenever the transition has completed
I tried keeping a local Set of created ViewHolders to call the transition on them directly
I tried to use data-binding to set the motionProgress to the MotionLayout
I tried to set viewHolder.isRecycable(true|false) to block recycling during the transition
I searched this great in-depth article about RVs for hint what to try next
Anyone had this problem and found a good solution?
Just to avoid confusion: big and small does not indicate that I want to collapse or expand each item! It is just a name for different arrangement of the motionlayouts' children.
class MatchCardAdapter() : DataBindingAdapter<Match>(DiffCallback, clickListener) {
private val viewHolders = ArrayList<RecyclerView.ViewHolder>()
private var direction = Direction.UNDEFINED
fun setMotionProgress(direction: MatchCardViewModel.Direction) {
if (this.direction == direction) return
this.direction = direction
viewHolders.forEach {
updateItemView(it)
}
}
private fun updateItemView(viewHolder: RecyclerView.ViewHolder) {
if (viewHolder.adapterPosition >= 0) {
val motionLayout = viewHolder.itemView as MotionLayout
when (direction) {
Direction.TO_END -> motionLayout.transitionToEnd()
Direction.TO_START -> motionLayout.transitionToStart()
Direction.UNDEFINED -> motionLayout.transitionToStart()
}
}
}
override fun onBindViewHolder(holder: DataBindingViewHolder<Match>, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
val itemView = holder.itemView
if (itemView is MotionLayout) {
if (!viewHolders.contains(holder)) {
viewHolders.add(holder)
}
updateItemView(holder)
}
}
override fun onViewRecycled(holder: DataBindingViewHolder<Match>) {
if (holder.adapterPosition >= 0 && viewHolders.contains(holder)) {
viewHolders.remove(holder)
}
super.onViewRecycled(holder)
}
}
I made some progress but this is not a final solution, it has a few quirks to polish. Like the animation from end to start doesn't work properly, it just jumps to the final position.
https://github.com/fmatosqg/SampleRecyclerView/commit/907ec696a96bb4a817df20c78ebd5cb2156c8424
Some things that I changed but are not relevant to the solution, but help with finding the problem:
made duration 1sec
more items in recycler view
recyclerView.setItemViewCacheSize(0) to try to keep as few unseen items as possible, although if you track it closely you know they tend to stick around
eliminated data binding for handling transitions. Because I don't trust it in view holders in general, I could never make them work without a bad side-effect
upgraded constraint library with implementation "androidx.constraintlayout:constraintlayout:2.0.0-rc1"
Going into details about what made it work better:
all calls to motion layout are done in a post manner
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
Compared actual vs desired properties
val goalProgress =
if (currentState) 1f
else 0f
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
if (motionLayout.currentState != desiredState) {
safeRunBlock {
startTransition(currentState)
}
}
}
This would be the full class of the partial solution
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
val motionLayout: MotionLayout =
binding.root.findViewById<MotionLayout>(R.id.root_item_recycler_view)
.also {
it.setTransitionDuration(1_000)
it.setDebugMode(DEBUG_SHOW_PROGRESS or DEBUG_SHOW_PATH)
}
var lastPosition: Int = -1
fun bind(item: T, position: Int, layoutState: Boolean) {
if (position != lastPosition)
Log.i(
"OnBind",
"Position=$position lastPosition=$lastPosition - $layoutState "
)
lastPosition = position
setMotionLayoutState(layoutState)
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
fun setMotionLayoutState(currentState: Boolean) {
val goalProgress =
if (currentState) 1f
else 0f
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
if (motionLayout.currentState != desiredState) {
Log.i("Pprogress", "Desired doesn't match at position $lastPosition")
safeRunBlock {
startTransition(currentState)
}
}
}
}
fun startTransition(currentState: Boolean) {
if (currentState) {
motionLayout.transitionToStart()
} else {
motionLayout.transitionToEnd()
}
}
}
Edit: added constraint layout version
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.
The code above is the RecyclerViewAdapter, which changes color only when it is the first item, as shown below.
class TestAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val textColor1 = Color.BLACK
private val textColor2 = Color.YELLOW
private val items = ArrayList<String>()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textColor = if(position==0) textColor1 else textColor2
holder.itemView.textView.setTextColor(textColor)
holder.itemView.textView.text = items[position]
}
fun move(from:Int,to:Int){
val item = items[from]
items.remove(item)
items.add(to,item)
notifyItemMoved(from,to)
}
}
In this state I would like to move Value 3 to the first position using the move function. The results I want are shown below.
But in fact, it shows the following results
When using notifyDataSetChanged, I can not see the animation transition effect,
Running the onBindViewHolder manually using findViewHolderForAdapterPosition results in what I wanted, but it is very unstable. (Causing other parts of the error that I did not fix)
fun move(from:Int,to:Int){
val item = items[from]
val originTopHolder = recyclerView.findViewHolderForAdapterPosition(0)
val afterTopHolder = recyclerView.findViewHolderForAdapterPosition(from)
items.remove(item)
items.add(to,item)
notifyItemMoved(from,to)
if(to==0){
onBindViewHolder(originTopHolder,1)
onBindViewHolder(afterTopHolder,0)
}
}
Is there any other way to solve this?
Using the various notifyItemFoo() methods, like moved/inserted/removed, doesn't re-bind views. This is by design. You could call
if (from == 0 || to == 0) {
notifyItemChanged(from, Boolean.FALSE);
notifyItemChanged(to, Boolean.FALSE);
}
in order to re-bind the views that moved.
notifyItemMoved will not update it. According to documentation:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter
This is a structural change event. Representations of other existing items in the data set are still considered up to date and will not be rebound, though their positions may be altered.
What you're seeing is expected.
Might want to look into using notifyItemChanged, or dig through the documentation and see what works best for you.
I would like to let my MainActivity know that there is something to do (e.g. calculate stuff) from a custom View. The View detects user inputs through touch and the MainActivity has to update certain user controls with a calculation from those View-Values. Basically I did an override on onTouchEvent:
override fun onTouchEvent(event: MotionEvent?): Boolean {
val x = event?.x
val y = event?.y
val dX = x?.minus(prevX)
val dY = y?.minus(prevY)
if(dX!! > 0){
lowerBound = x.toInt()
} else{
upperBound = x.toInt()
}
prevX = x!!.toInt()
prevY = y!!.toInt()
this.invalidate() //tell view to redraw
return true
}
How can I let MainActivityknow that lowerBound and upperBound updated?
As already recommended in the comments I'll go a little bit deeper in "hot to use a listener".
First of all, a "Listener" is nothing more than a interface which get called if something happen elsewhere. The best example is the View.setOnClickListener(View).
How to use that in your case?
Simply define a interface (best location is here inside your custom View):
interface OnBoundsUpdatedListener {
fun onBoundsUpdated(upperBound: Int, lowerBound: Int)
}
Then you have to create a property in your CustomView and call the Listener if the bounds have changed :
// Constructor and stuff
val onBoundsUpdateListener: OnBoundsUpdatedListener? = null
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
onBoundsUpdateListener?.onBoundsUpdated(upperBound, lowerBound)
...
}
In your Activtiy you can find that View and set the listener:
val myCustomView = findViewById<MyCustomView>(R.id.id_of_your_customview)
myCustomView.onBoundsUpdateListener = object : OnBoundsUpdatedListener {
override fun onBoundsUpdated(upperBound, lowerBound) {
// Get called if its called in your CustomView
}
}
Note: The code can be simplified. But that is the basic stuff 😉