CoordinatorLayout игнорирует поля для представлений с привязкой

Учитывая, что я использую такой макет:

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/main_content"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/flexible_space_image_height"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:statusBarScrim="@android:color/transparent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/mainView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />

    <android.support.design.widget.FloatingActionButton
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_marginBottom="20dp"
        app:fabSize="normal"
        app:layout_anchor="@id/appbar"
        app:layout_anchorGravity="bottom|center_horizontal"
        />

</android.support.design.widget.CoordinatorLayout>

Это в значительной степени стандартный пример Cheesesquare, за исключением кнопки FloatingActionButton, которую я хотел бы поднять примерно на 20 dp. .

Однако это не сработает. Независимо от того, использую ли я поля, отступы и т. д., кнопка всегда будет центрироваться на краю привязки, например:

FAB будет игнорировать поля

Как я могу поднять FAB на 20 dp, как предполагалось?


person Sebastian Roth    schedule 07.06.2015    source источник
comment
Сообщено на странице code.google.com/p/android/issues/detail? идентификатор=176096   -  person Sebastian Roth    schedule 07.06.2015
comment
В какой версии API возникает ошибка?   -  person lf215    schedule 09.02.2016


Ответы (6)


Я предлагаю элегантное решение для вас:

<android.support.design.widget.FloatingActionButton
    ...
    android:translationY="-20dp"
    ...
/>
person Duc Thang    schedule 29.04.2020
comment
учитывая общую производительность макета, это следует отметить как решение. Не используйте ненужные линейные макеты - person MEX; 25.12.2020
comment
это сводит меня с ума! так просто и никогда раньше я не использовал перевод в макете! обязательно поможет мне в других сложных ситуациях - person Fabio; 22.01.2021

Попробуйте поместить его в линейный макет с отступами:

<LinearLayout
  width=".."
  height=".."
  paddingBottom="20dp"
  app:layout_anchor="@id/appbar"
  app:layout_anchorGravity="bottom|center_horizontal">

  <android.support.design.widget.FloatingActionButton
        android:layout_width="70dp"
        android:layout_height="70dp"
        app:fabSize="normal" />

</LinearLayout>
person hasan    schedule 07.06.2015
comment
Спасибо. Хорошая попытка, НО: LinearLayout не объявляет @DefaultBehavior, который затем отключит взаимодействие с родительским CoordinatorLayout. Что касается FAB, я уверен, что вы видели демонстрацию сырного квадрата: он будет анимировать Out, как только будет достигнут порог прокрутки. Я все еще хочу эту функцию, просто с небольшим дополнительным нижним полем. - person Sebastian Roth; 07.06.2015
comment
как насчет заполнения вместо поля для FloatingActionButton. - person hasan; 07.06.2015
comment
Пробовал безуспешно. Как будто поля и отступы полностью игнорируются после установки поведения. - person Sebastian Roth; 07.06.2015

Поскольку может быть ошибки в дизайн-поддержке библиотеки, касающиеся CoordinatorLayout и полей, я написал FrameLayout, который реализует/копирует то же "Поведение", что и FAB, и позволяет установить padding для имитации эффекта:

Обязательно поместите его в пакет android.support.design.widget, так как ему нужен доступ к некоторым классам в пределах пакета.

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.widget.FrameLayout;

import com.company.android.R;

import java.util.List;

@CoordinatorLayout.DefaultBehavior(FrameLayoutWithBehavior.Behavior.class)
public class FrameLayoutWithBehavior extends FrameLayout {
    public FrameLayoutWithBehavior(final Context context) {
        super(context);
    }

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

    public FrameLayoutWithBehavior(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

    public static class Behavior extends android.support.design.widget.CoordinatorLayout.Behavior<FrameLayoutWithBehavior> {
        private static final boolean SNACKBAR_BEHAVIOR_ENABLED;
        private Rect mTmpRect;
        private boolean mIsAnimatingOut;
        private float mTranslationY;

        public Behavior() {
        }

        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayoutWithBehavior child, View dependency) {
            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
        }

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FrameLayoutWithBehavior child, View dependency) {
            if (dependency instanceof Snackbar.SnackbarLayout) {
                this.updateFabTranslationForSnackbar(parent, child, dependency);
            } else if (dependency instanceof AppBarLayout) {
                AppBarLayout appBarLayout = (AppBarLayout) dependency;
                if (this.mTmpRect == null) {
                    this.mTmpRect = new Rect();
                }

                Rect rect = this.mTmpRect;
                ViewGroupUtils.getDescendantRect(parent, dependency, rect);
                if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
                    if (!this.mIsAnimatingOut && child.getVisibility() == VISIBLE) {
                        this.animateOut(child);
                    }
                } else if (child.getVisibility() != VISIBLE) {
                    this.animateIn(child);
                }
            }

