Android共享元素转换:将ImageView从圆形转换为矩形再转回来

32

我正在尝试在两个活动之间进行共享元素转换。

第一个活动具有圆形图像视图,第二个活动具有矩形图像视图。我只想让圆从第一个活动过渡到第二个活动,在那里它变成一个正方形,然后在按返回键时回到圆形。

我发现转换效果不太好 - 在下面的动画中,您可以看到矩形图像视图似乎缩小到与圆的大小相匹配。正方形图像视图会出现一小段时间,然后出现圆形图像视图。我想摆脱正方形视图,以使圆形成为转换的终点。

有人知道如何做到这一点吗? enter image description here

我创建了一个小测试存储库,您可以在此处下载:https://github.com/Winghin2517/TransitionTest

第一个活动的代码 - 图像视图位于我的第一个活动的MainFragment中:

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;
    }
}

这是我的第二个活动,其中包含矩形imageview:

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();

    }
}

这是我传递给Picasso以生成圆形图像的PureCircleTransformation类:

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()";
    }
}

我知道在我的第一个活动中,圆形只是通过应用 Picasso 变换类进行“裁剪”,而 ImageView 只是一个被裁剪成圆形的正方形布局。也许这就是为什么当我从矩形过渡到正方形时,动画看起来像这样,但我真的希望过渡从矩形到圆形。

我认为有一种方法可以实现这个效果,在 whatsapp 应用中,我可以看到这个效果,但我就是无法弄清他们是如何做到的——如果你在 whatsapp 上点击你朋友的头像,应用程序会将圆形 ImageView 扩展为正方形。返回后,正方形又变回圆形。

enter image description here


1
这是你要找的吗?[链接](http://gph.is/2dbeYB9)我问这个问题是因为它并不完全是WhatsApp正在使用的。 - Vikram
实际上是的 - 你是怎么把它变成那样的?我正在寻找一个比你发布的更快的动画,但我可以改变时长。你能否将其作为解决方案发布,并附上您的存储库链接? - Simon
嗨,西蒙,抱歉拖延了一下,周末有些忙。我看到你对Beloo的回答感到满意。我将这个功能制作成了一个Android开源库在此处可用。对于开发人员来说,维护更少的代码始终是更好的:)。 - Vikram
你的库真的很棒,干得好。我的应用程序针对API 16及以上版本,如果你能完成ImageTransitionCompat,我会在我的下一个应用程序构建中将你的库合并进去。 - Simon
2个回答

29

我提供创建一个自定义视图的服务,该视图可以从圆形动画变成矩形然后再添加移动动画。同时,我还会为其添加自定义转换效果。

它看起来是这样的:
Circle to rect transition gif

以下是相关代码(重要部分)。
完整示例请查看 我的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();
}

已编辑:以前的CircleToRectTransition变体并不普遍,只适用于特定情况。检查修改后的示例,没有这个劣势。

已编辑2:原来你根本不需要自定义过渡效果,只需从SecondActivity中删除设置逻辑,它就可以通过默认方式工作了。采用这种方法,您可以设置过渡时间这种方式

已编辑3:为api<18提供后移支持

顺便说一下,您可以使用这种技术将此内容退回到Lollipop之前的设备上。在那里,您可以使用已经创建好的动画器。


非常好的回答。我刚刚下载了你的代码库,它运行得非常好。 - Simon
我还没有将它集成到我的应用程序中,一旦完成集成,我会接受您的答案。不用担心,在赏金时间结束之前,您将获得赏金。 - Simon
你好,我需要一种编程方式来动态设置circlerectview的circleradius值。我尝试创建一个方法来设置它,然后使视图无效,但似乎没有起作用。此外,当我使用低于21的api时,circlerectview类似乎无法正常工作。在api 16中,该类只绘制一个正方形,在api 19中,circlerectview绘制了具有半径的图片,但未填充第二个活动中整个imageview。 - Simon
@Simon,你遇到的问题是由于那些API无法正确处理clipPath而引起的。因此,我在init方法中提供了向后兼容性,请查看编辑后的答案。另外,我的自定义视图并不是那么通用,它只画一个角为圆形半径参数的矩形。因此,当宽度和高度相等且圆的半径为其一半时,将会画出圆形。如果你想要在程序中改变圆的半径,还需更改视图大小。 - Beloo
它不起作用,你能更新一下你的代码吗? - FarshidABZ

5

你需要添加一些代码:基本上,你需要实现自定义转换。但是大部分的代码可以重用。我会把代码推到github上供你参考,但是所需步骤如下:

SecondAcvitiy 创建你的自定义转换:

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

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

    getWindow().setSharedElementEnterTransition(transition);
}

CircularReveal 捕捉视图边界(起始和结束值),并提供两个动画,一个用于将圆形图片视图动画到大视图,另一个用于相反情况。

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

嗨,NoPauseAnimator是什么? - makovkastar
NoPauseAnimator --> https://github.com/BelooS/CircleToRect-ActivityTransition/blob/master/app/src/main/java/com/example/beloo/circlerectbitmapanimator/support/NoPauseAnimator.java - Emmanuelguther

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接