How can I scale textviews using shared element transitions?
Asked Answered
C

5

25

I am able to get TextViews to transition perfectly between two activities using ActivityOptions.makeSceneTransitionAnimation. However I want to make the text scale up as it transitions. I can see the material design example scaling up the text "Alphonso Engelking" in the contact card transition.

I've tried setting the scale attributes on the destination TextView and using the changeTransform shared element transitions, but it doesn't scale and the text ends up being truncated as it transitions.

How can I scale TextViews using shared element transition?

Colewort answered 28/10, 2014 at 2:9 Comment(0)
T
30

Edit:

As pointed out by Kiryl Tkach in the comments below, there is a better solution described in this Google I/O talk.


You can create a custom transition that animates a TextView's text size as follows:

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

Since changing the TextView's text size will cause its layout bounds to change during the course of the animation, getting the transition to work properly will take a little more effort than simply throwing a ChangeBounds transition into the same TransitionSet. What you will need to do instead is manually measure/layout the view in its end state in a SharedElementCallback.

I've published an example project on GitHub that illustrates the concept (note that the project defines two Gradle product flavors... one uses Activity Transitions and the other uses Fragment Transitions).

Tonnage answered 8/11, 2014 at 4:15 Comment(18)
Hey, so could you elaborate on what "properly" means? I asked a SO question (see below), regarding an animation that isnt looking good. Is it not advicable to define animations in XML, but do it the hard way in code? #27124061Intelligentsia
@Intelligentsia It is possible to reference the custom transition above in your XML files because it overrides the TextSizeTransition(Context, AttributeSet) constructor. For example, you could reference the custom transition above like this: <transition class="com.package.name.TextSizeTransition" />Tonnage
@Intelligentsia I answered your stack overflow question with a bit more detail on why your code doesn't work.Tonnage
Thanks for the input, I will try to figure out what you mean, as I am currently not entirely clear on that =)Intelligentsia
@Intelligentsia I linked to a sample project on GitHub in my answer... you can always start with that. :)Tonnage
I did. I only see a bunch of files that seems to be unrelated to each other?Intelligentsia
@Intelligentsia Did you run the application? Not sure what you mean by "they seem unrelated to each other", but the example text size transition in the project works for me when I run it.Tonnage
If I go the URL, I see activity_main.xml, TransitionActivity.java, and other files listed, no way to download complete project, I see no transition xml files, there is some Python script...?Intelligentsia
@Intelligentsia You can download the project using git clone https://github.com/alexjlockwood/custom-lollipop-transitions. Then import it into Android Studio.Tonnage
Nice solution using the SharedElementCallback feature. Something useful to add is that if your shared element is actually a ViewGroup and your TextView is centered inside it, you have to re-layout the TextView in SharedElementCallback.onSharedElementStart in addition to SharedElementCallback.onSharedElementEnd. See @AlexLockwood's EnterSharedElementCallback example.Colewort
@AlexLockwood Thanks for this! The only issue I have is that if the target TextView is not centered, the text jumps at the end. (you can reproduce by setting the layout_gravity of end_scene's TextView to left|centerInnocence
-> edit: I am messing around with text.layout in OnSharedElementEnd. That seems to be workingInnocence
My TextView is flickering / blinking while transitioning. Specifically the last word is flickering. weird. :(Fingerstall
Figured it out. You need to exclude the textviews from changeBounds Transition. Then there is no flickering. yay.Fingerstall
Could you please take a look at my post. Your blog and answers in SO help me a lot.Roll
Don't think that this code is correct because of the thrashing font cache. It is told here. This code will create a lot of cache with font size like 15.025 or 17.356, which will be never used in the future. The right way to do this is to swap text with drawable and after animation swap it back. Everything is told in this video.Venn
@KirylTkach I updated the post to suggest your solution, as I agree it is the better option.Tonnage
If you're using a container view to transition your animations in, you can 1) add TextSizeTransition class to your project 2) reference it in your transition xml file (example: github.com/googlesamples/android-unsplash/blob/master/app/src/…) 3) scene = getSceneForLayout(...) and use with scene.transition(activity, R.transition.your_transition_file_that_mimics_the_linked_example_in_2), object : TransitionListenerAdapter() {}) 3) you might have to re-import some classes in TextSizeTransition if you already migrated to androidxCrankle
P
4

I used solution from Alex Lockwood and simplified the use (it's only for TextSize of a TextView), I hope this will help:

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

and the class 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);
        }
    }
}
Phocine answered 25/11, 2015 at 10:26 Comment(1)
you forgot about mTransitionSet .setOrdering(TransitionSet.ORDERING_TOGETHER)Wort
B
3

This was covered in one of the Google I/O 2016 talks. The source for the transition which you can copy into your code is found here. If your IDE complains the addTarget(TextView.class); requires API 21, just remove the constructor and add the target either dynamically or in your xml.

i.e. (note this is in Kotlin)

val textResizeTransition = TextResize().addTarget(view.findViewById(R.id.text_view))
Bateau answered 12/7, 2017 at 17:42 Comment(0)
D
0

If you look at how ChangeBounds works, it operates on the left/right/top/bottom properties of the view.

What I expect is that you'll need to use the same text size across the two activities, and use the scaleX and scaleY properties in your launched activity to modify the text size as needed. Then, use a combination of ChangeBounds and ChangeTransform in your TransitionSet.

Dermatologist answered 31/10, 2014 at 1:16 Comment(7)
Yep looks like the textview bounds is animating correctly. However the actual text isn't scaling!Colewort
Great, that's what we'd expect. Now try to set the scale property in the destination activity and use both ChangeBounds and ChangeTransform together in a TransitionSet (e.g. TransitionSet.addTransition().addTransition())Dermatologist
@Dermatologist I thought an even simpler solution would be to create a custom TextSizeTransition like this... I still haven't been able to get it to work the way I want it, but do you think something like this is on the right track? I feel like modifying the view's scale properties is a hacky way to achieve this effect when you can just modify the text size...Tonnage
The more I experiment with custom transitions, the more I think that some custom transitions simply refuse to work when used with shared element transitions... :/Tonnage
@AlexLockwood TextSizeTransition looks good to me; you may need to use it in conjunction with ChangeBounds though. What wasn't working for you?Dermatologist
@Dermatologist I'll write up a short sample project demonstrating the problem tonight. I really want to figure out how to write custom transitions myself, but the only way I can ever get them to work is by modifying the shared views somehow in SharedElementCallback#onSharedElementStart() and even then it just feels so hacky.Tonnage
@AlexLockwood Depending on what you're modifying, that's probably okay. The onSharedElementStart and onSharedElementEnd callbacks are there for you to 'set the scene' as needed -- ultimately the framework can't do all the work for you.Dermatologist
M
0

My solutions for the TransitionAnimation, not quite on the topic but close, perhaps with modifications or just someone come in handy.

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)
                }
            })
        }
    }
}
Mythological answered 13/7, 2019 at 19:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.