如何使用共享元素转换来缩放文本视图?

25
我能够使用ActivityOptions.makeSceneTransitionAnimation使TextView在两个活动之间完美过渡。但我想在它过渡时将文本缩放。我可以看到Material Design示例中联系人卡片过渡中的文本“Alphonso Engelking”正在缩放。

我尝试在目标TextView上设置缩放属性并使用changeTransform共享元素过渡,但它不会缩放且文本会在过渡过程中被截断。

如何使用共享元素过渡缩放TextView?

5个回答

30

编辑:

如下评论中Kiryl Tkach所指出,有更好的解决方案在这个Google I/O演讲中描述。


您可以创建一个自定义转换,按照以下方式动画显示TextView的文本大小:

public class TextSizeTransition extends Transition {
    private static final String PROPNAME_TEXT_SIZE = "alexjlockwood:transition:textsize";
    private static final String[] TRANSITION_PROPERTIES = { PROPNAME_TEXT_SIZE };

    private static final Property<TextView, Float> TEXT_SIZE_PROPERTY =
            new Property<TextView, Float>(Float.class, "textSize") {
                @Override
                public Float get(TextView textView) {
                    return textView.getTextSize();
                }

                @Override
                public void set(TextView textView, Float textSizePixels) {
                    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePixels);
                }
            };

    public TextSizeTransition() {
    }

    public TextSizeTransition(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public String[] getTransitionProperties() {
        return TRANSITION_PROPERTIES;
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceof TextView) {
            TextView textView = (TextView) transitionValues.view;
            transitionValues.values.put(PROPNAME_TEXT_SIZE, textView.getTextSize());
        }
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 
                                   TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }

        Float startSize = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
        Float endSize = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
        if (startSize == null || endSize == null || 
            startSize.floatValue() == endSize.floatValue()) {
            return null;
        }

        TextView view = (TextView) endValues.view;
        view.setTextSize(TypedValue.COMPLEX_UNIT_PX, startSize);
        return ObjectAnimator.ofFloat(view, TEXT_SIZE_PROPERTY, startSize, endSize);
    }
}

由于更改 TextView 的文本大小会导致其布局边界在动画过程中发生变化,因此使转换正常工作需要比仅将 ChangeBounds 转换放入同一 TransitionSet 中多付出一些努力。相反,您需要在 SharedElementCallback 中手动测量/布局视图处于其结束状态。
我在GitHub上发布了一个示例项目,用于说明这个概念(请注意,该项目定义了两个Gradle产品风格...一个使用Activity Transitions,另一个使用Fragment Transitions)。

4
@Ted,你可以在XML文件中引用上面的自定义转换,因为它覆盖了 TextSizeTransition(Context, AttributeSet) 构造函数。例如,你可以像这样引用上面的自定义转换:<transition class="com.package.name.TextSizeTransition" /> - Alex Lockwood
2
使用SharedElementCallback功能的好方法。有一个有用的补充是,如果您的共享元素实际上是ViewGroup,并且您的TextView在其中居中,那么除了在SharedElementCallback.onSharedElementEnd中之外,您还必须在SharedElementCallback.onSharedElementStart中重新布局TextView。请参见@AlexLockwood的EnterSharedElementCallback示例。 - rlay3
我的TextView在过渡时闪烁/闪烁。具体来说,最后一个单词在闪烁。奇怪。:( - Ishaan Garg
1
不要因为字体缓存的问题而认为这段代码是正确的。在这里(https://youtu.be/4L4fLrWDvAU?t=17m31s)有详细说明。这个代码将会创建很多像15.025或17.356这样大小的缓存,但它们在未来永远不会被使用到。正确的方法是将文本与 drawable 交换,在动画完成后再交换回来。这一切都在这个视频中讲述(https://youtu.be/4L4fLrWDvAU)。 - Kiryl Tkach
@KirylTkach,我更新了帖子并建议使用您的解决方案,因为我认为这是更好的选择。 - Alex Lockwood
显示剩余13条评论

4

我使用了Alex Lockwood的解决方案并简化了使用(仅适用于TextView的TextSize),希望这会有所帮助:

public class Activity2 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity2);

        EnterSharedElementTextSizeHandler handler = new EnterSharedElementTextSizeHandler(this);

        handler.addTextViewSizeResource((TextView) findViewById(R.id.timer),
                R.dimen.small_text_size, R.dimen.large_text_size);
    }
}

