Как я могу масштабировать текстовые представления, используя общие переходы элементов?

Я могу заставить TextViews идеально переходить между двумя действиями, используя ActivityOptions.makeSceneTransitionAnimation. Однако я хочу, чтобы текст увеличивался при переходе. Я вижу пример материального дизайна, увеличивающий текст "Alphonso Engelking" в контакте карточный переход.

Я попытался установить атрибуты масштабирования в целевом TextView и использовать переходы общего элемента changeTransform, но он не масштабируется, и текст в конечном итоге усекается при переходе.

Как я могу масштабировать TextViews, используя переход общего элемента?




Ответы (5)


Редактировать:

Как указал Кирилл Ткач в комментариях ниже, есть лучшее решение, описанное в этот доклад 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... один использует переходы активности, а другой использует переходы фрагментов).

person Alex Lockwood    schedule 08.11.2014
comment
Эй, а не могли бы вы уточнить, что собственно означает? Я задал ТАК вопрос (см. ниже) относительно анимации, которая выглядит не очень хорошо. Не рекомендуется ли определять анимацию в XML, но делать это сложным путем в коде? stackoverflow.com/questions/27123561/ - person Ted; 01.12.2014
comment
@Ted Можно ссылаться на пользовательский переход выше в ваших XML-файлах, потому что он переопределяет конструктор TextSizeTransition(Context, AttributeSet). Например, вы можете сослаться на приведенный выше пользовательский переход следующим образом: <transition class="com.package.name.TextSizeTransition" /> - person Alex Lockwood; 03.12.2014
comment
@Ted Я ответил на ваш вопрос о переполнении стека более подробно, почему ваш код не работает. - person Alex Lockwood; 03.12.2014
comment
Спасибо за ответ, я постараюсь понять, что вы имеете в виду, так как я в настоящее время не совсем понимаю это =) - person Ted; 04.12.2014
comment
@Ted В своем ответе я связался с примером проекта на GitHub ... вы всегда можете начать с него. :) - person Alex Lockwood; 04.12.2014
comment
Я сделал. Я вижу только кучу файлов, которые кажутся не связанными друг с другом? - person Ted; 04.12.2014
comment
@Ted Ты запустил приложение? Не уверен, что вы подразумеваете под тем, что они кажутся не связанными друг с другом, но пример перехода размера текста в проекте работает для меня, когда я его запускаю. - person Alex Lockwood; 04.12.2014
comment
Если я перехожу по URL-адресу, я вижу список activity_main.xml, TransitionActivity.java и другие файлы, нет возможности загрузить весь проект, я не вижу XML-файлов перехода, есть какой-то скрипт Python...? - person Ted; 04.12.2014
comment
@Ted Вы можете загрузить проект, используя git clone https://github.com/alexjlockwood/custom-lollipop-transitions. Затем импортируйте его в Android Studio. - person Alex Lockwood; 04.12.2014
comment
Хорошее решение с использованием функции SharedElementCallback. Полезно добавить, что если ваш общий элемент на самом деле является ViewGroup, а ваш TextView центрирован внутри него, вам нужно переразметить TextView в SharedElementCallback.onSharedElementStart в дополнение к SharedElementCallback.onSharedElementEnd. См. пример EnterSharedElementCallback @AlexLockwood. - person rlay3; 14.04.2015
comment
@AlexLockwood Спасибо за это! Единственная проблема, с которой я столкнулся, заключается в том, что если цель TextView не центрирована, текст прыгает в конце. (вы можете воспроизвести, установив layout_gravity из end_scene TextView на left|center - person Boy; 25.05.2016
comment
-› редактировать: я возился с text.layout в OnSharedElementEnd. Кажется, это работает - person Boy; 25.05.2016
comment
Мой TextView мерцает/мигает при переходе. Конкретно последнее слово мерцает. странный. :( - person Ishaan Garg; 15.09.2016
comment
Догадаться. Вам нужно исключить текстовые представления из changeBounds Transition. Тогда мерцания нет. ура. - person Ishaan Garg; 15.09.2016
comment
Не могли бы вы взглянуть на моя запись. Ваш блог и ответы в SO мне очень помогают. - person Lym Zoy; 04.02.2017
comment
Не думайте, что этот код правильный, из-за перебора кеша шрифтов. Об этом сообщается здесь. Этот код создаст много кеша с размером шрифта 15.025 или 17.356, который никогда не будет использоваться в будущем. Правильный способ сделать это — заменить текст на drawable, а после анимации поменять его местами. Обо всем этом рассказывается в этом видео. - person Kiryl Tkach; 12.11.2017
comment
@KirylTkach Я обновил сообщение, чтобы предложить ваше решение, так как я согласен, что это лучший вариант. - person Alex Lockwood; 13.11.2017
comment
Если вы используете представление контейнера для переноса анимации, вы можете 1) добавить класс TextSizeTransition в свой проект 2) указать его в XML-файле перехода (пример: github.com/googlesamples/android-unsplash/blob/master/app/src/) 3) scene = getSceneForLayout(...) и использовать с scene.transition(activity, R.transition.your_transition_file_that_mimics_the_linked_example_in_2), object : TransitionListenerAdapter() {}) 3) возможно, вам придется повторно импортировать некоторые классы в TextSizeTransition, если вы уже перешли на androidx - person velasco622; 31.05.2019

