8

Background

Suppose you have a scrolling activity, with CoordinatorLayout and AppBarLayout, where you have a top header that can collapse upon scrolling at the bottom (which might have a RecyclerView or NestedScrollView).

Something like this:

enter image description here

Now, you need to put a loading view (say, a ProgressBar and a TextView inside a LinearLayout) in the center of the bottom area (where the text is shown), before the content appears.

The problem

I used a ViewAnimator to switch between the states, but if I choose to center the loading view, it is shown below the center, because the bottom area takes more space that is really shown (as you can scroll it).

enter image description here

What I tried

There are 2 solutions I've conducted that worked fine: 1. Get the size of the loading view and its parent, and use it to calculate the top margin of the loading view 2. Set the bottom padding of the loading view to be half of the upper area size (90dp in this case, which is half of the 180dp of the upper area). This is a bit easier, but if the UI of anything in the activity or the parent of the bottom area changes, it will ruin the fix for being in the center.

The code

Almost the same as the one from the IDE wizard, but here:

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>

The question

Thing is, I could calculate things myself, but I wonder if there is a different, easy way to do it. Maybe a flag of some sort that I'm unaware of ?

I ask this because the bottom area in the real app is more complex (Sample above is for demonstration of the issue), having a fragment or even a TabLayout&ViewPager with multiple fragments, so it's a bit weird to calculate things of the parent Activity in those fragments for this purpose.

Not only that, but I also have a tabLayout at the bottom (belongs to the activity) which makes it even more annoying to have into account.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • I didn't check the code, cuz I'm in a hurry..did you use gravity attribute / layout_centerVertical ? or layout_centerInParent ? Sorry for the rubish response.. – Ionut J. Bejan Jul 27 '17 at 13:51
  • @IonutJ.Bejan Yes, it's inside the above code. Centering is of the entire layout, which you can see as a whole only after you scroll (collapsed mode of the upper area), so you will see what I've got on the second screenshot. – android developer Jul 27 '17 at 13:55
  • please look at https://stackoverflow.com/a/58296374/940720 – gio Sep 24 '20 at 18:08

4 Answers4

2

try custom behaviour .

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
    }
}
Yarh
  • 4,459
  • 5
  • 45
  • 95
1

I think that removing app:layout_behavior="@string/appbar_scrolling_view_behavior" from the ViewAnimator should do the trick. Or if that does not work wrapping the ViewAnimator in a RelativeLayout without the behvariour so that that ViewGroup gets behind the AppBarLayout, hence your view will be centered.

Update 1 (not working)

Tested my suggestion of 2 views and it worked:

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

Just swap the visibility of @+id/loader and @+id/contentView in your View depending on your logic and that should work but sadly you miss the animation of ViewAnimator.

Update 2

I tested it with exactly this code (just changed your custom styles for android ones) and it centers the loader in the whole view. Just try adding/removing app:layout_behavior="@string/appbar_scrolling_view_behavior" in "@+id/loader" so you can see that the position of the loader changes:

With the behaviour it is centered on the area below the AppBarLayout.

Loading centered below AppBarLayout

Wihout it, it is centered on the screen.

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>

Update 3

Apparently when the CollapsingToolbarLayout is extended, it pushes the view below it downwards (if it has the app:layout_behavior="@string/appbar_scrolling_view_behavior") taking also into account the height of the inner Toolbar, that's why when it is in this state, the content of that view is not centered.

So one way to workaround this in this case that the view we want to center has transparent background and only occupies the center of the area is to take advantage of app:behavior_overlapTop / setOverlayTop(int) and overlap that view with the AppBarLayout with the height of the Toolbar (?attr/actionBarSize):

<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

Hope it helps

fmaccaroni
  • 3,846
  • 1
  • 20
  • 35
  • Won't work, plus the scrolling won't work well for the view within the ViewAnimatior, as the content (text in this sample) will be shown together with the top area. The behavior must exist on the child of the CoordinatorLayout, so that it will work fine. I tried it :( – android developer Jul 27 '17 at 14:02
  • And did you try moving the behaviour to the NestedScrollView? – fmaccaroni Jul 27 '17 at 14:05
  • Yes, it gets ignored, sadly. – android developer Jul 27 '17 at 14:14
  • What about adding 2 views to the Coordinator instead of just the ViewAnimator, one that has the behaviour with the NestedScroll and the other without the behaviour with the progress. You miss the ViewAnimator but you can swap the visibility to show/hide the progress and the NestedScrollView – fmaccaroni Jul 27 '17 at 14:31
  • Still doesn't work. The centering is of the entire area, and not of the bottom one. – android developer Jul 30 '17 at 07:03
  • Just tested it with the code of update2 and it works like a charm. Maybe it is a problem of your custom styles. If copying/pasting that code in your view does not work, please add the custom styles to the question and maybe I can help you to get to the solution. – fmaccaroni Jul 31 '17 at 15:21
  • Code of "update2" doesn't work for me. It doesn't allow to scroll and make the upper area expand. It just stays the same, even if the NestedScrollView is set to be visible and even after adding the flag you mentioned. It doesn't even start expanded. Please show entire code. Maybe you forgot writing here something. – android developer Aug 01 '17 at 06:50
  • Fixed in "Update 2" removing android:fitsSystemWindows="true" and setting a greater AppBarLayout height, hope it works now – fmaccaroni Aug 01 '17 at 13:51
  • Now it works, but the loader view isn't in the center of the bottom area, so it's back to what I tried. – android developer Aug 01 '17 at 14:41
  • As stated in the Update 2 if you add app:layout_behavior="@string/appbar_scrolling_view_behavior" to the "@+id/loader" it is centered in the bottom area, if you don't add it, it is centered on the screen (check the images); so just add the behaviour and that's it. – fmaccaroni Aug 01 '17 at 16:16
  • Already tried it. Still doesn't work. It's not in the center at all. It's lower, exactly as I've shown on my screenshot. – android developer Aug 01 '17 at 20:59
  • Fixed in "Update 3" with a workaround, hope this works for you – fmaccaroni Aug 03 '17 at 14:20
  • 1
    Now it works, but sadly because we have inner fragments and views, and we used ViewSwitcher (or ViewAnimator) to switch between the views, this is impossible to be used. In any case, I will accept this answer for now because I don't think there is a better solution than this for the simple case, and for calculations in the more complex case. – android developer Aug 06 '17 at 11:38
  • So what you did is add behavior_overlapTop ? Is it possible to set it in code, perhaps ? And then later to reset it ? – android developer Aug 06 '17 at 11:46
  • 1
    Yes it is possible with setOverlayTop(...), here you have https://stackoverflow.com/a/31039075/6248510 – fmaccaroni Aug 07 '17 at 13:40
  • And to disable, you set it to 0? – android developer Aug 08 '17 at 06:34
0

A bit more detailed explanation on @Yarh's answer which is pretty much perfect:

First create you own layout behavior (in my case I derived from AppBarLayout.ScrollingViewBehavior to preserve the original behavior)

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

Then in your layout just use the behavior on your ViewPager/FragmentContainer/ViewGroup or whatever, just make sure that its a sibling of the 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" />
luktant
  • 121
  • 1
  • 6
0

The top answer has a bug. If AppBarLayout is joined with RecyclerView, a keyboard appears, RecyclerView is scrolled above the keyboard, the keyboard hides, then appears an empty space (accupied by the keyboard before), the list cannot be scrolled there.

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() }
            }
        }
    }
}
CoolMind
  • 26,736
  • 15
  • 188
  • 224