Android - нижний колонтитул прокручивается за пределы экрана при использовании в CoordinatorLayout

У меня есть AppBarLayout который прокручивается за пределы экрана при прокрутке RecyclerView, Ниже RecyclerView E сть RelativeLayout это нижний колонтитул

Нижний колонтитул отображается только после прокрутки вверх - он ведет себя как

layout_scrollFlags="scroll|enterAlways"

но у него нет флагов прокрутки - это ошибка или я что-то не так делаю? Я хочу, чтобы это было всегда видно

перед прокруткой

после прокрутки

Обновить

открыл вопрос Google по этому вопросу - он был помечен "WorkingAsIntended", это все равно не помогает, потому что я хочу рабочее решение нижнего колонтитула внутри фрагмента.

Обновление 2

Вы можете найти активность и фрагмент xmls здесь -

обратите внимание, что если строка 34 в activity.xml - строка, содержащая app:layout_behavior="@string/appbar_scrolling_view_behavior" закомментирован конец текста виден с самого начала - в противном случае он виден только после прокрутки вверх

9 ответов

Решение

Я использую упрощенную версию решения Learn OpenGL ES ( /questions/29177560/android-nizhnij-kolontitul-prokruchivaetsya-za-predelyi-ekrana-pri-ispolzovanii-v-coordinatorlayout/29177565#29177565), которая улучшает решение Noa ( /questions/29177560/android-nizhnij-kolontitul-prokruchivaetsya-za-predelyi-ekrana-pri-ispolzovanii-v-coordinatorlayout/29177564#29177564). Он отлично работает для моей простой панели быстрого возврата над TabLayout с кнопками нижнего колонтитула в содержимом ViewPager каждой вкладки.

Просто установите FixScrollingFooterBehavior как layout_behavior в View/ViewGroup, который вы хотите сохранить выравниваемым в нижней части экрана.

Планировка:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?android:attr/actionBarSize"
                android:minHeight="?android:attr/actionBarSize"
                app:title="Foo"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                />

            <android.support.design.widget.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:tabMode="fixed"/>

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.spreeza.shop.ui.widgets.FixScrollingFooterBehavior"
        />

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

Поведение:

public class FixScrollingFooterBehavior extends AppBarLayout.ScrollingViewBehavior {

    private AppBarLayout appBarLayout;

    public FixScrollingFooterBehavior() {
        super();
    }

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

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        if (appBarLayout == null) {
            appBarLayout = (AppBarLayout) dependency;
        }

        final boolean result = super.onDependentViewChanged(parent, child, dependency);
        final int bottomPadding = calculateBottomPadding(appBarLayout);
        final boolean paddingChanged = bottomPadding != child.getPaddingBottom();
        if (paddingChanged) {
            child.setPadding(
                child.getPaddingLeft(),
                child.getPaddingTop(),
                child.getPaddingRight(),
                bottomPadding);
            child.requestLayout();
        }
        return paddingChanged || result;
    }


    // Calculate the padding needed to keep the bottom of the view pager's content at the same location on the screen.
    private int calculateBottomPadding(AppBarLayout dependency) {
        final int totalScrollRange = dependency.getTotalScrollRange();
        return totalScrollRange + dependency.getTop();
    }
}

Обновить

Приведенное ниже решение не работает для 5.1, так как оно работает в 5 - вместо getTop используйте getTranslationY в любых ваших вычислениях.

layout.getTop()-->(int)layout.getTranslationY()
appbar.getTop()+toolbar.getHeight()-->(int)(appbar.getTranslationY()+toolbar.getHeight())

Обновление 2 с новой библиотекой поддержки - 22.2.1 - нет различий между версиями 5.1 и prev, вы должны использовать только getTop и игнорировать предыдущее обновление в этом ответе

Исходное решение После изучения многих направлений оказывается, что решение на самом деле простое - добавьте paddingBottom к фрагменту и настройте его по мере прокрутки страницы.

