web-dev-qa-db-fra.com

Android Transition d'élément partagé: Transformer une ImageView d'un cercle en un rectangle et vice-versa

J'essaie de faire une transition d'élément partagé entre deux activités.

La première activité a une vue d'image circulaire et la deuxième activité a une vue d'image rectangulaire. Je veux juste que le cercle passe de la première activité à la deuxième activité où il devient un carré et retourne au cercle lorsque j'appuie.

Je trouve que la transition n'est pas si nette - dans l'animation ci-dessous, vous pouvez voir que la vue d'image rectangulaire semble réduire en taille jusqu'à ce qu'elle corresponde à la taille du cercle. La vue d'image carrée apparaît pendant une fraction de seconde, puis le cercle apparaît. Je veux me débarrasser de la vue d'image carrée pour que le cercle devienne le point final de la transition.

Quelqu'un sait comment c'est fait? enter image description here

J'ai créé un petit repo de test que vous pouvez télécharger ici: https://github.com/Winghin2517/TransitionTest

Le code de ma première activité - l'imageview se trouve dans le MainFragment de ma première activité:

public class MainFragment extends Android.support.v4.app.Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_view, container,false);
        final ImageView dot = (ImageView) view.findViewById(R.id.image_circle);
        Picasso.with(getContext()).load(R.drawable.snow).transform(new PureCircleTransformation()).into(dot);
        dot.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent i = new Intent(getContext(), SecondActivity.class);
                View sharedView = dot;
                String transitionName = getString(R.string.blue_name);
                ActivityOptionsCompat transitionActivityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), sharedView, transitionName);
                startActivity(i, transitionActivityOptions.toBundle());
            }
        });
        return view;
    }
}

Ceci est ma deuxième activité qui contient la vue d'image rectangulaire:

public class SecondActivity extends AppCompatActivity {

    ImageView backdrop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        backdrop = (ImageView) findViewById(R.id.picture);
        backdrop.setBackground(ContextCompat.getDrawable(this, R.drawable.snow));
    }

    @Override
    public void onBackPressed() {
        supportFinishAfterTransition();
        super.onBackPressed();

    }
}

Voici la classe PureCircleTransformation que je passe dans Picasso pour générer le cercle:

package test.com.transitiontest;

import Android.graphics.Bitmap;
import Android.graphics.BitmapShader;
import Android.graphics.Canvas;
import Android.graphics.Paint;

import com.squareup.picasso.Transformation;

public class PureCircleTransformation implements Transformation {

    private static final int STROKE_WIDTH = 6;

    @Override
    public Bitmap transform(Bitmap source) {
        int size = Math.min(source.getWidth(), source.getHeight());

        int x = (source.getWidth() - size) / 2;
        int y = (source.getHeight() - size) / 2;

        Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
        if (squaredBitmap != source) {
            source.recycle();
        }

        Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());

        Canvas canvas = new Canvas(bitmap);

        Paint avatarPaint = new Paint();
        BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
        avatarPaint.setShader(shader);

        float r = size / 2f;
        canvas.drawCircle(r, r, r, avatarPaint);

        squaredBitmap.recycle();
        return bitmap;
    }

    @Override
    public String key() {
        return "circleTransformation()";
    }
}

Je comprends que dans ma première activité, le cercle est simplement "découpé" en appliquant la classe de transformation Picasso et que la vue d'image est juste une disposition carrée découpée de sorte qu'elle apparaisse comme un cercle. C'est peut-être la raison pour laquelle l'animation ressemble à ceci alors que je passe d'un rectangulaire à un carré, mais je veux vraiment que la transition passe du rectangulaire à un cercle.

Je pense qu'il existe un moyen de le faire. Dans l'application WhatsApp, je peux voir l'effet, mais je n'arrive pas à comprendre comment ils ont réussi à le faire - Si vous cliquez sur la photo de profil de vos amis sur WhatsApp, l'application étend la vue d'image du cercle à un carré. Cliquer en arrière ramènera le carré au cercle.

enter image description here

31
Simon

Je propose de créer une vue personnalisée, qui peut s'animer du cercle au recto et au verso, puis envelopper la transition personnalisée autour d'elle en ajoutant une animation animée.

À quoi cela ressemble:
Circle to rect transition gif

Le code est ci-dessous (partie précieuse).
Pour un échantillon complet, vérifiez mon 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();
}

[~ # ~] édité [~ # ~] : la variante précédente de CircleToRectTransition n'était pas générale et ne fonctionnait que dans des cas spécifiques. Vérifiez l'exemple modifié sans cet inconvénient

EDITED2 : Il s'avère que vous n'avez pas du tout besoin d'une transition personnalisée, supprimez simplement la logique de configuration de SecondActivity et cela fonctionnera par voie par défaut. Avec cette approche, vous pouvez définir la durée de transition de cette façon .

EDITED3 : Backport fourni pour api <18

Soit dit en passant, vous pouvez rétroporter ce contenu sur des appareils pré-Lollipop en utilisant ne telle technique . Où vous pouvez utiliser des animateurs ont déjà été créés

27
Beloo

Il y a du code que vous devez ajouter: fondamentalement, vous devez implémenter une transition personnalisée. Mais la plupart du code peut être réutilisé. Je vais pousser le code sur github pour votre référence, mais les étapes nécessaires sont:

SecondAcvitiy Créez votre transition personnalisée:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Lollipop) {

    Transition transition = new CircularReveal();
    transition.setInterpolator(new LinearInterpolator());

    getWindow().setSharedElementEnterTransition(transition);
}

CircularReveal capture les limites de la vue (valeurs de début et de fin) et fournit deux animations, la première lorsque vous devez animer la vue circulaire de l'image à la grande, la seconde pour le cas inverse.

@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();
    }
}
5
Mimmo Grottoli