Android - 在 CoordinatorLayout 中使用时页脚滚动到屏幕外

Posted

技术标签:

【中文标题】Android - 在 CoordinatorLayout 中使用时页脚滚动到屏幕外【英文标题】:Android - footer scrolls off screen when used in CoordinatorLayout 【发布时间】:2015-08-26 23:11:08 【问题描述】:

我有一个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 从一开始就可见 - 否则,只有在向上滚动后才可见

【问题讨论】:

我浪费了半天时间试图弄清楚发生了什么。感谢找到您的问题,我刚刚摆脱了 CoordinatorLayout。一切都按预期进行 我有类似的问题,你能帮帮我吗:***.com/questions/47221647/… 【参考方案1】:

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_
    android:layout_>

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_
        android:layout_>

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_
            android:layout_
            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_
        android:layout_
        android:background="#C0C0C0"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <com.example.android.coordinatedeffort.widget.FooterBarLayout
        android:layout_
        android:layout_
        android:layout_gravity="bottom">

        <TextView
            android:layout_
            android:layout_
            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

【讨论】:

【参考方案2】:

用线性布局包围你的元素,像这样:

<android.support.design.widget.CoordinatorLayout >

  <LinearLayout
        android:orientation="vertical"
        android:layout_
        android:layout_>

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

【讨论】:

不要尝试这个答案,LinearLayout 将防止您的工具栏在滚动时折叠。正如官方文档 (developer.android.com/reference/com/google/android/material/…) 中所解释的那样,AppBarLayout 必须是 CoordinatorLayout 的直接子级,否则它将不起作用。【参考方案3】:
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,每个页面的内容长度可能不同。

【讨论】:

这是否适用于工具栏中的 scroll|enterAlways|snap 滚动标志?如何使用这种方法使我的工具栏滚动? @YuriHeupa 它确实适用于其他滚动标志。这种方法只是删除或添加 'SCROLL' 标志,不会改变其他标志。 When there is no 'SCROLL' flag other flags are just ignored 试过这个解决方案。当视图数量少于滚动高度但如果项目移动时根本不滚动时,它可以工作。这可能是在布局传递之后添加项目的时候。【参考方案4】:

我做了一些事情来确保我添加了 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 下方列出)才能正常工作

【讨论】:

看起来很老套。 另外,它在滚动视图的底部添加了太多的填充,所以内容远离页脚视图【参考方案5】:

我使用了 Learn OpenGL ES 解决方案 (https://***.com/a/33396965/778951) 的简化版本——它改进了 Noa 的解决方案 (https://***.com/a/31140112/1317564)。它适用于我在 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_
    android:layout_>

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_
        android:layout_>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_
                android:layout_
                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_
                android:layout_
                app:tabMode="fixed"/>

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_
        android:layout_
        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();
    

【讨论】:

非常感谢你在这件事上拉了我一天多的头发 @jhavatar 能否请您在此处添加有关此自定义行为如何工作的简要说明来提供帮助?它似乎工作得很好.. 一年多后,我的解决方案在迁移到支持库 23 时停止工作 - 使用了您的解决方案,效果很好 你拯救了我的一天 我有类似的问题,你能帮帮我吗:***.com/questions/47221647/…【参考方案6】:

我从 Noa 的解决方案 (https://***.com/a/31140112/1317564) 开始,它适用于手指拖动,但我遇到了甩动问题。在花了一些时间跟踪方法调用并尝试了不同的想法之后,我最终得到了以下解决方案:

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

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

【讨论】:

谢谢。这是迄今为止我找到的最好的解决方案。但是,它并不完美,因为它确实会产生闪烁效果。更多问题解释请参考***.com/questions/36098350/…。 我找到了一种方法,通过不使用 CoordinatorLayout - ***.com/a/36122127/72437 我尝试了您的解决方案。有用。但它给我的页脚留下了额外的填充。想不通原因。知道它是怎么发生的吗? 其他片段中也有额外的填充,它没有页脚。 花了几天时间获得了 50 个代表,所以我可以发表评论并感谢您的回答。这是正确的答案。另一个在许多情况下都有错误(离开片段然后返回等)。这修复了我与此问题相关的所有错误。【参考方案7】:

更新

下面的解决方案不适用于 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 并将其设置为 activityfragment 元素的行为来实现。

这里是代码的基础 - 它适用于只有工具栏的活动 - 你可以用 appbar.getTop() + toolbar.getHeight() 替换它,如果你的 appbar 包含 tabs。

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_
android:layout_>
<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_
    android:layout_
    android:elevation="3dp"
    app:elevation="3dp">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_
        android:layout_
        app:layout_scrollFlags="scroll|enterAlways"
        />
</android.support.design.widget.AppBarLayout>
<fragment
    android:id="@+id/fragment"
    android:name="com.example.noa.footer2.MainActivityFragment"
    android:layout_
    android:layout_
    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_
            android:layout_
            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_
    android:layout_
    android:background="@null"/>
<View
    android:layout_
    android:layout_
    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;
    

【讨论】:

在这一行获取类转换异常 CoordinatorLayout.LayoutParams lp = (LayoutParams) getView().getLayoutParams(); @VihaanVerma - 有什么例外? 哪个是用于投射的 LayoutParams 的导入?当使用 CoordinatorLayout.LayoutParams 进行转换时,它会为相对布局(布局中的根视图)提供类转换异常 CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) pager.getLayoutParams(); 不幸的是在动画过程中出现了问题(当您滑动并让它为工具栏的展开/折叠设置动画时)。【参考方案8】:

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

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

希望你解决了这个问题

【讨论】:

【参考方案9】:

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

【讨论】:

谢谢,这里有一个类似的答案,几天前被删除了(不是我)-您所指的解决方案不使用新的支持库

以上是关于Android - 在 CoordinatorLayout 中使用时页脚滚动到屏幕外的主要内容,如果未能解决你的问题,请参考以下文章

Android - 在 Android 1.6 中开发的应用程序可以在 Android 2.0 中运行吗?

如何在Android中启动JAVA程序

android中怎么设置组件在LinearLayout中居中

如何在android源代码中打印出日志

求教怎么在android的Logcat中输出日志

如何在Android中取得当前进程名