还有EnterSharedElementTextSizeHandler类:

public class EnterSharedElementTextSizeHandler extends SharedElementCallback {

    private final TransitionSet mTransitionSet;
    private final Activity mActivity;

    public Map<TextView, Pair<Integer, Integer>> textViewList = new HashMap<>();


    public EnterSharedElementTextSizeHandler(Activity activity) {

        mActivity = activity;

        Transition transitionWindow = activity.getWindow().getSharedElementEnterTransition();

        if (!(transitionWindow instanceof TransitionSet)) {
            mTransitionSet = new TransitionSet();
            mTransitionSet.addTransition(transitionWindow);
        } else {
            mTransitionSet = (TransitionSet) transitionWindow;
        }

        activity.setEnterSharedElementCallback(this);

    }


    public void addTextViewSizeResource(TextView tv, int sizeBegin, int sizeEnd) {

        Resources res = mActivity.getResources();
        addTextView(tv,
                res.getDimensionPixelSize(sizeBegin),
                res.getDimensionPixelSize(sizeEnd));
    }

    public void addTextView(TextView tv, int sizeBegin, int sizeEnd) {

        Transition textSize = new TextSizeTransition();
        textSize.addTarget(tv.getId());
        textSize.addTarget(tv.getText().toString());
        mTransitionSet.addTransition(textSize);

        textViewList.put(tv, new Pair<>(sizeBegin, sizeEnd));
    }

    @Override
    public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {

        for (View v : sharedElements) {

            if (!textViewList.containsKey(v)) {
                continue;
            }

            ((TextView) v).setTextSize(TypedValue.COMPLEX_UNIT_PX, textViewList.get(v).first);
        }
    }

    @Override
    public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
        for (View v : sharedElements) {

            if (!textViewList.containsKey(v)) {
                continue;
            }

            TextView textView = (TextView) v;

            // Record the TextView's old width/height.
            int oldWidth = textView.getMeasuredWidth();
            int oldHeight = textView.getMeasuredHeight();

            // Setup the TextView's end values.
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textViewList.get(v).second);

            // Re-measure the TextView (since the text size has changed).
            int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            textView.measure(widthSpec, heightSpec);

            // Record the TextView's new width/height.
            int newWidth = textView.getMeasuredWidth();
            int newHeight = textView.getMeasuredHeight();

            // Layout the TextView in the center of its container, accounting for its new width/height.
            int widthDiff = newWidth - oldWidth;
            int heightDiff = newHeight - oldHeight;
            textView.layout(textView.getLeft() - widthDiff / 2, textView.getTop() - heightDiff / 2,
                    textView.getRight() + widthDiff / 2, textView.getBottom() + heightDiff / 2);
        }
    }
}

你忘记了 mTransitionSet .setOrdering(TransitionSet.ORDERING_TOGETHER) - NickUnuchek

3
这是在Google I/O 2016的讲座中涵盖的内容。您可以将过渡效果的源代码复制到您的代码中,此处可找到源码。如果IDE抱怨addTarget(TextView.class);需要API 21,则仅删除构造函数并动态或在xml中添加目标即可。
即(请注意,这是用Kotlin写的):
val textResizeTransition = TextResize().addTarget(view.findViewById(R.id.text_view))

0

如果您查看ChangeBounds的工作方式,它会操作视图的左/右/上/下属性。

我期望的是,您需要在这两个活动中使用相同的文本大小,并在启动的活动中使用scaleXscaleY属性根据需要修改文本大小。 然后,在您的TransitionSet中使用ChangeBoundsChangeTransform的组合。


