Привет, %username%! Сегодня я хотел бы поделиться с тобой способом без лишних усилий реализовать анимированное изменение размеров компонента в приложении для Android.

Я много читал про анимацию, а вот использовать в своих интерфейсах до сих пор не довелось. Хотелось опробовать наконец всякие Layout Transitions, Animators, Layout Animations и написать по этому поводу статейку, чтобы и самому лучше запомнить, и другим разжевать. Закончилось, однако, всё гораздо прозаичней — кастомным ViewGroup и ObjectAnimator'ом.

Итак, мне захотелось сделать разворачивающийся при получении фокуса EditText, как в Chrome для Android, вот такой:



Быстро прошерстив StackOverflow для определения примерного направления движения нашёл 2 варианта реализации:

  1. Использовать ScaleAnimation.
  2. Так или иначе пошагово менять размер EditText'а и запрашивать requestLayout() на каждом шаге.

Первый вариант я сразу отмёл, как минимум, потому что буквы тоже растянутся. Второй вариант звучит куда логичней, за исключением того, что каждый шаг будет полностью отрабатывать цикл onMeasure/onLayout/onDraw для всей ViewGroup, хотя необходимо изменить отображение только EditText'а. К тому-же я подозревал, что такая анимация вовсе не будет смотреться плавной.

Берём за основу второй способ и начинаем думать как уйти от вызова requestLayout() на каждом шаге. Но начнём, как положено, с малого.

Пишем ViewGroup


Начнём с того, что создадим кастомный ViewGroup для размещения наших компонентов:

Разметка
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageButton
        style="@style/ImageButton"
        android:id="@+id/newTabButton"
        android:layout_width="@dimen/toolbar_button_size"
        android:layout_height="@dimen/toolbar_button_size"
        android:layout_gravity="start"
        android:contentDescription="@string/content_desc_add_tab"
        android:src="@drawable/ic_plus" />

    <Button
        android:id="@+id/tabSwitcher"
        android:layout_width="@dimen/toolbar_button_size"
        android:layout_height="@dimen/toolbar_button_size"
        android:layout_gravity="end"
        android:enabled="false" />

    <com.bejibx.webviewexample.widget.UrlBar
        android:id="@+id/urlContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5dp"
        android:freezesText="true"
        android:hint="@string/hint_url_container"
        android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
        android:inputType="textUri"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:singleLine="true"
        android:visibility="gone" />

</merge>


Код
public class ToolbarLayout extends ViewGroup
{
    private static final String TAG = ToolbarLayout.class.getSimpleName();
    private static final boolean DEBUG = true;

    private ImageButton mNewTabButton;
    private Button mTabSwitchButton;
    private UrlBar mUrlContainer;

    public ToolbarLayout(Context context)
    {
        super(context);
        initializeViews(context);
    }

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

