Android 共享元素转换:将 ImageView 从圆形转换为矩形,然后再转换回来
Posted
技术标签:
【中文标题】Android 共享元素转换:将 ImageView 从圆形转换为矩形,然后再转换回来【英文标题】:Android Shared Element Transition: Transforming an ImageView from a circle to a rectangle and back again 【发布时间】:2017-02-06 13:06:08 【问题描述】:我正在尝试在两个活动之间进行共享元素转换。
第一个活动有一个圆形图像视图,第二个活动有一个矩形图像视图。我只希望圆圈从第一个活动过渡到第二个活动,当我按回时它变成一个正方形并回到圆圈。
我发现过渡不是那么整齐 - 在下面的动画中,您可以看到矩形 imageview 的大小似乎在缩小,直到它与圆的大小相匹配。方形图像视图出现片刻,然后出现圆圈。我想摆脱方形图像视图,使圆圈成为过渡的终点。
有人知道这是怎么做到的吗?
我创建了一个小型测试仓库,您可以在这里下载: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;
这是我的第二个包含矩形图像视图的活动:
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()";
我明白,在我的第一个活动中,圆圈只是通过应用毕加索转换类被“剪切”出来的,而 imageview 只是一个方形布局,因此它显示为一个圆形。也许这就是我从矩形过渡到正方形时动画看起来像这样的原因,但我真的希望从矩形过渡到圆形。
我认为有办法做到这一点。在 whatsapp 应用程序中,我可以看到效果,但我似乎无法弄清楚他们是如何做到的 - 如果您在 whatsapp 上单击朋友的个人资料图片,该应用程序会将圆形图像视图扩展为正方形。单击返回将使正方形返回圆形。
【问题讨论】:
这是您要找的吗? Link 我问是因为它不完全是 WhatsApp 使用的。 其实是的——你是怎么把它变成这样的?我正在寻找比您发布的动画更快的动画,但我可以更改持续时间。您能否将其作为解决方案以及指向您的存储库的链接发布? 嗨,西蒙,抱歉耽搁了,周末有点忙。我看到你对 Beloo 的回答没意见。我已经把这个功能变成了一个安卓开源库available here。为开发人员维护的代码越少越好:)。 您的图书馆确实令人印象深刻,做得很好。我的应用程序面向 api 16 以上,如果您可以完成 ImageTransitionCompat,我会将您的库合并到我的应用程序的下一个版本中。 【参考方案1】:我提议创建一个自定义视图,它可以动画自己从圆形到矩形并返回,然后通过添加移动动画将自定义过渡包裹在它周围。
它的样子:
代码如下(有价值的部分)。 如需完整样本,请查看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();
已编辑:CircleToRectTransition
的先前变体不是通用的,仅在特定情况下有效。检查没有那个缺点的修改示例
EDITED2:事实证明,您根本不需要自定义转换,只需从SecondActivity
中删除设置逻辑,它将以默认方式工作。使用这种方法,您可以设置转换持续时间this way。
EDITED3:为 api 提供反向移植
顺便说一句,您可以使用such technique 将这些东西反向移植到棒棒糖之前的设备上。已经创建了可以使用动画师的地方
【讨论】:
非常非常好的答案。我刚刚下载了你的 repo,它运行良好。 我还没有将它集成到我的应用程序中,一旦我这样做了就会接受你的回答。不用担心。你将在赏金时间结束前获得赏金。 您好,我需要一种以编程方式设置 circlerectview circleradius 值的方法。我试图创建一个方法来设置它,然后使视图无效,但它似乎没有工作。此外,当我使用 21 以外的较低 api 时,circlerectview 类似乎无法正常工作。在 api 16 中,该类只绘制一个正方形,而在 api 19 中,circlerectview 使用半径绘制图片而不填充整个图像视图在第二个活动中。 @Simon 您的棒棒糖前问题与设备无法在该 api 上正确处理 clipPath 有关。所以我在init
方法中提供了反向端口,请参阅编辑后的答案。第二个,我的自定义视图不是那么通用,它只绘制一个带有corners = circleRadius 参数的矩形。因此,当宽度和高度相等且圆半径为一半时 - 将绘制圆。如果您想以编程方式更改圆半径,还应该更改视图的大小。
它不起作用,你能更新你的代码吗?【参考方案2】:
您需要添加一些代码:基本上您必须实现自定义转换。但是大部分代码都可以重用。我将把代码推送到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? NoPauseAnimator --> github.com/BelooS/CircleToRect-ActivityTransition/blob/master/…以上是关于Android 共享元素转换:将 ImageView 从圆形转换为矩形,然后再转换回来的主要内容,如果未能解决你的问题,请参考以下文章