I am making a productivity app with events that are represented as buttons holding basic information about the event.
The button is contained in a RelativeLayout alongside TextViews, an options menu and a switch. The parent layout's height is set to WRAP_CONTENT. It looks like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<Button
android:id="#+id/event_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="#+id/relativeLayout"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="2dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:minHeight="130dp"
android:elevation="0dp"
android:textSize="#dimen/event_item_title_fontSize"
android:visibility="visible"
/>
<include
android:id="#+id/relativeLayout"
layout="#layout/_event_layout_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</include> </RelativeLayout>
You can click the button to expand it and show additional information. The expand animation is achieved using a custom animation which updates the button's getLayoutParams().height and calls getParent().requestLayout() every time applyTransformation(...) is called. This works well on an API level 19 (KitKat) device.
Animation code:
//The current state of the button which gets updated on animation finish
boolean expanded = false;
#Override
public void onClick(final View v) {
//Getting references to views
final RelativeLayout layout = (RelativeLayout) button.getParent();
final View separator = layout.findViewById(R.id.separator), list =
layout.findViewById(R.id.notes_list);
final Button button = (Button) v;
final RelativeLayout content = (RelativeLayout) layout.findViewById(R.id.relativeLayout);
final RelativeLayout.LayoutParams lpl = (RelativeLayout.LayoutParams) list.getLayoutParams(),
lpsw = (RelativeLayout.LayoutParams) layout.findViewById(R.id.event_switch).getLayoutParams(),
lp = (RelativeLayout.LayoutParams) button.getLayoutParams();
//Finished getting references to views
final int startHeight = lp.height = button.getHeight();
//Prevents button from automatically resizing to fit its parent when requestLayout is called
lp.removeRule(RelativeLayout.ALIGN_BOTTOM);
layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
layout.removeOnLayoutChangeListener(this);
final int goalHeight = GetExpectedHeight(expanded);
Animation animation = new Animation() {
float alpha1 = (expanded ? startHeight : goalHeight) - separator.getTop(),
alpha2 = (expanded ? startHeight : goalHeight) - list.getTop();
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime < 1) {
//Updates the button's height
lp.height = (int) (startHeight + (goalHeight - startHeight) * interpolatedTime);
separator.setAlpha((lp.height - separator.getTop()) / alpha1);
list.setAlpha((lp.height - list.getTop()) / alpha2);
list.setScaleX(list.getAlpha());
list.setScaleY(list.getAlpha());
layout.requestLayout();
return;
}
//Animation finish
if (expanded) {
separator.setVisibility(View.GONE);
list.setVisibility(View.GONE);
}
lp.height = lp.MATCH_PARENT;
lp.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.relativeLayout);
//A custom method that basically requests layout for whole activity
instance.get().Invalidate();
expanded = !expanded;
this.cancel();
}
};
animation.setInterpolator(new DecelerateInterpolator());
animation.setDuration(400);
layout.startAnimation(animation);
}
});
float alpha = expanded ? 1 : 0;
separator.setAlpha(alpha);
list.setAlpha(alpha);
//Kicks off the onLayoutChange method above
if (!expanded) {
separator.setVisibility(View.VISIBLE);
list.setVisibility(View.VISIBLE);
}
else
layout.requestLayout();
}
However, on an Android 7.0 Nougat (API 24), there is an accompanying ripple effect which overlays the button with a rectangle that doesn't update its height to match the button's height during the animation. I don't know what it looks like on other Android versions because those are the only two devices I can test on. Is there a way to update the ripple effect's rectangle to scale with the button? I would prefer to keep the ripple effect if at all possible. So far I have tried calling performClick() on the button each frame of the animation but to no avail.
Here is a screenshot of the button during expand animation:
And a video of the animation:
https://streamable.com/vehf3
I would like to smoothly animate some views Y position based on the user scrolling a bottom sheet.
http://imgur.com/dVBlh83
However the animation is slightly jerky.
This is probably because scrolling a bottomsheet gives me a slideOffset at some interval, which is too infrequent to setTranlationY like I'm currently doing.
Is there a better way to do this.
Bottom sheet callback
BottomSheetBehavior.BottomSheetCallback() {
#Override public void onSlide(#NonNull View bottomSheetView, float slideOffset) {
int offset = (int) (slideOffset * 100);
if (offset > 0) {
scrollingHeaderView.animate(offset);
}
}
});
scrollingHeaderView
public void animate(int offset) {
position = -(offset / 2);
arrow.setTranslationY(offset * ANIMATION_RATIO_ARROW);
detailView.setTranslationY(offset * ANIMATION_RATIO);
}
I have tried using ObjectAnimator to create lots of small animations at each step.
ObjectAnimator.ofFloat(arrow, "translationY", arrow.getTranslationY(), position * ANIMATION_RATIO_ARROW).start();
in case it helps anyone else. I got it looking much better.
First i set the ration by pixel * range of values i'd get. So offset will be 0 -> 100. Mutliplied by the dp i want the view to move
private static int ANIMATION_RANGE = 100;
arrowScale = -(getResources().getDimensionPixelOffset(R.dimen.arrow_distance) / ANIMATION_RANGE);
containerScale = -(getResources().getDimensionPixelOffset(R.dimen.detail_distance) / ANIMATION_RANGE);
then as the value changes i set this using translationY
arrow.setTranslationY(position * arrowScale);
detailView.setTranslationY(position * containerScale);
I also needed a reset method, which animate back to the original position
public void reset() {
arrow.animate().setDuration(100).translationY(0);
detailView.animate().setDuration(100).translationY(0);
}
For having effects I scale up the child view which cause child view to go out side of its parent view. I have a button in child view, it works before scaling but after scaling doesn't work. what is going wrong? see image below:
for scaling child I use this code:
childView.bringToFront();
Animation a = new Animation() {
#Override
protected void applyTransformation(float t, Transformation trans) {
float scale = 1f * ( 1 - t ) + SCALE_UP_FACTOR * t;
childView.setScaleX(scale);
childView.setScaleY(scale);
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration(ANIM_DURATION);
a.setInterpolator(new Interpolator() {
#Override
public float getInterpolation(float t) {
t -= 1f;
return (t * t * t * t * t) + 1f; // (t-1)^5 + 1
}
});
childView.startAnimation(a);
the parent is a ViewPager :
<ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/invoice_list_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f5f5f5"
android:layout_gravity="center"
android:clipChildren="false"
android:clipToPadding="false"
/>
This should do the trick:
final View grandParent = (View) childView.getParent().getParent();
grandParent.post(new Runnable() {
public void run() {
Rect offsetViewBounds = new Rect();
childView.getHitRect(offsetViewBounds);
// After scaling you probably want to append your view to the new size.
// in your particular case it probably could be only offsetViewBounds.right:
// (animDistance - int value, which you could calculate from your scale logic)
offsetViewBounds.right = offsetViewBounds.right + animDistance;
// calculates the relative coordinates to the parent
((ViewGroup)parent).offsetDescendantRectToMyCoords(childView, offsetViewBounds);
grandParent.setTouchDelegate(new TouchDelegate(offsetViewBounds, childView));
}
});
Though I'm not sure whether it will work with Animation, but for scaling you could use something like that instead:
float scale = ...; // your scale logic
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(childView,
PropertyValuesHolder.ofFloat("scaleX", scale),
PropertyValuesHolder.ofFloat("scaleY", scale));
animator.setDuration(ANIM_DURATION);
animator.start();
And pay attention to the line android:clipChildren="false" for your parent view in XML-file.
So, I have a Layout that contains a Button and an ImageView. When you press the button the ImageView should slide out from the button like I just pulled down a rolldown curtain (bushing other views below it down). Basically what the image below show. When you press the button again the ImageView should, unlike the gif, smoothly animates up again.
.
Using this SO question I've managed to animate the height from 0 to full size but in the wrong direction. I set the scaleType to "Matrix" and the default behaviour when setting the height is to show the part from the top down to [height].
For the animation I'll need the opposite. So if I would set the height to 50dp it would show the bottom 50dp. Then I can move the ImageView down at the same time it's being revealed, thus giving the rolldown curtain effect.
I've looked throught all the different layout and view options and found nothing that seems to do this. So I'm guessing I need to specify the transformation matrix. I looked through the android.graphics.Matrix class but it's a little but too complicated for me. I simply have no idea how to use it.
If there is another, easier, way to do this then that would be fantastic but if not then I really need help with the matrix.
I'm also including the code here:
The Rolldown View XML
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/sliding_accordion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="#drawable/acc_image"
android:contentDescription="#string/accord"
android:scaleType="matrix"
android:layout_below="#+id/acc_button"
android:layout_marginTop="-10dp"
android:layout_centerHorizontal="true"/>
<Button
android:id="#+id/acc_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
The implementation in code.
(Note, the MyCustomAnimation class is a copy-paste version of the class found here)
//Called from all constructors
private void create()
{
final Context context = getContext();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout layout = (RelativeLayout) inflater.inflate(R.layout.widget_accordion, this, false);
final Button theButton = (Button) layout.findViewById(R.id.topic_button);
final ImageView accordionView = (ImageView) layout.findViewById(R.id.sliding_accordion);
accordionView.setVisibility(INVISIBLE);
theButton.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
if (accordionView.getVisibility() == View.VISIBLE)
{
MyCustomAnimation a = new MyCustomAnimation(accordionView, 1000, MyCustomAnimation.COLLAPSE);
height = a.getHeight();
accordionView.startAnimation(a);
}
else
{
MyCustomAnimation a = new MyCustomAnimation(accordionView, 1000, MyCustomAnimation.EXPAND);
a.setHeight(height);
accordionView.startAnimation(a);
}
}
});
this.addView(layout);
}
This took a long time perfect. But I managed to do it after a lot of experimenting.
I animate the margins of the drawer but because of the unexpected behavior of negative margins the button that opens the drawer can not be positioned on top.
When the drawer is closed the XML looks like so:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/accordion"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.animationtest.drawer.Drawer
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/topic_drawer"
android:layout_centerHorizontal="true"
android:layout_marginTop="0dp"
android:visibility="invisible"/>
<com.animationtest.drawer.Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/topic_btn"
android:layout_marginTop="58dp"/>
</RelativeLayout>
Then when the button is pressed the top_margin of the drawer is increased until it has come to whatever position is needed (in this case drawerHeight - someOffset).
I used android.view.animation.Animation to animate the widget my applyTransformation function looks something like this (Note that mLayoutParams are the drawer params):
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int valueDifference = Math.abs(startValue - endValue);
float valueChange = interpolatedTime * valueDifference;
if(currentState.equals(State.COLLAPSED)) {
// is closed and I want to open it
mLayoutParams.topMargin = Math.round(interpolatedTime * valueDifference);
}
else {
// is opened and I want to close it
mLayoutParams.topMargin = valueDifference - Math.round(interpolatedTime * valueDifference);
}
drawerView.requestLayout(); //this is my drawer
}
Finally, to hide the top of the drawer as it moves, I overrode my DrawerView's dispatchDraw method to looks like so:
#Override
protected void dispatchDraw(Canvas canvas) {
float height = getHeight();
float top = height - ((LayoutParams) getLayoutParams()).topMargin;
Path path = new Path();
RectF rectF = new RectF(0.0f, top, getWidth(), height);
path.addRoundRect(rectF, 0.0f, 0.0f, Path.Direction.CW);
canvas.clipPath(path);
super.dispatchDraw(canvas);
}
One final note:
Because of the Button's position one would need to set the widgets margin as a negative number for it to align correctly in a list or layout. In this case it would have to be -58dp.
I have created a 3D flip of a view using this android tutorial
However, I have done it programmatically and I would like to do it all in xml, if possible. I am not talking about simply shrinking a view to the middle and then back out, but an actual 3D flip.
Is this possible via xml?
Here is the answer, though it only works with 3.0 and above.
1) Create a new resources folder called "animator".
2) Create a new .xml file which I will call "flipping". Use the following xml code:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:valueFrom="0" android:valueTo="360" android:propertyName="rotationY" >
</objectAnimator>
No, the objectAnimator tags do not start with an uppercase "O".
3) Start the animation with the following code:
ObjectAnimator anim = (ObjectAnimator) AnimatorInflater.loadAnimator(mContext, R.animator.flipping);
anim.setTarget(A View Object reference goes here i.e. ImageView);
anim.setDuration(3000);
anim.start();
I got all this from here.
Since the answers to this question are fairly dated, here is a more modern solution relying on ValueAnimators.
This solution implements a true, visually appealing 3D-flip, because it not just flips the view, but also scales it while it is flipping (this is how Apple does it).
First we set up the ValueAnimator:
mFlipAnimator = ValueAnimator.ofFloat(0f, 1f);
mFlipAnimator.addUpdateListener(new FlipListener(frontView, backView));
And the corresponding update listener:
public class FlipListener implements ValueAnimator.AnimatorUpdateListener {
private final View mFrontView;
private final View mBackView;
private boolean mFlipped;
public FlipListener(final View front, final View back) {
this.mFrontView = front;
this.mBackView = back;
this.mBackView.setVisibility(View.GONE);
}
#Override
public void onAnimationUpdate(final ValueAnimator animation) {
final float value = animation.getAnimatedFraction();
final float scaleValue = 0.625f + (1.5f * (value - 0.5f) * (value - 0.5f));
if(value <= 0.5f){
this.mFrontView.setRotationY(180 * value);
this.mFrontView.setScaleX(scaleValue);
this.mFrontView.setScaleY(scaleValue);
if(mFlipped){
setStateFlipped(false);
}
} else {
this.mBackView.setRotationY(-180 * (1f- value));
this.mBackView.setScaleX(scaleValue);
this.mBackView.setScaleY(scaleValue);
if(!mFlipped){
setStateFlipped(true);
}
}
}
private void setStateFlipped(boolean flipped) {
mFlipped = flipped;
this.mFrontView.setVisibility(flipped ? View.GONE : View.VISIBLE);
this.mBackView.setVisibility(flipped ? View.VISIBLE : View.GONE);
}
}
That's it!
After this setup you can flip the views by calling
mFlipAnimator.start();
and reverse the flip by calling
mFlipAnimator.reverse();
If you want to check if the view is flipped, implement and call this function:
private boolean isFlipped() {
return mFlipAnimator.getAnimatedFraction() == 1;
}
You can also check if the view is currently flipping by implementing this method:
private boolean isFlipping() {
final float currentValue = mFlipAnimator.getAnimatedFraction();
return (currentValue < 1 && currentValue > 0);
}
You can combine the above functions to implement a nice function to toggle the flip, depending on if it is flipped or not:
private void toggleFlip() {
if(isFlipped()){
mFlipAnimator.reverse();
} else {
mFlipAnimator.start();
}
}
That's it! Simple and easy. Enjoy!
I have created a simple program for creating flip of view like :
In Activity you have to create this method, for adding flip_rotation in view.
private void applyRotation(View view)
{
final Flip3dAnimation rotation = new Flip3dAnimation(view);
rotation.applyPropertiesInRotation();
view.startAnimation(rotation);
}
for this, you have to copy main class used to provide flip_rotation.
import android.graphics.Camera;
import android.graphics.Matrix;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class Flip3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private Camera mCamera;
public Flip3dAnimation(View view) {
mFromDegrees = 0;
mToDegrees = 720;
mCenterX = view.getWidth() / 2.0f;
mCenterY = view.getHeight() / 2.0f;
}
#Override
public void initialize(int width, int height, int parentWidth,
int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
public void applyPropertiesInRotation()
{
this.setDuration(2000);
this.setFillAfter(true);
this.setInterpolator(new AccelerateInterpolator());
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees
+ ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
Log.e("Degree",""+degrees) ;
Log.e("centerX",""+centerX) ;
Log.e("centerY",""+centerY) ;
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
The tutorial or the link by om252345 don't produce believable 3D flips. A simple rotation on the y-axis isn't what's done in iOS. The zoom effect is also needed to create that nice flip feel. For that, take a look at this example.
There is also a video here.
One of the better solution to flip the image with out use of the resource animation , is as follow:-
ObjectAnimator animation = ObjectAnimator.ofFloat(YOUR_IMAGEVIEW, "rotationY", 0.0f, 360f); // HERE 360 IS THE ANGLE OF ROTATE, YOU CAN USE 90, 180 IN PLACE OF IT, ACCORDING TO YOURS REQUIREMENT
animation.setDuration(500); // HERE 500 IS THE DURATION OF THE ANIMATION, YOU CAN INCREASE OR DECREASE ACCORDING TO YOURS REQUIREMENT
animation.setInterpolator(new AccelerateDecelerateInterpolator());
animation.start();
The simplest way to do it is using ViewPropertyAnimator
mImageView.animate().rotationY(360f);
Using the fluent interface you can build more complex and exciting animation.
E.g. you can enable hardware acceleration just call withLayer() method(API 16). More here
If you want to figure out how to create 3d flick animation, please follow here and here
I implemended my own solution only for a research. It includes: cancelation, accelleration, support API >= 15 and is based on Property Animation.
The entire animation includes 4 parts, 2 for each side.
Every objectAnimator has a listener that defines current animation index and represents an image in the onAnimationStart and current play time value in the onAnimationCancel.
It looks like
mQuarterAnim1.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationStart(Animator animation) {
mQuarterCurrentAnimStartIndex = QUARTER_ANIM_INDEX_1;
mImageView.setImageResource(mResIdFrontCard);
}
#Override
public void onAnimationCancel(Animator animation) {
mQuarterCurrentAnimPlayTime = ((ObjectAnimator) animation).getCurrentPlayTime();
}
});
For start set call
mAnimatorSet.play(mQuarterAnim1).before(mQuarterAnim2)
If AnimatorSet was canceled we can calculate delta and run the reverse animation relying on the current index animation and the current play time value.
long degreeDelta = mQuarterCurrentAnimPlayTime * QUARTER_ROTATE / QUARTER_ANIM_DURATION;
if (mQuarterCurrentAnimStartIndex == QUARTER_ANIM_INDEX_1) {
mQuarterAnim4.setFloatValues(degreeDelta, QUARTER_FROM_1);
mQuarterAnim4.setDuration(mQuarterCurrentAnimPlayTime);
mAnimatorSet.play(mQuarterAnim4);
}
A full code snippet you can find here
Just put the view which you're going to animate it in place of viewToFlip.
ObjectAnimator flip = ObjectAnimator.ofFloat(viewToFlip, "rotationY", 0f, 360f); // or rotationX
flip.setDuration(2000); // 2 seconds
flip.start();
Adding to A. Steenbergen's great answer. When flipping the same view (updating a TextView for example) I removed the View.Visibility change in the constructor in order to keep the transition smoother.
public FlipListener(final View front, final View back) {
this.mFrontView = front;
this.mBackView = back;
}