    public ToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        initializeViews(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
    {
        super(context, attrs, defStyleAttr, defStyleRes);
        initializeViews(context);
    }

    private void initializeViews(Context context)
    {
        LayoutInflater.from(context).inflate(R.layout.fragment_address_bar_template, this, true);
        mUrlContainer = (UrlBar) findViewById(R.id.urlContainer);
        mNewTabButton = (ImageButton) findViewById(R.id.newTabButton);
        mTabSwitchButton = (Button) findViewById(R.id.tabSwitcher);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        if (DEBUG)
        {
            Log.d(TAG, LogHelper.onMeasure(widthMeasureSpec, heightMeasureSpec));
        }

        int widthConstrains = getPaddingLeft() + getPaddingRight();
        final int heightConstrains = getPaddingTop() + getPaddingBottom();
        int totalHeightUsed = heightConstrains;
        int childTotalWidth;
        int childTotalHeight;
        MarginLayoutParams lp;

        measureChildWithMargins(
                mNewTabButton,
                widthMeasureSpec,
                widthConstrains,
                heightMeasureSpec,
                heightConstrains);

        lp = (MarginLayoutParams) mNewTabButton.getLayoutParams();
        childTotalWidth = mNewTabButton.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        childTotalHeight =
                mNewTabButton.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        widthConstrains += childTotalWidth;
        totalHeightUsed += childTotalHeight;

        measureChildWithMargins(
                mTabSwitchButton,
                widthMeasureSpec,
                widthConstrains,
                heightMeasureSpec,
                heightConstrains);

        lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams();
        childTotalWidth =
                mTabSwitchButton.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        childTotalHeight =
                mTabSwitchButton.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        widthConstrains += childTotalWidth;
        totalHeightUsed = Math.max(childTotalHeight + heightConstrains, totalHeightUsed);

        /*
         * [FIXED] find out how to handle match_parent here
         * There was not a problem with match_parent interaction here. The real problem is
         * layout_height="wrap_content" on high-level container cause EditText to measure it's
         * height improperly. For now I'm just set layout_height on high-level layout to fixed value
         * (this make sense because of top-level layout structure, see activity_main.xml) which
         * measure EditText correctly.
         *
         * TODO I'm steel need to figure out whats going wrong in this particular case.
         */
        if (mUrlContainer.getVisibility() != GONE)
        {
            measureChildWithMargins(
                    mUrlContainer,
                    widthMeasureSpec,
                    widthConstrains,
                    heightMeasureSpec,
                    heightConstrains);

            lp = (MarginLayoutParams) mUrlContainer.getLayoutParams();
            childTotalWidth = mUrlContainer.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            childTotalHeight =
                    mUrlContainer.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            widthConstrains += childTotalWidth;
            totalHeightUsed = Math.max(childTotalHeight + heightConstrains, totalHeightUsed);
        }

        final int totalWidthUsed = widthConstrains;
        setMeasuredDimension(
                resolveSize(totalWidthUsed, widthMeasureSpec),
                resolveSize(totalHeightUsed, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight,
                            int parentBottom)
    {
        if (DEBUG)
        {
            Log.d(TAG, LogHelper.onLayout(changed, parentLeft, parentTop, parentRight,
                    parentBottom));
        }

        /*
         * Layout order:
         * 1. Layout "New tab" button on the left side.
         * 2. Layout "Tab switch" button on the right side.
         * 3. If url container is unfocused, layout it between "New tab" and "Tab switch" buttons.
         *    Otherwise layout it accordingly to mUrlContainerExpandedRect bounds.
         */
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();

        /*
         * Edges for url container left and right bounds. Move it during layout childs
         * located to right and left of url container.
         */
        int leftEdge = parentLeft + paddingLeft;
        int rightEdge = parentRight - paddingRight;

        int childLeft, childTop, childRight, childBottom, childWidth, childHeight;
        if (mNewTabButton.getVisibility() != GONE)
        {
            MarginLayoutParams lp = (MarginLayoutParams) mNewTabButton.getLayoutParams();

            childWidth = mNewTabButton.getMeasuredWidth();
            childHeight = mNewTabButton.getMeasuredHeight();
            childLeft = parentLeft + paddingLeft + lp.leftMargin;
            childTop = parentTop + paddingTop + lp.topMargin;
            childRight = childLeft + childWidth;
            childBottom = childTop + childHeight;

            mNewTabButton.layout(childLeft, childTop, childRight, childBottom);
            leftEdge = childRight + lp.rightMargin;
        }

        if (mTabSwitchButton.getVisibility() != GONE)
        {
            MarginLayoutParams lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams();

            childWidth = mTabSwitchButton.getMeasuredWidth();
            childHeight = mTabSwitchButton.getMeasuredHeight();
            childRight = parentRight - paddingRight - lp.rightMargin;
            childTop = parentTop + paddingTop + lp.topMargin;
            childLeft = childRight - childWidth;
            childBottom = childTop + childHeight;

            mTabSwitchButton.layout(childLeft, childTop, childRight, childBottom);
            rightEdge = childLeft - lp.leftMargin;
        }

        if (mUrlContainer.getVisibility() != GONE)
        {
            MarginLayoutParams lp = (MarginLayoutParams) mUrlContainer.getLayoutParams();

            childHeight = mUrlContainer.getMeasuredHeight();
            childLeft = leftEdge + lp.leftMargin;
            childTop = parentTop + paddingTop + lp.topMargin;
            childRight = rightEdge - lp.rightMargin;
            childBottom = childTop + childHeight;

            mUrlContainer.layout(childLeft, childTop, childRight, childBottom);
        }

    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p)
    {
        return new MarginLayoutParams(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams()
    {
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected void measureChildWithMargins(
            @NonNull View child,
            int parentWidthMeasureSpec,
            int widthUsed,
            int parentHeightMeasureSpec,
            int heightUsed)
    {
        MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec = getChildMeasureSpec(
                parentWidthMeasureSpec,
                widthUsed + layoutParams.leftMargin + layoutParams.rightMargin,
                layoutParams.width);

        int childHeightMeasureSpec = getChildMeasureSpec(
                parentHeightMeasureSpec,
                heightUsed + layoutParams.topMargin + layoutParams.bottomMargin,
                layoutParams.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}


Разметка содержит 3 элемента:

  1. Кнопка «Добавить таб», имеет фиксированный размер, находится слева.
  2. Кнопка «Выбрать таб», имеет фиксированный размер, находится справа.
  3. Поле для ввода URL (UrlBar, наследник от EditText'а), заполняет собой оставшееся свободное пространство.

Методы onMeasure и onLayout не представляют из себя ничего сложного — сначала меряем/располагаем кнопки, потом текстовое поле между ними.

Я делал всё это поверх другого примера, так что можно заметить присутствие лишнего кода. Например, кнопка «Добавить таб». Она отображается только при переключении в режим выбора таба, в нашем же случае она просто скрыта.

Добавляем аниматор


Сначала добавим параметр, который будет меняться во время анимации. Не будем напрямую изменять размер UrlBar'а из Animator'а, а введём переменную, которая будет отображать текущий прогресс анимации в процентах.

private static final float URL_FOCUS_CHANGE_FOCUSED_PERCENT = 1.0f;
private static final float URL_FOCUS_CHANGE_UNFOCUSED_PERCENT = 0.0f;

/**
 * 1.0 is 100% focused, 0 is unfocused
 */
private float mUrlFocusChangePercent;

Мы собираемся использовать ObjectAnimator, так что нужно добавить getter и setter для нашего параметра, однако, если minSdkVersion >= 14, то, чтобы избежать рефлексии, лучше создать поле класса Property для этого.

/**
 * Use actual property to avoid reflection when creating animators. For api from
 * 11 (3.0.X Honeycomb) to 13 (3.2 Honeycomb_mr2) we should use reflection (see {@link <a href="http://developer.android.com/guide/topics/graphics/prop-animation.html#object-animator">Animating with ObjectAnimator</a>}).
 * For older apis I'll recommend to use {@link <a href="http://nineoldandroids.com/">NineOldAndroids</a>} library.
 */
private final Property<ToolbarLayout, Float> mUrlFocusChangePercentProperty =
        new Property<ToolbarLayout, Float>(Float.class, "")
        {
            @Override
            public void set(ToolbarLayout object, Float value)
            {
                mUrlFocusChangePercent = value;
                mUrlContainer.invalidate();
                invalidate();
            }

            @Override
            public Float get(ToolbarLayout object)
            {
                return object.mUrlFocusChangePercent;
            }
        };

Теперь добавим 2 inner-класса и 2 поля для старта анимации.

private boolean mDisableRelayout;

private final UrlContainerFocusChangeListener mUrlContainerFocusChangeListener
            = new UrlContainerFocusChangeListener();

private class UrlContainerFocusChangeListener implements OnFocusChangeListener
{
    @Override
    public void onFocusChange(View v, boolean hasFocus)
    {
        if (DEBUG)
        {
            Log.d(TAG, LogHelper.onFocusChange(hasFocus));
        }

        // Trigger url focus animation
        if (mUrlFocusingLayoutAnimator != null && mUrlFocusingLayoutAnimator.isRunning())
        {
            mUrlFocusingLayoutAnimator.cancel();
            mUrlFocusingLayoutAnimator = null;
        }

        List<Animator> animators = new ArrayList<>();
        Animator animator;
        if (hasFocus)
        {
            animator = ObjectAnimator.ofFloat(this, mUrlFocusChangePercentProperty,
                URL_FOCUS_CHANGE_FOCUSED_PERCENT);
        }
        else
        {
            animator = ObjectAnimator.ofFloat(this, mUrlFocusChangePercentProperty,
                URL_FOCUS_CHANGE_UNFOCUSED_PERCENT);
        }
        animator.setDuration(URL_FOCUS_CHANGE_ANIMATION_DURATION_MS);
        animator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
        animators.add(animator);

        mUrlFocusingLayoutAnimator = new AnimatorSet();
        mUrlFocusingLayoutAnimator.playTogether(animators);

        mUrlFocusingLayoutAnimator.addListener(new UrlFocusingAnimatorListenerAdapter(hasFocus));
        mUrlFocusingLayoutAnimator.start();
    }
}

private class UrlFocusingAnimatorListenerAdapter extends AnimatorListenerAdapter
{
    private final boolean mHasFocus;

    public UrlFocusingAnimatorListenerAdapter(boolean hasFocus)
    {
        super();
        mHasFocus = hasFocus;
    }

    @Override
    public void onAnimationEnd(Animator animation)
    {
        mDisableRelayout = false;
        if (!hasFocus())
        {
            mTabSwitchButton.setVisibility(VISIBLE);
            requestLayout();
        }
    }

    @Override
    public void onAnimationStart(Animator animation)
    {
        if (mHasFocus)
        {
            mTabSwitchButton.setVisibility(GONE);
            requestLayout();
        }
        else
        {
            mDisableRelayout = true;
        }
    }
}

Не забудем зарегистрировать наш OnFocusChangeListener в initializeViews!
private void initializeViews(Context context)
{
    //...
    mUrlContainer.setOnFocusChangeListener(mUrlContainerFocusChangeListener);
}

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

  1. При изменении фокуса мы создаём ObjectAnimator, который пошагово изменяет переменную, обозначающую процент получения фокуса полем.
  2. На каждом шаге вызывается invalidate() для ViewGroup. Данный метод не приводит к переразметке, он только перерисовывает компонент.

Процесс получения фокуса UrlBar'ом будет происходить следующим образом:

  1. Скрываем все остальные элементы чтобы они не мешали отрисовке анимации (в нашем случае это кнопка переключения табов).
  2. Вызываем requestLayout() чтобы после завершения анимации реальные границы UrlBar'а совпадали с наблюдаемыми (помните, что после вызова requestLayout() методы onMeasure+onLayout могут быть вызваны с задержкой!).
  3. Начинаем пошагово менять процент выполнения анимации, вызывая на каждом шаге invalidate().
  4. Вручную на каждом шаге высчитываем границы UrlBar'а для текущего процента и перерисовываем его.

При потере фокуса UrlBar'ом скрывать элементы и вызывать requestLayout() нужно наоборот, в конце работы анимации. Также, введём переменную для отключения этапа разметки, и не забудем добавить изменения в методы onMeasure и onLayout:

private boolean mDisableRelayout;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    if (!mDisableRelayout)
    {
        // ...
    }
    else
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

@Override
protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight,
                        int parentBottom)
{
    if (!mDisableRelayout)
    {
        // ...
    }
}

Готовимся к рисованию


Чтобы посчитать размер UrlBar'а на каждом шаге нам нужно знать его начальный и конечный размер. Добавим 2 переменные, в которые будем запоминать этот размер и в очередной раз немного поменяем onLayout:

/**
 * Rectangle, which represents url container bounds relative to it's
 * parent bounds when unfocused.
 */
private final Rect mUrlContainerCollapsedRect = new Rect();

/**
 * Rectangle, which represents url container bounds relative to it's
 * parent bounds when FOCUSED.
 */
private final Rect mUrlContainerExpandedRect = new Rect();

@Override
protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight,
                        int parentBottom)
{
    //...
    updateUrlBarCollapsedRect();

    /*
     * Здесь задаётся финальный размер UrlBar'а. Мы хотим развернуть наш UrlBar на весь ViewGroup.
     */
    mUrlContainerExpandedRect.set(0, 0, parentRight, parentBottom);
}

/*
 * Запоминаем размер UrlBar'а без фокуса. Поскольку кнопка добавления таба не показывается
 * вместе с ним, то считаем только правую границу по ширине кнопки переключения табов.
 */
private void updateUrlBarCollapsedRect()
{
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingTop = getPaddingTop();

    int rightEdge = getMeasuredWidth() - paddingRight;

    MarginLayoutParams lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams();
    rightEdge -= (lp.leftMargin + mTabSwitchButton.getMeasuredWidth() + lp.rightMargin);

    lp = (MarginLayoutParams) mUrlContainer.getLayoutParams();
    int childHeight = mUrlContainer.getMeasuredHeight();
    int childLeft = paddingLeft + lp.leftMargin;
    int childTop = paddingTop + lp.topMargin;
    int childRight = rightEdge - lp.rightMargin;
    int childBottom = childTop + childHeight;

    mUrlContainerCollapsedRect.set(childLeft, childTop, childRight, childBottom);
}

Рисуем!


Помните, непосредственно во время анимации реальный размер UrlBar'а не меняется, это происходит либо в начале, либо в конце анимации, а по-умолчанию отрисовывает он себя в соответствии с границами, полученными на этапе разметки. Таким образом, во время анимации реальный размер компонента больше наблюдаемого. Чтобы уменьшить в этой ситуации наблюдаемый размер при отрисовке UrlBar'а воспользуемся хитростью — будем делать clipRect на canvas'е.

Ещё одна хитрость заключается в том, чтобы убрать фон у UrlBar'а и отрисовывать его вручную.

Немножечко меняем разметку.

<com.bejibx.webviewexample.widget.UrlBar
        ...
        android:background="@null" />

Вводим переменную для отрисовки фона.

private Drawable mUrlContainerBackground;

/**
 * Variable to store url background padding's. This is important when we use
 * 9-patch as background drawable.
 */
private final Rect mUrlBackgroundPadding = new Rect();

private void initializeViews(Context context)
{
    //...
    mUrlContainerBackground = ApiCompatibilityHelper.getDrawable(getResources(),
            R.drawable.textbox);
    mUrlContainerBackground.getPadding(mUrlBackgroundPadding);
}

И, наконец, отрисовка! Добавим в метод drawChild(Canvas, View, long) условие для UrlBar'а:

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime)
{
    if (child == mUrlContainer)
    {
        boolean clipped = false;

        if (mUrlContainerBackground != null)
        {
            canvas.save();

            int clipLeft = mUrlContainerCollapsedRect.left;
            int clipTop = mUrlContainerCollapsedRect.top;
            int clipRight = mUrlContainerCollapsedRect.right;
            int clipBottom = mUrlContainerCollapsedRect.bottom;

            int expandedLeft = mUrlContainerExpandedRect.left - mUrlBackgroundPadding.left;
            int expandedTop = mUrlContainerExpandedRect.top - mUrlBackgroundPadding.top;
            int expandedRight = mUrlContainerExpandedRect.right + mUrlBackgroundPadding.right;
            int expandedBottom =
                    mUrlContainerExpandedRect.bottom + mUrlBackgroundPadding.bottom;

            if (mUrlFocusChangePercent == URL_FOCUS_CHANGE_FOCUSED_PERCENT)
            {
                clipLeft = expandedLeft;
                clipTop = expandedTop;
                clipRight = expandedRight;
                clipBottom = expandedBottom;
            }
            else
            {
                // No need to compute those when url bar completely focused or unfocused.
                int deltaLeft = clipLeft - expandedLeft;
                int deltaTop = clipTop - expandedTop;
                int deltaRight = expandedRight - clipRight;
                int deltaBottom = expandedBottom - clipBottom;

                clipLeft -= deltaLeft * mUrlFocusChangePercent;
                clipTop -= deltaTop * mUrlFocusChangePercent;
                clipRight += deltaRight * mUrlFocusChangePercent;
                clipBottom += deltaBottom * mUrlFocusChangePercent;
            }

            mUrlContainerBackground.setBounds(clipLeft, clipTop, clipRight,
                    clipBottom);
            mUrlContainerBackground.draw(canvas);

            canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
            clipped = true;
        }

        boolean result = super.drawChild(canvas, mUrlContainer, drawingTime);
        if (clipped)
        {
            canvas.restore();
        }
        return result;
    }

    return super.drawChild(canvas, child, drawingTime);
}

Всё готово, можно запускать и смотреть:



Заключение


Принимаясь за работу, я ожидал, что задача окажется пустяковой и я справлюсь с ней буквально за один вечер. В который раз я натыкаюсь на эти грабли. Если у вас есть другие варианты реализации или замечания к текущей — обязательно поделитесь ими в комментариях.

Я же искренне надеюсь, что данный пример окажется для кого-то полезным. Удачи и да прибудет с вами плавная анимация!

Комментарии (14)


  1. Cher
    16.10.2015 01:12

    с размахом )
    не было бы проще одновременно менять padding и margin у вьюхи URL'а?
    легко можно добиться какого угодно эффекта разворачивания со стоянием самого контента на месте

    android:paddingTop="20dp"? 
    android:layout_marginTop="20dp"?
    
    анимируем к:
    
    android:paddingTop="40dp"
    android:layout_marginTop="0dp"?
    

    «контент» вьюхи на месте на месте, бэкграунд разъехался
    ps: в чем сакральный смысл наследоваться от ViewGroup, а не RelativeLayout? все эти onMeasure и тд.


    1. bejibx
      16.10.2015 01:53

      не было бы проще одновременно менять padding и margin у вьюхи URL'а?
      Было бы проще, но тогда на каждый шаг анимации приходилось бы по вызову onMargin+onLayout, что, на мой взгляд, не оптимально.
      ps: в чем сакральный смысл наследоваться от ViewGroup, а не RelativeLayout? все эти onMeasure и тд.
      В данном конкретном случае исключительно в том, чтобы иметь полный контроль над тем, как дочерние вьюхи измеряются и располагаются. В принципе, я бы мог реализовать всё тоже самое с использованием FrameLayout не потеряв в производительности. Но это только потому, что разметка достаточно простая. Чем сложнее разметка, тем ощутимей выгода от использования таких вот ViewGroup. Я как раз собираюсь перевести замечательную статью по этому поводу как только смогу достучаться до автора. Пока можете самостоятельно ознакомится с оригиналом.

      Кстати, почему вы упомянули именно RelativeLayout?


      1. Cher
        16.10.2015 11:49

        Было бы проще, но тогда на каждый шаг анимации приходилось бы по вызову onMargin+onLayout,

        если ToolbarLayout будет иметь фиксированную высоту (а в вашем случае это напрашивается) то не должно быть onLayout от изменения дочерних элементов.


        1. bejibx
          16.10.2015 12:27

          Кажется, вы не совсем верно понимаете процедуру размещения элементов. Сама по себе вьюха не имеет возможности разместить себя внутри контейнера, она только может намекнуть ему, что хочет расположиться вот здесь и иметь вот такой размер. А в обязанности контейнера входит задать вьюхе размер в соответствии с запрошенным (или просто по своему усмотрению), вычислить координаты для её размещения и сказать этой вьюхе «теперь ты имеешь вот такой размер и расположена вот по таким координатам». Другими словами, чтобы на самом деле изменить размер любого компонента, нужно заново выполнить measure+layout+draw на его контейнере. Вот материал на эту тему — How Android draws views.


          1. Cher
            16.10.2015 14:56

            имеется ввиду что если ToolbarLayout имеет фиксированные размеры то при изменении margin/padding его дочерней вьюхи то «measure+layout+draw» выполнится на ToolbarLayout но не вызовет ту же самую цепочку выше по иерархии.


            1. bejibx
              16.10.2015 15:08

              Да, но смысл был избавиться именно от этих вызовов у ToolbarLayout во время работы анимации.


  1. HotIceCream
    16.10.2015 10:15
    +4

    Такого эффекта как на последнем видео я добился следующим образом:
    1. В layout, который содержит EditText и Button добавил android:animateLayoutChanges=«true»
    2. В слушателе setOnFocusChangeListener изменяю visibility кнопки.


    1. bejibx
      16.10.2015 12:29

      Но вы умолчали о самом интересном — как именно вы заставляете EditText «разъезжаться» по всей ширине контейнера?

      PS: возможно вы просто не заметили на видео — изначально ширина и высота EditText'а меньше родительского контейнера. Когда он получает фокус — он не только уезжает своей правой границей на место исчезнувшей кнопки, он занимает собой весь родительский контейнер.


      1. HotIceCream
        16.10.2015 12:30

        Самый простой способ: LinearLayout + android:layout_weight=1


        1. bejibx
          16.10.2015 16:04

          Всё равно мы не поняли друг друга. Возьмём для примера простой xml:

          <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="60px"
          	android:orientation="horizontal">
          
              <EditText
                  android:id="@+id/urlContainer"
                  android:layout_width="0px"
                  android:layout_height="match_parent"
          		android:layout_weight="1"
                  android:layout_margin="10px" />
          
              <Button
                  android:id="@+id/tabSwitcher"
                  android:layout_width="60px"
                  android:layout_height="60px"
                  android:layout_gravity="right" />
          
          </LinearLayout>

          Допустим, компоненты отресайзились и мы получили следующее:
          • Контейнер, width: 400px, height: 60px
          • Button, width: 60px, height: 60px
          • EditText, width: 400-60-10-10=320px, height:60-10-10=40px

          Теперь при получении фокуса мы скрываем кнопку. При вашем подходе EditText расползётся вправо на место пропавшей кнопки, то есть станет 380x40px.

          Моя же задача — сделать так, чтобы он стал по размеру родительского контейнера — 400x60px, хотя-бы визуально.


          1. HotIceCream
            16.10.2015 16:33

            Ну еще добавить изменение padding у LineraLayout. И будет совсем точь в точь)

            Конечно, если потребуется более сложное поведение, то без customviewgroup не обойтись.


            1. bejibx
              16.10.2015 17:12

              Ну еще добавить изменение padding у LineraLayout. И будет совсем точь в точь)
              Воооот, я как раз надеялся, что вы подскажете как это сделать с использованием данного фреймворка.

              Конечно, если потребуется более сложное поведение, то без customviewgroup не обойтись.
              Тут дело скорее не в ViewGroup, как я уже отвечал выше, в данном конкретном случае всё то же самое можно сделать с помощью обычного FrameLayout, даже практически onMeasure и onLayout переписывать не придётся. Это уже я заморочился с прицелом на более сложное расположение элементов в будущем.


    1. Jukobob
      16.10.2015 12:49
      +1

      Опередили вы меня друг, но в дополнение скажу что надо в LayoutTransaction установить «CHANGING»

      Код
      public class SearchView extends LinearLayout implements View.OnFocusChangeListener {
          EditText editText;
          Button button;
          public SearchView(Context context) {
              super(context);
              init(context);
          }
      
          public SearchView(Context context, AttributeSet attrs) {
              super(context, attrs);
              init(context);
          }
      
          public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
              super(context, attrs, defStyleAttr);
              init(context);
          }
      
          void init(Context c){
              setOrientation(HORIZONTAL);
              setGravity(Gravity.CENTER_VERTICAL);
              LayoutTransition layoutTransition = new LayoutTransition();
              layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
              setLayoutTransition(layoutTransition);
              button = new Button(c);
              button.setText("+");
              button.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
              addView(button);
              editText = new EditText(c);
              editText.setHint("Address");
              editText.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
              editText.setOnFocusChangeListener(this);
              addView(editText);
          }
      
          @Override
          public void onFocusChange(View v, boolean hasFocus) {
              button.setVisibility(hasFocus ? GONE : VISIBLE);
          }
      }
      


      1. bejibx
        16.10.2015 16:16

        Только тогда у EditText LayoutParams должны быть

        <EditText
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="match_parent" />
        

        И эффект выходит немного другой

        Однако для простых анимаций, конечно, гораздо удобней и быстрее пользоваться именно LayoutTransitions.