嗯,看起来TextView的边界正确地进行了动画。但是实际的文字并没有进行缩放! - rlay3
太好了,这正是我们所期望的。现在尝试在目标活动中设置比例属性,并在TransitionSet中同时使用ChangeBounds和ChangeTransform(例如TransitionSet.addTransition().addTransition())。 - klmprt
1
@klmprt 我认为更简单的解决方案是创建一个自定义的TextSizeTransition,就像这个一样。我还没有能够按照我想要的方式使其工作,但你觉得这样的东西是否在正确的轨道上?我觉得修改视图的比例属性是一种实现此效果的hack方式,而你可以直接修改文本大小... - Alex Lockwood
越是尝试自定义转场,我就越觉得有些自定义转场在与共享元素转场一起使用时会出现问题... :/ - Alex Lockwood
@AlexLockwood TextSizeTransition 对我来说看起来不错;但你可能需要与 ChangeBounds 一起使用。你遇到了什么问题? - klmprt
显示剩余2条评论

0

我的过渡动画解决方案,虽然不完全是关于这个主题,但很接近,也许需要进行修改或对某人有用。

package com.example.android.basictransition

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder.ofFloat
import android.content.Context
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

class ScaleTransition : Transition {

    companion object {

        private const val LAYOUT_WIDTH = "ScaleTransition:layout_width"
        private const val LAYOUT_HEIGHT = "ScaleTransition:layout_height"
        private const val POSITION_X = "ScaleTransition:position_x"
        private const val POSITION_Y = "ScaleTransition:position_y"
        private const val SCALE_X = "ScaleTransition:scale_x"
        private const val SCALE_Y = "ScaleTransition:scale_y"

        private val PROPERTIES = arrayOf(
                LAYOUT_WIDTH,
                LAYOUT_HEIGHT,
                POSITION_X,
                POSITION_Y,
                SCALE_X,
                SCALE_Y
        )
    }

    constructor() : super()

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun getTransitionProperties(): Array<String> {
        return PROPERTIES
    }

    override fun captureStartValues(transitionValues: TransitionValues) {
        captureValues(transitionValues)
    }

    override fun captureEndValues(transitionValues: TransitionValues) {
        resetValues(transitionValues.view)
        captureValues(transitionValues)
    }

    private fun captureValues(transitionValues: TransitionValues) = with(transitionValues.view) {
        transitionValues.values[LAYOUT_WIDTH] = width.toFloat()
        transitionValues.values[LAYOUT_HEIGHT] = height.toFloat()
        transitionValues.values[POSITION_X] = x
        transitionValues.values[POSITION_Y] = y
        transitionValues.values[SCALE_X] = scaleX
        transitionValues.values[SCALE_Y] = scaleY
    }

    private fun resetValues(view: View) = with(view) {
        translationX = 0f
        translationY = 0f
        scaleX = 1f
        scaleY = 1f
    }

    override fun createAnimator(
            sceneRoot: ViewGroup,
            start: TransitionValues?,
            end: TransitionValues?
    ): Animator? {
        if (start == null || end == null) {
            return null
        }

        val startWidth = start.values[LAYOUT_WIDTH] as Float
        val endWidth = end.values[LAYOUT_WIDTH] as Float
        val startHeight = start.values[LAYOUT_HEIGHT] as Float
        val endHeight = end.values[LAYOUT_HEIGHT] as Float

        val startX = start.values[POSITION_X] as Float
        val endX = end.values[POSITION_X] as Float
        val startY = start.values[POSITION_Y] as Float
        val endY = end.values[POSITION_Y] as Float

        val startScaleX = start.values[SCALE_X] as Float
        val startScaleY = start.values[SCALE_Y] as Float

        end.view.translationX = (startX - endX) - (endWidth - startWidth) / 2
        end.view.translationY = (startY - endY) - (endHeight - startHeight) / 2

        end.view.scaleX = (startWidth / endWidth) * startScaleX
        end.view.scaleY = (startHeight / endHeight) * startScaleY

        return ObjectAnimator.ofPropertyValuesHolder(end.view,
                ofFloat(View.TRANSLATION_X, 0f),
                ofFloat(View.TRANSLATION_Y, 0f),
                ofFloat(View.SCALE_X, 1f),
                ofFloat(View.SCALE_Y, 1f)).apply {
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator?) {
                    resetValues(start.view)
                    resetValues(end.view)
                }
            })
        }
    }
}

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