Should it be set at the AppBarLayout sibling's parent or at the first Scrollable View inside its sibling?
With Material Design for Android, there are Views that let us work with the behavior of the layout depending on its surroundings, one of them is the CoordinatorLayout, as this CodePath guide mentions:
CoordinatorLayout extends the ability to accomplish many of the
Google's Material Design scrolling effects. Currently, there are
several ways provided in this framework that allow it to work without
needing to write your own custom animation code.
The one I'm interested in now is:
Expanding or contracting the Toolbar or header space to make room for the main content.
So, we would use the AppBarLayout with a Toolbar with app:layout_scrollFlags set and another ViewGroup sibling to the AppBarLayout with app:layout_behavior.
My question is: in what exact ViewGroup (or maybe View) should we put that
app:layout_behavior?
So far, I've tried with (And they have all worked, and they are all siblings to the AppBarLayout):
Scrolling View
First ViewGroup inside a Scrollable View
ScrollView inside a ViewGroup
And this one didn't work:
ViewGroup with no Scrollable View children.
There are multiple examples online, but none of them really state where should you put it, like:
http://www.ingloriousmind.com/blog/quick-look-on-the-coordinatorlayout/
https://guides.codepath.com/android/Handling-Scrolls-with-CoordinatorLayout
https://developer.android.com/training/basics/firstapp/building-ui.html
https://www.bignerdranch.com/blog/becoming-material-with-android-design-support-library/
Check this link: https://developer.android.com/reference/android/support/design/widget/AppBarLayout.html
AppBarLayout also requires a separate scrolling sibling in order to
know when to scroll. The binding is done through the
AppBarLayout.ScrollingViewBehavior class, meaning that you
should set your scrolling view's behavior to be an instance of AppBarLayout.ScrollingViewBehavior. A string resource containing the
full class name is available.
They mentioned about that, it should be the View which will be shown under the AppBarLayout like this:
<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.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<!-- Your scrolling content -->
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<android.support.v7.widget.Toolbar
...
app:layout_scrollFlags="scroll|enterAlways"/>
<android.support.design.widget.TabLayout
...
app:layout_scrollFlags="scroll|enterAlways"/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
My question is: in what exact ViewGroup (or maybe View) should we put
that app:layout_behavior?
And in this link: http://guides.codepath.com/android/Handling-Scrolls-with-CoordinatorLayout
Next, we need to define an association between the AppBarLayout and
the View that will be scrolled. Add an app:layout_behavior to a
RecyclerView or any other View capable of nested scrolling such as
NestedScrollView. The support library contains a special string
resource #string/appbar_scrolling_view_behavior that maps to
AppBarLayout.ScrollingViewBehavior, which is used to notify the
AppBarLayout when scroll events occur on this particular view. The
behavior must be established on the view that triggers the event.
Make sure you added the appbar_scrolling_view_behavior field in your String.xml
<!-- The class name to the ScrollingChildBehavior required for AppBarLayout -->
<string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>
And as everyone knows we just can use this like below
<android.support.v7.widget.RecyclerView
android:id="#+id/rvSomeList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
Its just for info not OP answer.
app:layout_behavior should be set to those views which are direct child of Coordinator layout
AppBarLayout also requires a separate scrolling sibling in order to
know when to scroll.
This description from Android is woefully incomplete and caused me hours of wasted time.
Scrolling sibling is a misnomer and need not be a scrolling view of any type.
For example, below my AppBarLayout, I'm using a ViewPager2 that will render a Fragment that will render a Scrollview, so I needed to set app:layout_behavior="#string/appbar_scrolling_view_behavior" directly on the ViewPager2 in the main layout, NOT the deeply nested Scrollview in the fragment layout.
I also have no use for scrolling the AppBarLayout or any of its children on or off the screen, so I falsely assumed I could get away with not setting the app:layout_behavior anywhere.
Wrong.
This reveals a more insidious issue: AppBarLayout requires the scrolling sibling, yes. But not just to "know when to scroll", but to actually adjust the size of the sibling to fit properly on screen alongside it! Otherwise, the sibling maintains its configured size and will be nudged downward offscreen by the height of the AppBarLayout! You can even see this in Android Studio's layout editor.
Long story short: If you're going to use an AppBarLayout, you need to mark one of your views with app:layout_behavior="#string/appbar_scrolling_view_behavior", whether it's a scroll view or not.
I had to add the following to the gradle file otherwise it gave me a compile error.
implementation 'com.google.android.material:material:1.0.0'
Hope this would help some others too!
For someone who uses CoordinatorLayout with FragmentContainer and AppBarLayout:
It is really good to set the app:layout_behavior also on the container (not just on NestedScrollView or RecyclerView). It deletes unnecessary bottom margin of the FragmentContainer and guarantees that the appbar hides when the keyboard is shown.
Related
I have a RecyclerView using a LinearLayoutManager with HORIZONTAL orientation, nested inside a FrameLayout using the BottomSheet Behavior.
When attempting to drag vertically across the RecyclerView, the BottomSheet doesn't respond to the drag event. Presumably this is because vertical scrolling is disabled for a LayoutManager with horizontal orientation.
I've tried overriding LinearLayoutManager.canScrollVertically() and returning true. This sort of works.. If you drag vertically in a very careful manner, the BottomSheet will respond. As soon as any horizontal movement is involved however, the BottomSheet stops responding to vertical drag events.
I'm not sure if overriding canScrollVertically() is the right approach here - it certainly doesn't feel right from a UX point of view.
I've also noticed that if I use a ViewPager rather than a RecyclerView with a horizontally oriented LayoutManager, the BottomSheet responds to vertical swipe events as desired.
Is there some other method of LayoutManager, RecyclerView, BottomSheet Behavior, or something else altogether that can help propagate the vertical scroll events on to the BottomSheet Behavior?
There's an example of the problem here:
https://github.com/timusus/bottomsheet-test
(Problem can be reproduced with commit #f59a7031)
Just expand the first bottom sheet.
Where does the problem lies? In FrameLayout. BottomSheet works perfectly when put inside CoordinatorLayout. Then BottomSheet can pass it's scrolling state through CoordinatorLayout to other views put as direct children of CoordinatorLayout.
Why RecyclerView was not able to pass scroll state to BottomSheet? It is not a direct child of CoordinatorLayout. But there exists a way to pass them: RecyclerView must be in put in view that implements NestedScrollingParent and NestedScrollingChild. The answer to that is: NestedScrollView
So your fragment_sheetX.xml layouts should look like:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical"
android:fillViewport="true">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.NestedScrollView>
Notice also android:fillViewport="true" as otherwise, your RecyclerView will not take whole height.
However it still will not work. Why? RecyclerView must be told to pass vertical scrolling to parent. How? The answer is recyclerView.setNestedScrollingEnabled(false);, but that is better described here.
Btw: MultiSheetView is a great feature and a very interesting approach to mobile UX design.
What to implement: ConstraintLayout or CoordinatorLayout for proper material design in android ?
CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout
CoordinatorLayout is intended for two primary use cases:
As a top-level application decor or chrome layout
As a container for a specific interaction with one or more child views
By default, if you add multiple children to a FrameLayout, they would overlap each other. A FrameLayout should be used most often to hold a single child view. The main appeal of the CoordinatorLayout is its ability to coordinate the animations and transitions of the views within it. By specifying Behaviors for child views of a CoordinatorLayout you can provide many different interactions within a single parent and those views can also interact with one another. View classes can specify a default behavior when used as a child of a CoordinatorLayout using the CoordinatorLayout.DefaultBehavior annotation.
Behaviors may be used to implement a variety of interactions and additional layout modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons that stick to other elements as they move and animate.
ConstraintLayout is a super-powered ViewGroup similar to a RelativeLayout, but more flexible than RelativeLayout.
ConstraintLayout
ConstraintLayout allows you to create large and complex layouts with a flat view hierarchy (no nested view groups). It's similar to RelativeLayout in that all views are laid out according to relationships between sibling views and the parent layout, but it's more flexible than RelativeLayout and easier to use with Android Studio's Layout Editor.
ConstraintLayout can be used anywhere, you don't need any other ViewGroup like RelativeLayout, LinearLayout or FrameLayout once you start using ConstraintLayout.
There are currently various types of constraints that you can use:
Relative positioning
Margins
Centering positioning
Circular positioning
Visibility behavior
Dimension constraints
Chains
Virtual Helpers objects
Optimizer
What to implement: ConstraintLayout or CoordinatorLayout for proper material design in android ?
You may need to use both ConstraintLayout and CoordinatorLayout to build efficient UI and material animations.
A common example which uses both CoordinatorLayout and ConstraintLayout is given below for your reference.
Use Coordinatorlayout as the top-level application decor. It will usually used to layout AppBarLayout , FloatingActionButton, and the main body of your screen, say NestedScrollView. Inside the NestedScrollView use ConstraintLayout to describe the rest of the layout as a flat hierarchy.
<androidx.coordinatorlayout.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">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!-- Your scrolling content -->
<androidx.constraintlayout.widget.ConstraintLayout
...>
<!-- body of constraint layout -->
<Button android:id="#+id/button" ...
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<androidx.appcompat.widget.Toolbar
...
app:layout_scrollFlags="scroll|enterAlways"/>
<com.google.android.material.tabs.TabLayout
...
app:layout_scrollFlags="scroll|enterAlways"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
What do the above snippet? here you go.
We have placed the androidx.coordinatorlayout.widget.CoordinatorLayout as the root layout. And we put androidx.core.widget.NestedScrollView and com.google.android.material.appbar.AppBarLayout as direct children.
We defined app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" attribute for androidx.core.widget.NestedScrollView. This is the key point. We defined a behavior for the NestedScrollView. That is we are telling the Coordinator layout that the NestedScrollView depends on the AppBarLayout.
Of course, Behaviors don’t do anything on their own, but CoordinatorLayout does. It act accordingly and helps to intercept touch events, window insets, measurement, layout, and nested scrolling. So here, it places the NestedScrollView below the AppBarLayout as we instructed. Cool right?
We placed the ConstraintLayout inside the NestedScrollView to make it scrollable. As we already discussed, the ConstraintLayout is used to align child views with in the bounds of the ConstraintLayout.
Can I add ConstraintLayout inside another ConstraintLayout?
Of course yes, You can use any combination to align views as per your design requirements.
Can I add CoordinatorLayout inside another CoordinatorLayout ?
That is not a usual practice. the most common use case of CoordinatorLayout is as a the top-level application decor to coordinate between other direct children. But yes, if you really want to nest the CoordinatorLayout, you can do so by creating a custom CoordinatorLayout which extends the CoordinatorLayout and implements NestedScrollingChild to pass the scroll events to the parent CoordinatorLayout.
Bonus point
You can use the powerful MotionLayout which is a subclass of ConstraintLayout for building animations.
You may check this for a detailed example for custom animation using MotionLayout.
CoordinatorLayout is intended to be the top-level layout for activity to manage the Behaviors e.g. interactions and animations.
ConstraintLayout's main goal is to provide a convenient way to create a flat layout with multiple children (much more powerful RelativeLayout).
So the CoordinatorLayout is to manage the complex behavior (especially animations) of your activity's components, and ConstraintLayout for components proper placement (especially list items).
It seems like you (almost) always use a CoordinatorLayout, and sometimes use a ConstraintLayout inside. See the following resources
The codelab at https://codelabs.developers.google.com/codelabs/material-design-style/index.html#3 only uses a CoordinatorLayout
The example android-sunflower app ("illustrating Android development best practices") uses neither for the top-level activity, but uses both inside its fragment_plant_detail.xml, with the ConstraintLayout being inside the CoordinatorLayout:
<layout ...>
<data .../>
<android.support.design.widget.CoordinatorLayout ...>
<android.support.design.widget.AppBarLayout ...>
<android.support.design.widget.CollapsingToolbarLayout ...>
<ImageView... />
<android.support.v7.widget.Toolbar... />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView ...>
<android.support.constraint.ConstraintLayout ...>
<TextView.../>
<TextView... />
</android.support.constraint.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton ... />
</android.support.design.widget.CoordinatorLayout>
</layout>
#Darish has a great, comprehensive answer to this one.
I second everything he said and wanted to just add a little info. In my experience, a Constraint layout as the parent view is good enough most of the time. When you need to bring in the Coordinator Layout is when you have specific behaviors that you want to manage (for example Bottom Sheets). Coordinator Layout is more trouble than it's worth if you won't be using the behavior capabilities of it or if you are trying to mess with multiple views as CoordinatorLayout acts as a "super-powered FrameLayout".
I wrote a blog post a while ago with illustrations about the differences and usages of Coordinator vs. Constraint layout. Check it out here if you are interested.
I would also second the plug for MotionLayout as a great comprehensive way to add animations to your layouts without too much additional code! This Google Developers series with examples is a great way to get started with MotionLayout.
So, I have a situation, I need to create something like this:
Where white background view is nested view, and the image with all the elements is in the appbar. Now, I managed to create it without NestedScrollView being over the AppbarLayout, but how can I achieve that, how can I put NestedScrollView above AppBarLayout without loosing any functionality?
In oreder to make this work, you need to do following:
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_overlapTop="64dp"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
Where overlapTop is how much it will overlap and layout_behavior is the thing you need here. Hope it helps.
I would like to display on the bottom of the view frame layout which has some layouts inside of the FrameLayout hidden and are displayed after some button click. I would like to display the FrameLayout to start from bottom of the screen above all other views.
How can i do it in the right way please?
Many thanks for any advice.
I have implemented something similar to what you would like to achieve...first of all you should place your entire Activity of Fragment layout inside a CoordinatorLayout. Then you should place the FrameLayout which you want to show above all other views as the last View inside the CoordinatorLayout. You could give the FrameLayout a fixed height(200px) and the same negative value as margin bottom(-200px) so it can not be visible in the first place. And after some button click you can animate your desired view (in this case FrameLayout) to overlap all other views starting from the bottom of the screen.
<?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:orientation="vertical">
//Your views here
<FrameLayout //Your FrameLayout
android:layout_width="match_parent"
android:layout_height="200px" android:layout_marginBottom="-200px">
//Place whatever views you prefer here
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
Since Android Support Library 23.2 you can use Materials Bottom Sheets like the Google Map one:
Include the support design library (use the most recent version):
compile 'com.android.support:design:24.1.1'
You are supposed to use a component which is aware of nested scrolling like NestedScrollView (which extends FrameLayout by the way) and RecyclerView inside a CoordinatorLayout and add this behavior to it:
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
And to make it visible on the bottom just a little:
app:behavior_peekHeight="64dp"
I am using an NSV in a CL for the ability to have the toolbar compress when the NSV scrolls down. The problem that I am having is that my NSV is not scrolled to the top when it loads, instead, it is offset from the top of the NSV by quite a margin (I am not certain where this spacing is coming from, it's not in the layout).
Please take a look at the screen captures, the first one shows how the NSV loads and you can clearly see the NSV has scrolled down quite a bit from the top by comparing the second (when I scroll the NSV to the top manually):
I did some updates to this layout and it caused this to occur, previously, it loaded at the top without issue. However, I did not add any spacing that should have caused this.
Here is the layout I'm using for this:
<android.support.design.widget.CoordinatorLayout
android:id="#+id/cl_goal_detail"
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="0dp"
android:layout_weight="1">
<android.support.design.widget.AppBarLayout
android:id="#+id/abl_goal_detail"
android:layout_width="match_parent"
android:layout_height="144dp"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_goal_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/content_space_double"
app:collapsedTitleTextAppearance="#style/title.dark"
app:expandedTitleTextAppearance="#style/display3.plus.dark"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar_goal_detail"
style="#style/toolbar"
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/nsv_goal_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/content_space_half"
android:paddingLeft="#dimen/content_space_half"
android:paddingRight="#dimen/content_space_half"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<FrameLayout
android:id="#+id/container_goal_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="fill_vertical"/>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
Any ideas would be appreciated!
OK! After a solid DAY of debugging every single component of my layout and Fragment I identified what I believe is a bug.
First, the issue: Turns out that having elements in your NSV's child view that change visibility to View.GONE upon runtime are causing the list to scroll down. I noticed that the list scrolls to just above the element where the visibility was toggled (including any margins set on the view).
Second, the fix: I fixed this issue by setting all the views to have android:visibility="gone" in the xml layout, then, I toggle each view's visibility as needed. Previously, the views were visible by default and then I worked from there. I just needed to change my logic to start with them all GONE, not terribly difficult.
I assume this works because the views you are going to hide at runtime do not form a part of the overall height calculation when the NSV is created in onCreateView(). Once the fragment progresses past onCreateView() it's safe to dynamically change the views, however, if the views are calculated as part as the height in onCreateView() and THEN hidden with View.GONE, measurements go wonky and you end up with a list scrolled down significantly.
Have you tried adding below line in your viewgroup i.e. FrameLayout in your case
android:descendantFocusability="blocksDescendants"
I think this will also work for you.
If not try it adding in NSV.
In my case, there was an EditText near the bottom of my scrolling content that was grabbing focus. Since NestedScrollView does some weird layout stuff, the focused view didn't scroll to the top when the activity started, so the real cause was not readily apparent. Adding this to the NestedScrollView's child layout fixed it for me:
android:focusableInTouchMode="true"
Your post answer helped me a lot to find out my issue (btw, it was the same). But I got it worked in a different way. I guess you are using a RecyclerView. In my case I'm using 3 RecyclerViews. Well, from your answer I started hiding the recyclers and I found out just one of them was causing this issue. What I did is I populated with a postDelayed:
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
recyler.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
recyler.setAdapter(new MyAdapter(myList));
}
}, 3000);
That worked fine!