How can I scale textviews using shared element transitions?
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).
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); } }}
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))