Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android ImageView morph: from Square to Circle (Solution updated)

I'm using CircularReveal to create animation to make square album art into circle. And the following is a short snippet.

int cx = mImageView.getMeasuredWidth() / 2;
int cy = mImageView.getMeasuredHeight() / 2;

// get the initial radius for the clipping circle
int initialRadius = mImageView.getWidth() / 2;

// create the animation (the final radius is zero)
Animator anim = ViewAnimationUtils.createCircularReveal(mImageView, cx, cy, mImageView.getWidth(), initialRadius);
anim.setDuration(500);
anim.start();

The problem is, after animation, the image does not stay in circular shape. I was looking for something like Animation.fillAfter(boolean fillAfter) call, but animator does not have that option.

Below is the current (malfunctioning) behavior.

enter image description here

Any suggestion to fix the image to circle after the animation?

like image 704
Saehun Sean Oh Avatar asked Oct 03 '16 20:10

Saehun Sean Oh


2 Answers

I solved this by completely replacing this CircularRevealView to a custom mask using GradientDrawable with my custom CardView.

my xml (tmp_activity.xml)

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/background_button"
    tools:context=".TempActivity_">

    <com.myapp.customviews.AnimatableCardView
        android:id="@+id/base_view"
        android:layout_marginLeft="@dimen/margin_medium"
        android:layout_centerVertical="true"
        android:layout_width="@dimen/album_art_small"
        android:layout_height="@dimen/album_art_small"
        app:cardElevation="0dp">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerVertical="true"
                android:layout_centerHorizontal="true"
                android:src="@drawable/charlie"
                android:id="@+id/imageView2"/>

    </com.myapp.customviews.AnimatableCardView>
</RelativeLayout>

my activity (Note that I use Android Annotations, not findViewById(..))

@EActivity(R.layout.tmp_activity)
public class TempActivity extends BaseActivity {
    @ViewById(R.id.base_view)
    ViewGroup mParent;

    @ViewById(R.id.imageView2)
    ImageView mImageView;

    GradientDrawable gradientDrawable;

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    private volatile boolean isCircle = false;
    @Override
    protected void onViewsCreated() {
        super.onViewsCreated();

        gradientDrawable = new GradientDrawable();
        gradientDrawable.setCornerRadius(30.0f);
        gradientDrawable.setShape(GradientDrawable.RECTANGLE);
        mParent.setBackground(gradientDrawable);

        mImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (isCircle) {
                    makeSquare();
                }
                else {
                    makeCircle();
                }
                isCircle = !isCircle;
            }
        });
    }

    private void makeCircle() {
        ObjectAnimator cornerAnimation =
                ObjectAnimator.ofFloat(gradientDrawable, "cornerRadius", 30f, 200.0f);

        Animator shiftAnimation = AnimatorInflater.loadAnimator(this, R.animator.slide_right_down);
        shiftAnimation.setTarget(mParent);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(500);
        animatorSet.playTogether(cornerAnimation, shiftAnimation);
        animatorSet.start();
    }

    private void makeSquare() {
        ObjectAnimator cornerAnimation =
                ObjectAnimator.ofFloat(gradientDrawable, "cornerRadius", 200.0f, 30f);

        Animator shiftAnimation = AnimatorInflater.loadAnimator(this, R.animator.slide_left_up);
        shiftAnimation.setTarget(mParent);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(500);
        animatorSet.playTogether(cornerAnimation, shiftAnimation);
        animatorSet.start();
    }
}

My custom CardView (AnimatableCardView)

public class AnimatableCardView extends CardView {
    private float xFraction = 0;
    private float yFraction = 0;

    private ViewTreeObserver.OnPreDrawListener preDrawListener = null;

    public AnimatableCardView(Context context) {
        super(context);
    }

    public AnimatableCardView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AnimatableCardView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // Note that fraction "0.0" is the starting point of the view. This includes margins.
    // If this view was placed in (200,300), moveTo="0.1" for xFraction will give you (220,300)
    public void setXFraction(float fraction) {
        this.xFraction = fraction;

        if (((ViewGroup) getParent()).getWidth() == 0) {
            if (preDrawListener == null) {
                preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
                        setXFraction(xFraction);
                        return true;
                    }
                };
                getViewTreeObserver().addOnPreDrawListener(preDrawListener);
            }
            return;
        }

        float translationX = Math.max(0, (((ViewGroup) getParent()).getWidth()) * fraction - (getWidth() * getScaleX() / 2));
        setTranslationX(translationX);
    }

    public float getXFraction() {
        return this.xFraction;
    }

    public void setYFraction(float fraction) {
        this.yFraction = fraction;

        if (((ViewGroup) getParent()).getHeight() == 0) {
            if (preDrawListener == null) {
                preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
                        setYFraction(yFraction);
                        return true;
                    }
                };
                getViewTreeObserver().addOnPreDrawListener(preDrawListener);
            }
            return;
        }

        float translationY = Math.max(0, (((ViewGroup) getParent()).getHeight()) * fraction - (getHeight() * getScaleY() / 2));
        setTranslationY(translationY);
    }

    public float getYFraction() {
        return this.yFraction;
    }
}

