Android Shared Element Transition: Transforming an ImageView from a circle to a rectangle and back again

前端 未结 2 1218
小蘑菇
小蘑菇 2021-01-30 07:33

I\'m trying to do a shared element transition between two activities.

The first activity has a circle imageview and the second activity has a rectangular imageview. I ju

相关标签:
2条回答
  • 2021-01-30 07:49

    There's some code you need to add: basically you have to implement a custom transition. But most of the code can be reused. I'm going to push the code on github for your reference, but the steps needed are:

    SecondAcvitiy Create your custom transition:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
        Transition transition = new CircularReveal();
        transition.setInterpolator(new LinearInterpolator());
    
        getWindow().setSharedElementEnterTransition(transition);
    }
    

    CircularReveal capture view bounds (start and end values) and provide two animations, the first one when you need to animate the circular image view to the big one, the second for the reverse case.

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public class CircularReveal extends Transition {
    
        private static final String BOUNDS = "viewBounds";
    
        private static final String[] PROPS = {BOUNDS};
    
        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        private void captureValues(TransitionValues values) {
            View view = values.view;
            Rect bounds = new Rect();
            bounds.left = view.getLeft();
            bounds.right = view.getRight();
            bounds.top = view.getTop();
            bounds.bottom = view.getBottom();
    
            values.values.put(BOUNDS, bounds);
        }
    
        @Override
        public String[] getTransitionProperties() {
            return PROPS;
        }
    
        @Override
        public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
            if (startValues == null || endValues == null) {
                return null;
            }
    
            Rect startRect = (Rect) startValues.values.get(BOUNDS);
            final Rect endRect = (Rect) endValues.values.get(BOUNDS);
    
            final View view = endValues.view;
    
            Animator circularTransition;
            if (isReveal(startRect, endRect)) {
                circularTransition = createReveal(view, startRect, endRect);
                return new NoPauseAnimator(circularTransition);
            } else {
                layout(startRect, view);
    
                circularTransition = createConceal(view, startRect, endRect);
                circularTransition.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        view.setOutlineProvider(new ViewOutlineProvider() {
                            @Override
                            public void getOutline(View view, Outline outline) {
                                Rect bounds = endRect;
                                bounds.left -= view.getLeft();
                                bounds.top -= view.getTop();
                                bounds.right -= view.getLeft();
                                bounds.bottom -= view.getTop();
                                outline.setOval(bounds);
                                view.setClipToOutline(true);
                            }
                        });
                    }
                });
                return new NoPauseAnimator(circularTransition);
            }
        }
    
        private void layout(Rect startRect, View view) {
            view.layout(startRect.left, startRect.top, startRect.right, startRect.bottom);
        }
    
        private Animator createReveal(View view, Rect from, Rect to) {
    
            int centerX = from.centerX();
            int centerY = from.centerY();
            float finalRadius = (float) Math.hypot(to.width(), to.height());
    
            return ViewAnimationUtils.createCircularReveal(view, centerX, centerY,
                from.width()/2, finalRadius);
        }
    
        private Animator createConceal(View view, Rect from, Rect to) {
    
            int centerX = to.centerX();
            int centerY = to.centerY();
            float initialRadius = (float) Math.hypot(from.width(), from.height());
    
            return ViewAnimationUtils.createCircularReveal(view, centerX, centerY,
                initialRadius, to.width()/2);
        }
    
        private boolean isReveal(Rect startRect, Rect endRect) {
            return startRect.width() < endRect.width();
        }
    }
    
    0 讨论(0)
  • 2021-01-30 07:55

    I offer to create a custom view, which can animate itself from circle to rect and back and then wrap custom transition around it with adding moving animation.

    How it is looks like:

    Code is below (valuable part).
    For full sample, check my github.

    CircleRectView.java:

    public class CircleRectView extends ImageView {
    
    private int circleRadius;
    private float cornerRadius;
    
    private RectF bitmapRect;
    private Path clipPath;
    
    private void init(TypedArray a) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }
    
        if (a.hasValue(R.styleable.CircleRectView_circleRadius)) {
            circleRadius = a.getDimensionPixelSize(R.styleable.CircleRectView_circleRadius, 0);
            cornerRadius = circleRadius;
        }
        clipPath = new Path();
        a.recycle();
    }
    
    public Animator animator(int startHeight, int startWidth, int endHeight, int endWidth) {
        AnimatorSet animatorSet = new AnimatorSet();
    
        ValueAnimator heightAnimator = ValueAnimator.ofInt(startHeight, endHeight);
        ValueAnimator widthAnimator = ValueAnimator.ofInt(startWidth, endWidth);
    
        heightAnimator.addUpdateListener(valueAnimator -> {
            int val = (Integer) valueAnimator.getAnimatedValue();
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            layoutParams.height = val;
    
            setLayoutParams(layoutParams);
            requestLayoutSupport();
        });
    
        widthAnimator.addUpdateListener(valueAnimator -> {
            int val = (Integer) valueAnimator.getAnimatedValue();
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            layoutParams.width = val;
    
            setLayoutParams(layoutParams);
            requestLayoutSupport();
        });
    
        ValueAnimator radiusAnimator;
        if (startWidth < endWidth) {
            radiusAnimator = ValueAnimator.ofFloat(circleRadius, 0);
        } else {
            radiusAnimator = ValueAnimator.ofFloat(cornerRadius, circleRadius);
        }
    
        radiusAnimator.setInterpolator(new AccelerateInterpolator());
        radiusAnimator.addUpdateListener(animator -> cornerRadius = (float) (Float) animator.getAnimatedValue());
    
        animatorSet.playTogether(heightAnimator, widthAnimator, radiusAnimator);
    
        return animatorSet;
    }
    
    /**
     * this needed because of that somehow {@link #onSizeChanged} NOT CALLED when requestLayout while activity transition end is running
     */
    private void requestLayoutSupport() {
        View parent = (View) getParent();
        int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.EXACTLY);
        parent.measure(widthSpec, heightSpec);
        parent.layout(parent.getLeft(), parent.getTop(), parent.getRight(), parent.getBottom());
    }
    
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //This event-method provides the real dimensions of this custom view.
    
        Log.d("size changed", "w = " + w + " h = " + h);
    
        bitmapRect = new RectF(0, 0, w, h);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
    
        Drawable drawable = getDrawable();
    
        if (drawable == null) {
            return;
        }
    
        if (getWidth() == 0 || getHeight() == 0) {
            return;
        }
    
        clipPath.reset();
        clipPath.addRoundRect(bitmapRect, cornerRadius, cornerRadius, Path.Direction.CW);
        canvas.clipPath(clipPath);
        super.onDraw(canvas);
    }
    

    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    public class CircleToRectTransition extends Transition {
    private static final String TAG = CircleToRectTransition.class.getSimpleName();
    private static final String BOUNDS = "viewBounds";
    private static final String[] PROPS = {BOUNDS};
    
    @Override
    public String[] getTransitionProperties() {
        return PROPS;
    }
    
    private void captureValues(TransitionValues transitionValues) {
        View view = transitionValues.view;
        Rect bounds = new Rect();
        bounds.left = view.getLeft();
        bounds.right = view.getRight();
        bounds.top = view.getTop();
        bounds.bottom = view.getBottom();
        transitionValues.values.put(BOUNDS, bounds);
    }
    
    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }
    
    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }
    
    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }
    
        if (!(startValues.view instanceof CircleRectView)) {
            Log.w(CircleToRectTransition.class.getSimpleName(), "transition view should be CircleRectView");
            return null;
        }
    
        CircleRectView view = (CircleRectView) (startValues.view);
    
        Rect startRect = (Rect) startValues.values.get(BOUNDS);
        final Rect endRect = (Rect) endValues.values.get(BOUNDS);
    
        Animator animator;
    
        //scale animator
        animator = view.animator(startRect.height(), startRect.width(), endRect.height(), endRect.width());
    
        //movement animators below
        //if some translation not performed fully, use it instead of start coordinate
        float startX = startRect.left + view.getTranslationX();
        float startY = startRect.top + view.getTranslationY();
    
        //somehow end rect returns needed value minus translation in case not finished transition available
        float moveXTo = endRect.left + Math.round(view.getTranslationX());
        float moveYTo = endRect.top + Math.round(view.getTranslationY());
    
        Animator moveXAnimator = ObjectAnimator.ofFloat(view, "x", startX, moveXTo);
        Animator moveYAnimator = ObjectAnimator.ofFloat(view, "y", startY, moveYTo);
    
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(animator, moveXAnimator, moveYAnimator);
    
        //prevent blinking when interrupt animation
        return new NoPauseAnimator(animatorSet);
    }
    

    MainActivity.java :

     view.setOnClickListener(v -> {
            Intent intent = new Intent(this, SecondActivity.class);
            ActivityOptionsCompat transitionActivityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, view, getString(R.string.circle));
    
            ActivityCompat.startActivity(MainActivity.this, intent , transitionActivityOptions.toBundle());
        });
    

    SecondActivity.java :

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setSharedElementEnterTransition(new CircleToRectTransition().setDuration(1500));
            getWindow().setSharedElementExitTransition(new CircleToRectTransition().setDuration(1500));
        }
    
        super.onCreate(savedInstanceState);
        ...
     }
    @Override
    public void onBackPressed() {
        supportFinishAfterTransition();
    }
    

    EDITED: Previous variant of CircleToRectTransition wasn't general and worked only in specific case. Check modified example without that disadvantage

    EDITED2: It turns out that you don't need custom transition at all, just remove setup logic from SecondActivity and it will be working via default way. With this approach you could set transition duration this way.

    EDITED3: Provided backport for api < 18

    By the way, you can backport this stuff onto pre-lollipop devices with using such technique. Where you can use animators have been already created

    0 讨论(0)
提交回复
热议问题