Заполнение необходимо, чтобы скрыть изменения в положении панели инструментов y - раскладка координатора перемещает всю страницу вверх и вниз по мере исчезновения и повторного появления панели инструментов.

Это может быть достигнуто путем расширения AppBarLayout.ScrollingViewBehavior и устанавливая это как поведение элемента фрагмента действия.

Вот основы кода - он работает для действия только с панелью инструментов - вы можете заменить его на appbar.getTop() + toolbar.getHeight() и это будет работать лучше, если ваша панель приложений содержит вкладки.

activity.xml

<android.support.design.widget.CoordinatorLayout
android:id="@+id/main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:elevation="3dp"
    app:elevation="3dp">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|enterAlways"
        />
</android.support.design.widget.AppBarLayout>
<fragment
    android:id="@+id/fragment"
    android:name="com.example.noa.footer2.MainActivityFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="com.example.noa.footer2.MyBehavior"
    tools:layout="@layout/fragment"/>
</android.support.design.widget.CoordinatorLayout>

fragment.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingBottom="48dp"
            android:background="@android:color/holo_green_dark"
            tools:context=".MainActivityFragment">
<android.support.v7.widget.RecyclerView
    android:id="@+id/list"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@null"/>
<View
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_alignParentBottom="true"
    android:background="@android:color/holo_red_light"/>
</RelativeLayout>

MainActivityFragment # onActivityCreated

    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        CoordinatorLayout.LayoutParams lp = (LayoutParams) getView().getLayoutParams();
        MyBehavior behavior = (MyBehavior) lp.getBehavior();
        behavior.setLayout(getView());
    }

MyBehavior

public class MyBehavior extends AppBarLayout.ScrollingViewBehavior {

    private View layout;

    public MyBehavior() {
    }

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

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        boolean result = super.onDependentViewChanged(parent, child, dependency);
        if (layout != null) {
            layout.setPadding(layout.getPaddingLeft(), layout.getPaddingTop(), layout
                .getPaddingRight(), layout.getTop());
        }
        return result;
    }

    public void setLayout(View layout) {
        this.layout = layout;
    }
}

