Android - 在 CoordinatorLayout 中使用时,页脚会滚出屏幕

Android - footer scrolls off screen when used in CoordinatorLayout

我有一个 AppBarLayout,它会在滚动 RecyclerView 时滚出屏幕。 在 RecyclerView 下面有一个 RelativeLayout 是页脚。

页脚仅在向上滚动后显示 - 它表现得像

layout_scrollFlags="scroll|enterAlways"

但它没有任何滚动标志 - 这是一个错误还是我做错了什么?我希望它始终可见

滚动前

滚动后

更新

打开了一个 google issue - 它被标记为 'WorkingAsIntended' 这仍然没有帮助,因为我想要一个片段内页脚的工作解决方案。

更新 2

you can find the activity and the fragment xmls here -

请注意,如果 activity.xml 中的第 34 行 - 包含 app:layout_behavior="@string/appbar_scrolling_view_behavior" 的行被注释掉,文本 end 从一开始就可见 - 否则,它仅在向上滚动后可见

我认为创建固定的页眉和页脚可以解决您的问题。我会在评论中写下这个,但我没有 50 个代表。你可以弄清楚怎么做 here

有一个库可以解决您的问题。希望这真的对你有帮助 Here is the library

您提到的另一个问题修复了页脚。下面是相对布局,因此请在页脚上使用 android:layout_alignParentBottom="true" 功能。

希望你已经解决了问题

更新

下面的解决方案不适用于 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 和以前的版本之间没有区别,您应该只使用 getTop 并忽略此答案中的先前更新

原解 在查看了很多方向后发现解决方案实际上很简单 - 添加 paddingBottom 到片段并随着页面滚动调整它。

需要填充以覆盖工具栏 y 位置的变化 - 协调器布局将整个页面作为 工具栏 消失又出现。

这可以通过扩展 AppBarLayout.ScrollingViewBehavior 并将其设置为 activity[= 的 fragment 元素的行为来实现63=].

这是代码的基础知识 - 它适用于只有工具栏的 activity - 你可以用 appbar.getTop() + toolbar.getHeight() 替换它,如果你的 appbar 包括 选项卡 .

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());
    }

我的行为

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

我开始使用 Noa 的解决方案 (),它适用于手指拖动,但我 运行 遇到了快速移动的麻烦。在花了一些时间跟踪方法调用并尝试不同的想法后,这是我最终得到的解决方案:

// Workaround for https://code.google.com/p/android/issues/detail?id=177195
// Based off of solution originally found here: 
@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;
                }
            }
        });
    }
}

我不喜欢重新检查每个动画帧的布局,而且这个解决方案并不完美,因为如果以编程方式 expanding/collapsing 应用栏布局,我已经看到了一些问题,但现在我还没有找到更好的解决方案。性能在新设备上很好,在旧设备上可以接受。如果有人这样做,请随时将我的答案作为来源并重新发布。

我使用的是 Learn OpenGL ES 解决方案的简化版本 () -- which improves on Noa's solution ()。它适用于我简单的快速 return 工具栏,位于 TabLayout 上方,每个选项卡的 ViewPager 内容中都有页脚按钮。

只需将 FixScrollingFooterBehavior 设置为 View/ViewGroup 上的 layout_behavior,您希望在屏幕底部保持对齐。

布局:

<?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();
    }
}

我做了一些事情来确保我添加了 android:layout_gravity="end|bottom" 到我想要的 CoordinatorLayout

底部的 XML 中的布局

然后在代码中设置:

 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 轴上更高(在 XML 中的 RecyclerView 下方列出)才能正常工作

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);
            }
        }
    }
}

当从属视图(RecyclerViewNestedScrollView)中的滚动内容小于视图高度时,此行为基本上从 AppBarLayout 中删除了滚动标志 SCROLL,即。当不需要滚动时。它还会覆盖偏移滚动视图,这通常由 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>

Android CoordinatorLayout 底部布局行为示例

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