slide_right_down.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:ordering="together">
        <objectAnimator
            android:interpolator="@android:anim/accelerate_decelerate_interpolator"
            android:propertyName="xFraction"
            android:duration="@android:integer/config_mediumAnimTime"
            android:valueFrom="0.0"
            android:valueTo="0.5"
            android:valueType="floatType"/>

        <objectAnimator
            android:interpolator="@android:anim/accelerate_decelerate_interpolator"
            android:propertyName="yFraction"
            android:duration="@android:integer/config_mediumAnimTime"
            android:valueFrom="0.0"
            android:valueTo="0.1"
            android:valueType="floatType"/>

        <objectAnimator
            android:duration="@android:integer/config_mediumAnimTime"
            android:propertyName="scaleX"
            android:valueFrom="1.0"
            android:valueTo="1.5"/>

        <objectAnimator
            android:duration="@android:integer/config_mediumAnimTime"
            android:propertyName="scaleY"
            android:valueFrom="1.0"
            android:valueTo="1.5"/>
</set>

slide_left_up.xml

<?xml version="1.0" encoding="utf-8"?>
<set android:ordering="together"
     xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="xFraction"
        android:duration="@android:integer/config_mediumAnimTime"
        android:valueFrom="0.5"
        android:valueTo="0.0"
        android:valueType="floatType"/>
    <objectAnimator
        android:propertyName="yFraction"
        android:duration="@android:integer/config_mediumAnimTime"
        android:valueFrom="0.1"
        android:valueTo="0.0"
        android:valueType="floatType"/>

    <objectAnimator
        android:duration="@android:integer/config_mediumAnimTime"
        android:propertyName="scaleX"
        android:valueFrom="1.5"
        android:valueTo="1.0"/>

    <objectAnimator
        android:duration="@android:integer/config_mediumAnimTime"
        android:propertyName="scaleY"
        android:valueFrom="1.5"
        android:valueTo="1.0"/>
</set>

This is the result (it's a lot faster and smoother from the device)

enter image description here

like image 191
Saehun Sean Oh Avatar answered Sep 23 '22 17:09

Saehun Sean Oh


Another way that you can do this is with a MotionLayout and ImageFilterView. ImageFilterView, introduced in ConstraintLayout version 2.0, allows for the manipulation of images. It can do amazing things like crossfade two images but it can also modify the radius of a given image. My example doesn't have the small bounce that the one that the accepted answer posted but it would be easy to add with KeyFrames.

Here's what my solution looks like: VIDEO

And here are the files required to make it happen

First, your activity/fragment should look like the following (note, this can be a subView as well if you wanted to incorporate this into an existing layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".MainActivity"
    app:layoutDescription="@xml/motion_scene">

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/puthPic"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@drawable/puth"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

Note the MotionLayout has a field called app:layoutDescription that points to @xml/motion_scene... this is what the motion_scene.xml layout looks like

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="500"
    motion:motionInterpolator="easeInOut">
    <OnClick
        motion:clickAction="toggle"
        motion:targetId="@+id/puthPic" />
</Transition>
<ConstraintSet android:id="@+id/start">
    <Constraint android:id="@id/puthPic">
        <Layout
            android:layout_width="128dp"
            android:layout_height="128dp"
            android:layout_marginStart="16dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
        <CustomAttribute
            motion:attributeName="roundPercent"
            motion:customFloatValue="1" />
    </Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
    <Constraint android:id="@id/puthPic">
        <Layout
            android:layout_width="128dp"
            android:layout_height="128dp"
            android:layout_marginEnd="16dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
        <CustomAttribute
            motion:attributeName="roundPercent"
            motion:customFloatValue="0.000001" />
    </Constraint>
</ConstraintSet>
</MotionScene>

And just with that small amount of code, the motionLayout interpolates between the location and the circle to square for you!

You'll notice that I put motion:customFloatValue="0.000001" as the end scene value for the percentRound. This is because there is an existing bug that causes the image to stay rectangular if you set the percentRound to 0.0. I have filed this bug and you can see more about it here if you want.

And there you have it, another way to animate between a square and circular view with ease!

like image 32
kjanderson2 Avatar answered Sep 23 '22 17:09

kjanderson2