Я начал с решения Ноа ( /questions/29177560/android-nizhnij-kolontitul-prokruchivaetsya-za-predelyi-ekrana-pri-ispolzovanii-v-coordinatorlayout/29177564#29177564), и оно работало от перетаскивания пальцев, но у меня были проблемы с бросками. Потратив некоторое время, чтобы отследить вызовы метода и опробовать различные идеи, вот решение, которое я выбрал:

// Workaround for https://code.google.com/p/android/issues/detail?id=177195
// Based off of solution originally found here: https://stackru.com/a/31140112/1317564
@SuppressWarnings("unused")
public class CustomScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
    private AppBarLayout appBarLayout;
    private boolean onAnimationRunnablePosted = false;

    @SuppressWarnings("unused")
    public CustomScrollingViewBehavior() {

    }

    @SuppressWarnings("unused")
    public CustomScrollingViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        if (appBarLayout != null) {
            // We need to check from when a scroll is started, as we may not have had the chance to update the layout at
            // the start of a scroll or fling event.
            startAnimationRunnable(child, appBarLayout);
        }
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, final View child, int parentWidthMeasureSpec, int widthUsed,
                                  int parentHeightMeasureSpec, int heightUsed) {
        if (appBarLayout != null) {
            final int bottomPadding = calculateBottomPadding(appBarLayout);
            if (bottomPadding != child.getPaddingBottom()) {
                // We need to update the padding in onMeasureChild as otherwise we won't have the correct padding in
                // place when the view is flung, and the changes done in onDependentViewChanged will only take effect on
                // the next animation frame, which means it will be out of sync with the new scroll offset. This is only
                // needed when the view is flung -- when dragged with a finger, things work fine with just
                // implementing onDependentViewChanged().
                child.setPadding(child.getPaddingLeft(), child.getPaddingTop(), child.getPaddingRight(), bottomPadding);
            }
        }

        return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, final View child, final View dependency) {
        if (appBarLayout == null)
            appBarLayout = (AppBarLayout) dependency;

        final boolean result = super.onDependentViewChanged(parent, child, dependency);
        final int bottomPadding = calculateBottomPadding(appBarLayout);
        final boolean paddingChanged = bottomPadding != child.getPaddingBottom();
        if (paddingChanged) {
            // If we've changed the padding, then update the child and make sure a layout is requested.
            child.setPadding(child.getPaddingLeft(),
                    child.getPaddingTop(),
                    child.getPaddingRight(),
                    bottomPadding);
            child.requestLayout();
        }

        // Even if we didn't change the padding, if onDependentViewChanged was called then that means that the app bar
        // layout was changed or was flung. In that case, we want to check for these changes over the next few animation
        // frames so that we can ensure that we capture all the changes and update the view pager padding to match.
        startAnimationRunnable(child, dependency);
        return paddingChanged || result;
    }

    // Calculate the padding needed to keep the bottom of the view pager's content at the same location on the screen.
    private int calculateBottomPadding(AppBarLayout dependency) {
        final int totalScrollRange = dependency.getTotalScrollRange();
        return totalScrollRange + dependency.getTop();
    }

    private void startAnimationRunnable(final View child, final View dependency) {
        if (onAnimationRunnablePosted)
            return;

        final int onPostChildTop = child.getTop();
        final int onPostDependencyTop = dependency.getTop();
        onAnimationRunnablePosted = true;
        // Start looking for changes at the beginning of each animation frame. If there are any changes, we have to
        // ensure that layout is run again so that we can update the padding to take the changes into account.
        child.postOnAnimation(new Runnable() {
            private static final int MAX_COUNT_OF_FRAMES_WITH_NO_CHANGES = 5;
            private int previousChildTop = onPostChildTop;
            private int previousDependencyTop = onPostDependencyTop;
            private int countOfFramesWithNoChanges;

            @Override
            public void run() {
                // Make sure we request a layout at the beginning of each animation frame, until we notice a few
                // frames where nothing changed.
                final int currentChildTop = child.getTop();
                final int currentDependencyTop = dependency.getTop();
                boolean hasChanged = false;

                if (currentChildTop != previousChildTop) {
                    previousChildTop = currentChildTop;
                    hasChanged = true;
                    countOfFramesWithNoChanges = 0;
                }
                if (currentDependencyTop != previousDependencyTop) {
                    previousDependencyTop = currentDependencyTop;
                    hasChanged = true;
                    countOfFramesWithNoChanges = 0;
                }
                if (!hasChanged) {
                    countOfFramesWithNoChanges++;
                }
                if (countOfFramesWithNoChanges <= MAX_COUNT_OF_FRAMES_WITH_NO_CHANGES) {
                    // We can still look for changes on subsequent frames.
                    child.requestLayout();
                    child.postOnAnimation(this);
                } else {
                    // We've encountered enough frames with no changes. Do a final layout request, and don't repost.
                    child.requestLayout();
                    onAnimationRunnablePosted = false;
                }
            }
        });
    }
}

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

package pl.mkaras.utils;

import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;

public class ScrollViewBehaviorFix extends AppBarLayout.ScrollingViewBehavior {

    public ScrollViewBehaviorFix() {
        super();
    }

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