            return false;
        }

        private void updateFabTranslationForSnackbar(CoordinatorLayout parent, FrameLayoutWithBehavior fab, View snackbar) {
            float translationY = this.getFabTranslationYForSnackbar(parent, fab);
            if (translationY != this.mTranslationY) {
                ViewCompat.animate(fab)
                          .cancel();
                if (Math.abs(translationY - this.mTranslationY) == (float) snackbar.getHeight()) {
                    ViewCompat.animate(fab)
                              .translationY(translationY)
                              .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
                              .setListener((ViewPropertyAnimatorListener) null);
                } else {
                    ViewCompat.setTranslationY(fab, translationY);
                }

                this.mTranslationY = translationY;
            }

        }

        private float getFabTranslationYForSnackbar(CoordinatorLayout parent, FrameLayoutWithBehavior fab) {
            float minOffset = 0.0F;
            List dependencies = parent.getDependencies(fab);
            int i = 0;

            for (int z = dependencies.size(); i < z; ++i) {
                View view = (View) dependencies.get(i);
                if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
                    minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - (float) view.getHeight());
                }
            }

            return minOffset;
        }

        private void animateIn(FrameLayoutWithBehavior button) {
            button.setVisibility(View.VISIBLE);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                ViewCompat.animate(button)
                          .scaleX(1.0F)
                          .scaleY(1.0F)
                          .alpha(1.0F)
                          .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
                          .withLayer()
                          .setListener((ViewPropertyAnimatorListener) null)
                          .start();
            } else {
                Animation anim = android.view.animation.AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_in);
                anim.setDuration(200L);
                anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                button.startAnimation(anim);
            }

        }

        private void animateOut(final FrameLayoutWithBehavior button) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                ViewCompat.animate(button)
                          .scaleX(0.0F)
                          .scaleY(0.0F)
                          .alpha(0.0F)
                          .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
                          .withLayer()
                          .setListener(new ViewPropertyAnimatorListener() {
                              public void onAnimationStart(View view) {
                                  Behavior.this.mIsAnimatingOut = true;
                              }

                              public void onAnimationCancel(View view) {
                                  Behavior.this.mIsAnimatingOut = false;
                              }

                              public void onAnimationEnd(View view) {
                                  Behavior.this.mIsAnimatingOut = false;
                                  view.setVisibility(View.GONE);
                              }
                          })
                          .start();
            } else {
                Animation anim = android.view.animation.AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_out);
                anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                anim.setDuration(200L);
                anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() {
                    public void onAnimationStart(Animation animation) {
                        Behavior.this.mIsAnimatingOut = true;
                    }

                    public void onAnimationEnd(Animation animation) {
                        Behavior.this.mIsAnimatingOut = false;
                        button.setVisibility(View.GONE);
                    }
                });
                button.startAnimation(anim);
            }

        }

        static {
            SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
        }
    }
}
person Sebastian Roth    schedule 07.06.2015

Простой обходной путь - привязать случайный макет к тому месту, где был привязан FAB, дать ему определенное поле, а затем привязать FAB к случайному макету, например

<LinearLayout
 android:orientation="horizontal"
 android:id="@+id/fab_layout"
 android:layout_width="5dp"
 android:layout_height="5dp"
 android:layout_marginRight="80dp"
 android:layout_marginEnd="80dp"
 app:layout_anchor="@id/collapsing_toolbar"
 app:layout_anchorGravity="bottom|end"/>

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_map"
    app:layout_anchor="@id/fab_layout"
    app:elevation="6dp"
    app:pressedTranslationZ="12dp"
 />
person Hessesian    schedule 10.01.2016

Чтобы закрепить FloatingActionButton под AppBar следующим образом: Fab как привязка к AppBar

Расширьте FloatingActionButton и переопределите offsetTopAndBottom:

public class OffsetFloatingActionButton extends FloatingActionButton
{
    public OffsetFloatingActionButton(Context context)
    {
        this(context, null);
    }

    public OffsetFloatingActionButton(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);
        ViewCompat.offsetTopAndBottom(this, 0);
    }

    @Override
    public void offsetTopAndBottom(int offset)
    {
        super.offsetTopAndBottom((int) (offset + (getHeight() * 0.5f)));
    }
}
person user1185087    schedule 22.06.2016
comment
Это работает в том смысле, что смещает fab относительно стыка, но ломает анимацию скрытия/отображения, которая не знает о смещении. - person zyamys; 20.01.2017

Я смог обойти эту проблему, используя как layout_anchor, так и layout_anchorGravity вместе с некоторым дополнением. Атрибут привязки позволяет вам иметь позицию View относительно другого представления. Однако это работает не совсем так, как RelativeLayout. Подробнее об этом следовать.

layout_anchor указывает, какой View должен располагаться желаемый View (т. е. этот View должен располагаться относительно View, указанного в атрибуте layout_anchor). Затем layout_anchorGravity указывает, с какой стороны относительного View будет расположен текущий View, используя типичный Значения силы тяжести (top, bottom, center_horizontal и т. д.).

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

Чтобы обойти эту проблему, я применил к FAB некоторые отступы, достаточные для того, чтобы верхний край FAB касался нижнего края TextView:

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_anchor="@id/your_buttons_id_here"
    android:layout_anchorGravity="bottom"
    android:paddingTop=16dp" />

Возможно, вам придется увеличить отступ, чтобы получить желаемый эффект. Надеюсь, это поможет!

person coolDude    schedule 09.09.2018