在CoordinatorLayout中,是否可以轻松地将AppBarLayout下方视图的内容居中显示?

9

背景

假设您有一个可滚动的活动,带有CoordinatorLayout和AppBarLayout,在其中您有一个顶部标题栏,该标题栏可以在底部滚动时折叠(可能具有RecyclerView或NestedScrollView)。

类似于这样:

enter image description here

现在,您需要在内容出现之前在底部区域的中心放置一个加载视图(例如,一个LinearLayout中的ProgressBar和TextView)。

问题

我使用ViewAnimator来切换状态,但如果我选择将加载视图居中,则会显示在中心下方,因为底部区域占用的空间比实际显示的空间更大(因为您可以滚动它)。

enter image description here

我的解决方法

我尝试了两个有效的解决方法: 1. 获取加载视图和其父级的大小,并使用它来计算加载视图的上边距 2. 将加载视图的底部填充设置为上部区域大小的一半(在此情况下为90dp,这是上部区域180dp的一半)。这种方法稍微简单一些,但如果活动中的任何UI或底部区域的父级发生更改,则会破坏居中的修复。

代码

与IDE向导中的代码几乎相同,但这里:

ScrollingActivity.java

public class ScrollingActivity extends AppCompatActivity {
    private ViewAnimator mViewAnimator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
        mViewAnimator = findViewById(R.id.viewAnimator);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_scrolling, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            mViewAnimator.showNext();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

res/layout/activity_scrolling.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:fitsSystemWindows="true"
    tools:context="com.example.user.myapplication.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true" android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent"
            android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar">

            <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/AppTheme.PopupOverlay"/>

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

    <ViewAnimator
        android:id="@+id/viewAnimator" android:layout_width="match_parent" android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:id="@+id/loader" android:layout_width="match_parent" android:layout_height="match_parent"
            android:gravity="center" android:orientation="vertical">

            <ProgressBar
                android:layout_width="wrap_content" android:layout_height="wrap_content"/>

            <TextView
                android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="loading"/>

        </LinearLayout>

        <android.support.v4.widget.NestedScrollView
            android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content" android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin" android:text="@string/large_text"/>

        </android.support.v4.widget.NestedScrollView>
    </ViewAnimator>

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

res/menu/menu_scrolling.xml

<menu 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" tools:context="com.example.user.myapplication.ScrollingActivity">
    <item
        android:id="@+id/action_settings" android:orderInCategory="100" android:title="click"
        app:showAsAction="always"/>
</menu>

问题

我知道如何自己计算,但我想知道是否有其他简单的方法。也许有一些我不知道的标志?

我问这个问题是因为真实应用程序中的底部区域更加复杂(上面的示例仅演示问题),具有多个片段甚至带有 TabLayout&ViewPager 的多个片段,因此在这些片段中计算父 Activity 的内容有些奇怪。

不仅如此,我还在底部有一个 tabLayout(属于 activity),这使得考虑更加麻烦。


我没有检查代码,因为我很匆忙。你使用了gravity属性/layout_centerVertical还是layout_centerInParent?抱歉回复不太好。 - Ionut J. Bejan
@IonutJ.Bejan 是的,它在上面的代码中。居中是整个布局的,只有在滚动后才能完整地看到(上部区域的折叠模式),所以您将看到我在第二张截图中得到的内容。 - android developer
请查看以下链接:https://dev59.com/L1kS5IYBdhLWcg3wgXLf#58296374 - gio
4个回答

2
尝试自定义行为。
class ViewBelowAppBarBehavior @JvmOverloads constructor(
        context: Context? = null, attrs: AttributeSet? = null) : CoordinatorLayout.Behavior<View>(context, attrs) {

    override fun layoutDependsOn(parent: CoordinatorLayout?, child: View?, dependency: View?) = dependency is AppBarLayout

    override fun onDependentViewChanged(parent: CoordinatorLayout?, child: View?, dependency: View?): Boolean {
        val appBar = dependency as AppBarLayout
        // т.к. y - отрицательное число
        val currentAppBarHeight = appBar.height + appBar.y 
        val parentHeight = parent?.height ?: 0
        val placeHolderHeight = (parentHeight - currentAppBarHeight).toInt()
        child?.layoutParams?.height = placeHolderHeight
        child?.requestLayout()
        return false
    }
}

1
看起来很有前途?你能否在GitHub上发布一个POC,这样我就可以试用一下? - android developer

1
我认为从ViewAnimator中删除app:layout_behavior="@string/appbar_scrolling_view_behavior"应该可以解决问题。 如果这样不行,可以将ViewAnimator放在一个RelativeLayout中,但不设置behaviour,这样ViewGroup就会在AppBarLayout后面,因此您的视图将居中。
更新1(无效):
尝试了我的建议,在两个视图中进行测试,成功实现。
<android.support.design.widget.CoordinatorLayout
    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:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">

<android.support.design.widget.AppBarLayout
    android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height"
    android:fitsSystemWindows="true" android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent"
        android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar">

