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

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

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

我发现转换不是那么整洁 – 在下面的animation中,您可以看到矩形图像视图似乎减小了大小,直到它与圆的大小相匹配。 方形图像视图会瞬间出现,然后出现圆圈。 我想摆脱方形图像视图,以便圆圈成为过渡的终点。

谁知道这是怎么做到的? 在此处输入图像描述

我已经创建了一个小测试回购,你可以在这里下载: https : //github.com/Winghin2517/TransitionTest

我的第一个活动的代码 – imageview位于我的第一个活动的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; } } 

这是我的第二个包含矩形图像视图的活动:

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

我确实理解,在我的第一个活动中,通过应用毕加索变换类来“切割”圆圈,并且图像视图只是切出的方形布局,使其显示为圆形。 也许这就是animation看起来像这样的原因,因为我正在从矩形过渡到正方形,但我真的希望过渡从矩形变为圆形。

我认为有办法做到这一点。 在whatsapp应用程序中,我可以看到效果,但我似乎无法弄清楚他们是如何设法做到的 – 如果您点击whatsapp上朋友的个人资料图片,该应用程序会将圆形图像视图扩展为正方形。 单击返回将使方块返回圆圈。

在此处输入图像描述

Solutions Collecting From Web of "Android共享元素转换:将ImageView从圆形转换为矩形,然后再转换回来"

我提议创建一个自定义视图,它可以自动从圆形到矩形和背面animation,然后通过添加移动animation包裹自定义过渡。

看起来像是这样的:
圆到矩形过渡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先前变体不是通用的,仅在特定情况下有效。 检查修改后的示例没有那个缺点

EDITED2 :事实certificate你根本不需要自定义转换,只需从SecondActivity删除设置逻辑,它将以默认方式工作。 使用这种方法,您可以通过这种方式设置转换持

EDITED3 :为api <18提供了后端口

顺便说一句,你可以使用这种技术将这些东西后移到前棒棒糖设备上。 您已经创建了使用animation师的地方

您需要添加一些代码:基本上您必须实现自定义转换。 但是大多数代码都可以重用。 我将在github上推送代码供您参考,但所需的步骤是:

SecondAcvitiy创建自定义转换:

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

CircularReveal捕获视图边界(开始和结束值)并提供两个animation,第一个是需要将圆形图像视图设置为大视图时的animation,第二个是相反情况。

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