    public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                                  int heightUsed) {
        if (child.getLayoutParams().height == -1) {
            List<View> dependencies = parent.getDependencies(child);
            if (dependencies.isEmpty()) {
                return false;
            }

            final AppBarLayout appBar = findFirstAppBarLayout(dependencies);
            if (appBar != null && ViewCompat.isLaidOut(appBar)) {
                int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                if (availableHeight == 0) {
                    availableHeight = parent.getHeight();
                }

                final int height = availableHeight - appBar.getMeasuredHeight();
                int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);

                parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
                int childContentHeight = getContentHeight(child);

                if (childContentHeight <= height) {
                    updateToolbar(parent, appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed, false);

                    heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
                    parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);

                    return true;
                } else {
                    updateToolbar(parent, appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed, true);

                    return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
                }
            }
        }

        return false;
    }

    private static int getContentHeight(View view) {
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;

            int contentHeight = 0;
            for (int index = 0; index < viewGroup.getChildCount(); ++index) {
                View child = viewGroup.getChildAt(index);
                contentHeight += child.getMeasuredHeight();
            }
            return contentHeight;
        } else {
            return view.getMeasuredHeight();
        }
    }

    private static AppBarLayout findFirstAppBarLayout(List<View> views) {
        int i = 0;

        for (int z = views.size(); i < z; ++i) {
            View view = views.get(i);
            if (view instanceof AppBarLayout) {
                return (AppBarLayout) view;
            }
        }

        throw new IllegalArgumentException("Missing AppBarLayout in CoordinatorLayout dependencies");
    }

    private void updateToolbar(CoordinatorLayout parent, AppBarLayout appBar, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                               int heightUsed, boolean toggle) {
        toggleToolbarScroll(appBar, toggle);

        appBar.forceLayout();
        parent.onMeasureChild(appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }

    private void toggleToolbarScroll(AppBarLayout appBar, boolean toggle) {
        for (int index = 0; index < appBar.getChildCount(); ++index) {
            View child = appBar.getChildAt(index);

            if (child instanceof Toolbar) {
                Toolbar toolbar = (Toolbar) child;
                AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
                int scrollFlags = params.getScrollFlags();

                if (toggle) {
                    scrollFlags |= AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL;
                } else {
                    scrollFlags &= ~AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL;
                }

                params.setScrollFlags(scrollFlags);
            }
        }
    }
}

Такое поведение в основном убирает флаг прокрутки SCROLL от AppBarLayout, при прокрутке контента в зависимом просмотре (RecyclerView, NestedScrollView) меньше высоты просмотра, т.е. когда прокрутка не нужна. Он также переопределяет смещение прокрутки, которое обычно выполняется AppBarLayout.ScrollingViewBehavior, Хорошо работает при добавлении нижнего колонтитула, т.е. кнопка, чтобы прокрутить вид или в ViewPagerгде длина контента может быть разной на каждой странице.

Окружите свои элементы линейным слоем, вот так:

<android.support.design.widget.CoordinatorLayout >

  <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout>
      <android.support.v7.widget.Toolbar />
    </android.support.design.widget.AppBarLayout>
    <include layout="@layout/content_main" />

    </LinearLayout>

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

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

Я сделал что-то вроде того, что добавил android:layout_gravity="end|bottom" к макету в XML, который я хотел в нижней части CoordinatorLayout

и затем установите в коде:

 mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @SuppressLint("NewApi")
        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
            if (mFooterView != null) {
                final int height = mFooterView.getHeight();
                mRecyclerView.setPadding(0, 0, 0, height);
                mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        }
    });

Обратите внимание: что для правильной работы нижний колонтитул View/ViewGroup должен быть выше по оси z (указан под RecyclerView в XML)

Пример использования Android CoordinatorLayout Bottom Layout

activity_bottom.xml

<android.support.design.widget.CoordinatorLayout 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.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimaryDark"
            app:layout_scrollFlags="scroll|enterAlways"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
    </android.support.design.widget.AppBarLayout>

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

    <com.example.android.coordinatedeffort.widget.FooterBarLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_gravity="bottom">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="#007432"
            android:gravity="center"
            android:text="Footer View"
            android:textColor="@android:color/white"
            android:textSize="25sp" />
    </com.example.android.coordinatedeffort.widget.FooterBarLayout>

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

FooterBarLayout.java

FooterBarBehavior.java

Существует библиотека для вашей проблемы. Надеюсь, это действительно поможет вам Вот библиотека

И еще одна проблема, которую вы упомянули, исправила нижний колонтитул. ниже приведен относительный макет, поэтому используйте функцию android:layout_alignParentBottom="true" на вашем нижнем колонтитуле.

Надеюсь, я решил проблему