        <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/AppTheme.PopupOverlay"/>

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

<android.support.v4.widget.NestedScrollView
    android:id="@+id/contentView" android:layout_width="match_parent" android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <TextView
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin" android:text="@string/large_text"/>

</android.support.v4.widget.NestedScrollView>

<LinearLayout
    android:id="@+id/loader" android:layout_width="match_parent" android:layout_height="match_parent"
    android:gravity="center" android:orientation="vertical" android:visibility="gone">

    <ProgressBar
        android:layout_width="wrap_content" android:layout_height="wrap_content"/>

    <TextView
        android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="loading"/>

</LinearLayout>

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

只需根据您的逻辑在视图中交换@+id/loader和@+id/contentView的可见性,那应该能起作用,但遗憾的是您会错过ViewAnimator的动画。
更新2
我测试了正好这段代码(只是将您的自定义样式更改为android样式),它会将加载程序居中于整个视图。只需尝试添加/删除app:layout_behavior="@string/appbar_scrolling_view_behavior"在"@+id/loader"中,这样您就可以看到加载器的位置发生变化:
使用行为时,它位于AppBarLayout下方的区域中心。

Loading centered below AppBarLayout

没有它,它会居中显示在屏幕上。

Loading centered on the screen

<android.support.design.widget.CoordinatorLayout
    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"
    tools:context="com.example.user.myapplication.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar" 
        android:layout_width="match_parent" 
        android:layout_height="180dp" 
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout" 
            android:layout_width="match_parent" 
            android:layout_height="match_parent"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed" 
            app:toolbarId="@+id/toolbar">

            <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.v4.widget.NestedScrollView
        android:id="@+id/contentView" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:layout_margin="10dp" 
            android:text="@string/large_text"/>

    </android.support.v4.widget.NestedScrollView>

    <LinearLayout
        android:id="@+id/loader" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"
        android:gravity="center" 
        android:orientation="vertical" 
        android:visibility="gone">

        <ProgressBar
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="loading"/>

