HI i have the below animation for a view below:
val duration = 2000L
val visible = 1.0f
imageAVater.apply {
animate().translationYBy(-100f).alpha(visible).setDuration(duration).setListener(object : AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
visibility = View.VISIBLE
}
})
}
i want it to move from slightly off-position and into position and to also reveal itself by setting the alpha .
SO far neither works.
ALl the code above does is move the image from current default position on my layout(lets say i positioned it in xml along the Y axis position 200) and then it moves from position 200 to position 100 and also the alpha does not work, the item is visible all the time despite it being set to View.Gone in my xml
android:visibility="gone"
How can i set a start and end Y axis value for this translation animation and how can i get the alpha to work so that the view appears from hidden/gone?
i want it to start at 200 y and have it transition to 100 y and to also reveal itself from being hidden/gone to being shown at the same time as the transition
you need to specify starting values for y and alpha. EG.
imageAVater.apply {
alpha = 0f
animate().alpha(1f).setDuration(2000).start()
}
leave the view's visibility always to visible. You can not animate a view that is gone
Related
I have a SwipeToDismiss instance to delete items with dismissThresholds 75%.
If user swipes row too fast without reaching 75% threshold the row being deleted. How to prevent that?
Here is code where I execute an action:
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.deleteCity(city)
}
true
}
)
I ran into the same issue and found a fix that works for me. My theory is, that when the swipe is very fast but doesn't go very far (doesn't reach the set fractional threshold), the layout just immediately resets. In this case (DismissToStart), meaning the view snaps back to the right screen edge, giving us the value of 1.0f for the threshold and thus triggering the confirmStateChange because the fraction is per definition higher than our threshold. The problem being that our threshold is measured from the right screen edge, and this fracion (in my theory) is measured from the left screen edge.
So my solution is to track the current fractional value, and inside confirmStateChange check if the current value is higher than the threshold, BUT NOT 1.0f. In a real world scenario, I think it's impossible to reach 1.0 with actual swiping the finger from right to left, so the solution seems safe to me.
val dismissThreshold = 0.25f
val currentFraction = remember { mutableStateOf(0f) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
if (currentFraction.value >= dismissThreshold && currentFraction.value < 1.0f) {
onSwiped(item)
}
}
dismissOnSwipe
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.animateItemPlacement(),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(dismissThreshold)
},
background = {
Box(...) {
currentFraction.value = dismissState.progress.fraction
...
}
}
dismissContent = {...}
)
I'm dealing with the same issue, it seems to be a feature, not a bug, but I did not find the way to disable it.
*If the user has dragged their finger across over 50% of the screen
width, the app should trigger the rest of the swipe back animation. If
it's less than that, the app should snap back to the full app view.
If the gesture is quick, ignore the 50% threshold rule and swipe back.*
https://developer.android.com/training/wearables/compose/swipe-to-dismiss
I have viewpager having 2 items on second item i want to show button translating from downwards on swiping back to first item.
Below is my code:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if(position == 1){
signIn.visibility = View.VISIBLE
signIn.animate().translationY(signIn.height.toFloat())
}
else{
signIn.visibility = View.INVISIBLE
signIn.animate().translationY(signIn.height.toFloat())
}
}
})
How can I implement the above functionality?
signIn won't move in Y direction because you are feeding it the same value - signIn.height.toFloat.
On top of that, you are making the signIn button INVISIBLE before you try to animate. You won't see any animation.
Here is how I would do it. Say, I want to hide the signIn button with vertical slide (to bottom) + fade out animation.
if (position == 1) {
// SHOW
// assuming that the button is gone before showing it
signIn.setTranslationY(100); // setting the starting Y position of the button
signIn.setAlpha(0f); // zeroing the alpha
signIn.setVisibility(View.VISIBLE); // now we can make it visible and start animating
signIn.animate().setDuration(500)
.translationY(0) // Y goes down from 100 to 0
.alpha(1f) // alpha goes form 0 to 1
.start();
} else {
// HIDE
signIn.animate()
.setDuration(500)
.translationY(100) // it animates Y of the button from 0 to 100
.alpha(0) // animates alpha from 1 to 0
.withEndAction(new Runnable() {
#Override
public void run() {
// this code runs when the animation ends. Every time when you hide a
// button with an animation, you should set its visibility to `GONE` not
// `INVISIBLE` because, `INVISIBLE` buttons can still be clicked which may
// produce undesired flow in your app
signIn.setVisibility(View.GONE);
}
}).start();
}
PS. The code snipped I wrote is in Java, you can still copy and paste it to you Editor and it will automatically convert it to Kotlin.
Don't just set visibility to INVISIBLE immediately before you animate it, you will be animating an invisible view, aka it will not show the animation. Instead, make it invisible after the translation ends.
You are translating Y with a positive value each time. The view will eventually end up outside of the screen. You should be doing a negative translation in one animation, and positive in the opposite animation.
Even better, instead of changing visibility, why don't you just animate alpha with the Y translation.
EDIT:
From docs
private fun crossfade() {
contentView.apply {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
alpha = 0f
visibility = View.VISIBLE
// Animate the content view to 100% opacity, and clear any animation
// listener set on the view.
animate()
.alpha(1f)
.setDuration(shortAnimationDuration.toLong())
.setListener(null)
}
// Animate the loading view to 0% opacity. After the animation ends,
// set its visibility to GONE as an optimization step (it won't
// participate in layout passes, etc.)
loadingView.animate()
.alpha(0f)
.setDuration(shortAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loadingView.visibility = View.GONE
}
})
}
Now, you just need to add your translationY() to the animate() chain, like this:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if(position == 1){
signIn.apply {
alpha = 0
visibility = view.VISIBLE
animate()
.alpha(1f)
.translationY(signIn.height.toFloat())
}
}
else{
signIn.animate()
.alpha(0f)
.translationY(-signIn.height.toFloat())
.withEndAction {signIn.visibility = View.GONE}
}
}
}
})
I want to scroll to a particular item in my RecyclerView, but using LinearLayoutManager.scrollToPosition(position: Int), the item is sometimes at the top of the screen and sometimes at the bottom.
I want it to scroll to this item and place it at the center of the screen.
I can achieve this with a smooth scroller using other answers, but I often want to jump very far in the list, so waiting for the smooth scroll to complete is unacceptable.
I found many answers for achieving this with a smooth scroller, but in case you want to achieve this without smooth scrolling (to immediately jump a large distance for example), you can do it with a normal LinearLayoutManager/GridLayoutManager by calculating the offset yourself.
I subclassed LinearLayoutManager so that I could just use the scrollToPosition function and have it always scroll to that position with the item in the center, but you can just use the scrollToPositionWithOffset function of the LinearLayoutManager with a manual offset each time if you prefer.
class CenterScrollLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean): LinearLayoutManager(context, orientation, reverseLayout) {
override fun scrollToPosition(position: Int) {
//this will place the top of the item at the center of the screen
val height = getApplicationContext().resources.displayMetrics.heightPixels
val offset = height/2
//if you know the item height, you can place the center of the item at the center of the screen
// by subtracting half the height of that item from the offset:
// val height = getApplicationContext().resources.displayMetrics.heightPixels
// //(say item is 40dp tall)
// val itemHeight = 40F * getApplicationContext().resources.displayMetrics.scaledDensity
// val offset = height/2 - itemHeight/2
//depending on if you have a toolbar or other headers above the RecyclerView,
// you may want to subtract their height as well:
// val height = getApplicationContext().resources.displayMetrics.heightPixels
// //(say item is 40dp tall):
// val itemHeight = 40F * getApplicationContext().resources.displayMetrics.scaledDensity
// //(say toolbar is 56dp tall, which is the default action bar height for portrait mode)
// val toolbarHeight = 56F * getApplicationContext().resources.displayMetrics.scaledDensity
// val offset = height/2 - itemHeight/2 - toolbarHeight
//call scrollToPositionWithOffset with the desired offset
super.scrollToPositionWithOffset(position, offset)
}
}
Then, to use it, you need to first set this as the layout manager for your RecyclerView, likely inside OnCreate (if in an activity) or OnViewCreated (if in a fragment):
yourRecyclerView.apply {
adapter = yourRecyclerViewAdapter
layoutManager = CenterScrollLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
Then, just scroll to a desired position, and the item should be in the center:
yourRecyclerView.scrollToPosition(desiredPosition)
This also works with a GridLayoutManager.
I have a circle at the center of the screen inside which there's an ImageView + TextView. I have another two ImageView+TextView, one at the top and another at bottom of the screen.
My requirement is :
I want a copy of the top ImageView+TextView and a copy of the bottom ImageView+TextView to move in animation into the center of the circle, thereby changing the value of the textView inside the circle.
For example:
Say top textView has value 200 and bottom textview has value 300. I want a portion of those values (say 100 or 150) to animate and move into the circle, but the original values 200 and 300 should remain on the same position.
I've tried using TranslateAnimation. However I face issues finding the x and y coordinates of the center circle. It is not exactly going to the center of the circle. Also original view's position is not retained.
TranslateAnimation animation = new
TranslateAnimation(startLayout.getX(),endLayout.getX(),
startLayout.getY(),endLayout.getY);
animation.setDuration(1000);
animation.setFillAfter(false);
startView.startAnimation(animation);
startLayout is the linearlayout in which ImageView and TextView reside.
Please help! Thanks!
I had the same issue and I fixed by using the next code (sorry is in Kotlin, but works the same in Java).Let's say viewFirst wants to reach viewTwo position:
(DON'T USE):
viewFirst.animate()
.translationX(viewSecond.x)
.translationY(viewSecond.y)
.setDuration(1000)
.withEndAction {
//to make sure that it arrives,
//but not needed actually these two lines
viewFirst.x = viewSecond.x
viewFirst.y = viewSecond.y
}
.start()
(USE THIS SOLUTION):
viewFirst.animate()
.x(viewSecond.x)
.y(viewSecond.y)
.setDuration(1000)
.withEndAction {
//to make sure that it arrives,
//but not needed actually these two lines
viewFirst.x = viewSecond.x
viewFirst.y = viewSecond.y
}
.start()
Using the getX() and getY() methods define the position of the view in pixels, but the constructor you use defines Float type values that must be values from 0.0f to 1.0f
TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
This is another option using the view`s position in pixels:
viewFirst.animate()
.x(viewSecond.getX())
.y(viewSecond.getY())
.setDuration(1000).withEndAction(new Runnable() {
#Override
public void run() {
viewFirst.setX(tv2.getX());
viewFirst.setY(tv2.getY());
}
}).start();
Try this for accurate coordinates
private fun moveView(viewToBeMoved: View, targetView: View) {
val targetX: Float =
targetView.x + targetView.width / 2 - viewToBeMoved.width / 2
val targetY: Float =
targetView.y + targetView.height / 2 - viewToBeMoved.height / 2
viewToBeMoved.animate()
.x(targetX)
.y(targetY)
.setDuration(2000)
.withEndAction {
targetView.visibility = View.GONE
}
.start()
}
I have a View that has an OnClickListener. When clicked, the view translates up to a certain position on the page. This is no problem, the view goes where it should. When the view is clicked again, I would like to position it somewhere else, but this is not the case. After a little bit of trouble shooting, I found that my View's getTop() method returns the same value - even after the translation animation has moved the view to a different part of the screen. For the second animation, it is not using the current position (as I would expect), it instead uses the initial position.
Few things that I am doing: I am using the ObjectAnimation class rather than the TranslationAnimation class, since I wanted to keep the OnClickListener functioning. With the TranslationAnimation class, I found that the view was correctly moved, but the OnClickListener was only working in the area that the View started from. Using the ObjectAnimation class, I was able to easily get the translation to work AND the OnClickListener functions correctly - it is triggered where the view currently is on the screen.
Here's what I have so far:
final LinearLayout child = layouts.get(i); //ArrayList containing some dynamic layouts
final int offset = target - child.getTop();
ObjectAnimator anim = ObjectAnimator.ofFloat(child,"translationY",offset);
anim.setDuration(250);
anim.start();
This is what happens when the view is clicked the first time. It translates up along the Y axis, where the offset determines how far the View needs to move from its current position.
Now, here's what happens on the second click. The goal here was to align the view with the parent's base.
target = parent.getBottom();
offset = target - child.getTop();
anim = ObjectAnimator.ofFloat(child, "translationY",offset);
anim.setDuration(250);
anim.start();
prev = child;
This is where things fall apart - child.getTop() returns the Y coordinate of the view's ORIGINAL position. Not the current position. So after the animation, the view is placed well below the bottom of the parent. I read a different question which stated that I should use child.getY() instead, which is supposed to give me the translationY position plus the top position, but this didn't lead to any better results. I can't seem to get this to work just right. I'd simply like to move the view from its current position to the bottom of the screen, but this appears to be a hard thing to accomplish. Any help would be appreciated.
EDIT
I have added an animation listener:
ObjectAnimator anim = ObjectAnimator.ofFloat(child,"translationY",offset);
anim.setDuration(250);
anim.addListener(new ObjectAnimator.AnimatorListener(){
#Override
public void onAnimationStart(Animator animation) {
System.out.println("start: " + child.getTop() + " " + child.getY());
}
#Override
public void onAnimationEnd(Animator animation) {
System.out.println("end: " + child.getTop() + " " + child.getY() + " " + child.getTranslationY());
child.setTop((int)child.getY());
System.out.println(child.getTop());
}
#Override
public void onAnimationCancel(Animator animation) {}
#Override
public void onAnimationRepeat(Animator animation) {}
});
anim.start();
Here I am setting the listener to try to change where the Top of the view is located. Behaviour is again not working as expected. The view is actually sent up above the screen when I do this. Output of the System.out looks like this:
start: 2008 2008.0
end: 2008 478.0 -1530.0
478
So calling child.getTop() after the animation is complete and setting a new position returns a positive integer, but the view is not actually completely on screen. It is above the screen, partly visible. The height of the view itself is about 700px. I am still so confused as to why this is such a hard thing to accomplish.
EDIT 2
I have also tried setting layoutparams inside the onAnimationEnd method:
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams();
params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
params.topMargin = (int)child.getY();
child.setLayoutParams(params);
Result: child.getTop() still returns the original position of 2008.
You can get the very bottom of the screen coordinates like this :
float bottomOfScreen = getResources().getDisplayMetrics().heightPixels;
but you probably want it minus the height of your LinearLayout or else your LinearLayout will be cut off by the bottom :
float bottomOfScreen = getResources().getDisplayMetrics().heightPixels
- child.getHeight();
// if you want a little more space to the bottom
// try something like - child.getHeight()*2;
Then use ViewPropertyAnimator to animate your LL like this :
child.animate()
.translationY(bottomOfScreen)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setDuration(250);
The Interpolator is just to make the animation more realistic.
In the case that child.getHeight() returns 0 , your Linear Layout has not been finished setting up by the system, in that case you might want to do something like :
child.post(new Runnable() {
#Override
public void run() {
float bottomOfScreen = getResources().getDisplayMetrics().heightPixels
- child.getHeight()*2;
child.animate()
.translationY(bottomOfScreen)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setDuration(250);
}
});
Remember that a duration of 250 milliseconds is very fast, and does usually not look cool translating stuff on the screen, so you might want to set it a little higher, but thats just a matter of taste.