Search code examples
androidandroid-support-libraryandroiddesignsupport

CoordinatorLayout ignores margins for views with anchor


Given I'm using a layout like this:

<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>

Which is pretty much the standard Cheesesquare sample - except the FloatingActionButton, which I would like to move up by about 20dp.

However, this will not work. No matter if I use margin, padding etc - the button will always be centered at the edge of the anchor, like this:

FAB will ignore the margin

How can I move the FAB up by 20dp as intended?


Solution

  • As there might be bugs in the design-support-lib concerning the CoordinatorLayout & margins, I wrote a FrameLayout that implements/copies the same "Behavior" like the FAB and allows to set a padding to simulate the effect:

    Be sure to put it in the android.support.design.widget package as it needs to access some package-scoped classes.

    /*
     * 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;
            }
        }
    }