    </LinearLayout>

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

更新 3

显然,当CollapsingToolbarLayout被扩展时,它会向下推动其下方的视图(如果它具有app:layout_behavior="@string/appbar_scrolling_view_behavior"),同时还考虑内部工具栏的高度,这就是为什么在此状态下,该视图的内容不居中的原因。

因此,在这种情况下解决此问题的一种方法是,将要居中的视图设置为透明背景,并且仅占据区域的中心,利用app:behavior_overlapTop / setOverlayTop(int),将该视图与具有工具栏高度(?attr/actionBarSize)的AppBarLayout重叠:

<android.support.design.widget.CoordinatorLayout 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"
    tools:context="com.example.user.myapplication.ScrollingActivity">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="180dp"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
            app:toolbarId="@+id/toolbar">
            <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.v4.widget.NestedScrollView
        android:id="@+id/contentView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:text="@string/large_text" />
    </android.support.v4.widget.NestedScrollView>
    <LinearLayout
        android:id="@+id/loader"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="gone"
        app:behavior_overlapTop="?attr/actionBarSize"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <ProgressBar
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="loading"/>
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

Loading centered below CollapsingToolbarLayout extended

希望它有所帮助。

“update2”代码对我不起作用。它不允许滚动并使上部区域扩展。即使将NestedScrollView设置为可见并添加了您提到的标志,它仍然保持不变。它甚至没有开始扩展。请展示整个代码。也许您在这里忘记写些什么了。 - android developer
在“更新2”中修复了android:fitsSystemWindows =“true”并设置更大的AppBarLayout高度,希望现在可以正常工作。 - fmaccaroni
现在它可以工作了,但是加载视图不在底部区域的中心,所以又回到了我尝试过的那种情况。 - android developer
1
现在它可以工作了,但遗憾的是,由于我们有内部片段和视图,并且我们使用ViewSwitcher(或ViewAnimator)在视图之间切换,这是不可能使用的。无论如何,我现在会接受这个答案,因为我认为对于简单情况来说,没有比这更好的解决方案,而对于更复杂的情况下的计算也是如此。 - android developer
1
是的,可以使用setOverlayTop(...)实现,这里有一个链接https://dev59.com/_l0Z5IYBdhLWcg3wzC9W#31039075。 - fmaccaroni
显示剩余11条评论

0
更详细的解释,@Yarh的答案已经非常完美了:
首先创建自己的布局行为(在我的情况下,我从AppBarLayout.ScrollingViewBehavior派生出来以保留原始行为)。
class AdaptiveHeightScrollingViewBehavior(
    context: Context,
    attrs: AttributeSet
) : AppBarLayout.ScrollingViewBehavior(context, attrs) {

    override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        (dependency as? AppBarLayout)?.let { appBar ->
            val appBarHeight = appBar.height + appBar.y
            child.layoutParams.height = (parent.height - appBarHeight).toInt()
            child.requestLayout()
        }
        return super.onDependentViewChanged(parent, child, dependency)
    }
}

然后在您的布局中,只需在ViewPager / FragmentContainer / ViewGroup或其他任何地方使用该行为,只需确保它是AppBarLayout的同级元素即可。

<androidx.viewpager.widget.ViewPager
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="com.sample.behavior.AdaptiveHeightScrollingViewBehavior" />

如果您在快速滚动时使用此功能,会产生一些不必要的影响,例如巨大的空白重叠视图。 - martinseal1987

0
顶部答案有一个bug。如果AppBarLayoutRecyclerView结合使用,键盘会出现,RecyclerView在键盘上方滚动,键盘隐藏后,会出现一个空白区域(之前被键盘占据的),列表无法在那里滚动。

enter image description here

class AdaptiveHeightScrollingViewBehavior @JvmOverloads constructor(
    context: Context? = null,
    attrs: AttributeSet? = null
) : AppBarLayout.ScrollingViewBehavior(context, attrs) {

    override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: View,
        dependency: View
    ): Boolean {
        updateChildHeightIfAppBarLayout(dependency, child, parent)
        return super.onDependentViewChanged(parent, child, dependency)
    }

    override fun onLayoutChild(
        parent: CoordinatorLayout,
        child: View,
        layoutDirection: Int
    ): Boolean {
        for (view in parent.getDependencies(child)) {
            updateChildHeightIfAppBarLayout(view, child, parent)
        }
        return true
    }

    private fun updateChildHeightIfAppBarLayout(
        view: View,
        child: View,
        parent: CoordinatorLayout
    ) {
        if (view is AppBarLayout) {
            val appBarHeight = view.height + view.y
            val newHeight = (parent.height - appBarHeight).toInt()
            if (child.layoutParams.height != newHeight) {
                child.layoutParams.height = newHeight
                child.post { child.requestLayout() }
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接