Я использовал решение Алекса Локвуда и упростил использование (это только для TextSize TextView), надеюсь, это поможет:

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);
        }
    }
}
person Thibaud Michel    schedule 25.11.2015
comment
ты забыл про mTransitionSet .setOrdering(TransitionSet.ORDERING_TOGETHER) - person NickUnuchek; 19.07.2018

Об этом говорилось в одном из докладов Google I/O 2016. Исходный код перехода, который вы можете скопировать в свой код, находится здесь. Если ваша IDE жалуется, что addTarget(TextView.class); требует API 21, просто удалите конструктор и добавьте цель либо динамически, либо в свой xml.

т.е. (обратите внимание, что это в Котлине)

val textResizeTransition = TextResize().addTarget(view.findViewById(R.id.text_view))
person odiggity    schedule 12.07.2017

Если вы посмотрите, как работает ChangeBounds, то увидите, что он работает с левым/правым/верхним/нижним свойствами представления.

Я ожидаю, что вам нужно будет использовать один и тот же размер текста в двух действиях и использовать свойства scaleX и scaleY в запущенном действии, чтобы изменить размер текста по мере необходимости. Затем используйте комбинацию ChangeBounds и ChangeTransform в своем TransitionSet.

person klmprt    schedule 31.10.2014
comment
Да, похоже, что границы текстового представления анимируются правильно. Однако реальный текст не масштабируется! - person rlay3; 31.10.2014
comment
Отлично, этого мы и ожидали. Теперь попробуйте установить свойство масштаба в целевом действии и использовать вместе ChangeBounds и ChangeTransform в TransitionSet (например, TransitionSet.addTransition().addTransition()) - person klmprt; 31.10.2014
comment
@klmprt Я думал, что еще более простым решением будет создание пользовательского TextSizeTransition, например это ... Мне все еще не удалось заставить его работать так, как я хочу, но вы думаете, что что-то подобное находится на правильном пути? Я чувствую, что изменение свойств масштаба представления - это хакерский способ добиться этого эффекта, когда вы можете просто изменить размер текста... - person Alex Lockwood; 04.11.2014
comment
Чем больше я экспериментирую с пользовательскими переходами, тем больше мне кажется, что некоторые пользовательские переходы просто отказываются работать при использовании с общими переходами элементов... :/ - person Alex Lockwood; 04.11.2014
comment
@AlexLockwood TextSizeTransition мне нравится; вам может понадобиться использовать его в сочетании с ChangeBounds. Что не работало для вас? - person klmprt; 04.11.2014
comment
@klmprt Сегодня вечером я напишу короткий пример проекта, демонстрирующий проблему. Я действительно хочу понять, как писать пользовательские переходы самостоятельно, но единственный способ заставить их работать — это каким-то образом изменить общие представления в SharedElementCallback#onSharedElementStart(), и даже тогда это кажется таким хакерским. - person Alex Lockwood; 04.11.2014
comment
@AlexLockwood В зависимости от того, что вы изменяете, это, вероятно, нормально. Обратные вызовы onSharedElementStart и onSharedElementEnd предназначены для того, чтобы вы могли «установить сцену» по мере необходимости — в конечном итоге фреймворк не может сделать всю работу за вас. - person klmprt; 04.11.2014

Мои решения для TransitionAnimation, не совсем по теме но близко, возможно с доработками или просто кому пригодится.

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)
                }
            })
        }
    }
}
person maXp    schedule 13.07.2019