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

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

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

 layout_scrollFlags="scroll|enterAlways" 

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

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

Введите описание изображения здесь

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

Введите описание изображения здесь

Обновить

Открыла проблему google на этом – она ​​была отмечена как «WorkAsIntended», это все равно не помогает, потому что я хочу работать с нижним колонтитулом внутри фрагмента.

Обновление 2

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

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

Solutions Collecting From Web of "Android – нижний колонтитул прокручивается с экрана при использовании в CoordinatorLayout"

Я использую упрощенную версию решения Learn OpenGL ES ( https://stackoverflow.com/a/33396965/778951 ), которая улучшает решение Noa ( https://stackoverflow.com/a/31140112/1317564 ). Он отлично работает для моей простой панели быстрого возврата выше 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 к фрагменту и настройте его как свитки страниц.

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

Это может быть достигнуто путем расширения 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; } } 

Я начал с решения Ноа ( https://stackoverflow.com/a/31140112/1317564 ), и он работал на перетаскивание пальцев, но у меня возникали проблемы с выбросами. Проведя некоторое время, чтобы проследить вызовы метода и попробовать разные идеи, вот решение, в котором я закончил:

 // Workaround for https://code.google.com/p/android/issues/detail?id=177195 // Based off of solution originally found here: https://stackoverflow.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 , где длина содержимого может отличаться на каждой странице.

Я думаю, что создание фиксированного верхнего и нижнего колонтитула может решить вашу проблему. Я бы написал это в комментариях, но у меня нет 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:layout_alignParentBottom="true" на нижнем колонтитуле.

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