Parallax scrolling is a technique in computer graphics and web design, where background images move by the camera slower than foreground images, creating an illusion of depth in a 2D scene and adding to the immersion.
Before android developed some layouts and widgets like CoordinatorLayout
, AppBarLayout
, CollapsingToolbarLayout
, Toolbar etc it was very difficult to make parallax header view in android because you need to write a lot of code for that
Before we start jumping in and see all types of scrolling behavior, we needs to be clear about the basic setup and implementation. Use Design Support Library to achieve AppBar scrolling behavior. This library provides many of the material design components.
In app build.gradle
dependencies { ... compile 'com.android.support:design:25.0.1' compile 'com.android.support:recyclerview-v7:25.0.1' compile 'com.android.support:cardview-v7:25.0.1' }
Open your app XML layout file and add CoordinatorLayout
, AppBarLayout
, CollapsingToolbarLayout
using design support library and inside CollapsingToolbarLayout
add an ImageView
for header parallax image and Toolbar
for appbar/actionbar. Following is the complete content of XML layout file.
<?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:id="@+id/coordinatorLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="200dp" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:id="@+id/ivParallax" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@drawable/city" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:title="Cities" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/nestedScrollView" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/rvItems" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout>
We have a couple of new Layout types which we’ve added in here. The outer one is CoordinatorLayout
. This is the main workhorse which does an awful lot for us yet requires very little in the way of configuration. It is essentially a marshaller which applies externally defined behaviours to the child Views
within CoordinatorLayout
.
CoordinatorLayout
CoordinatorLayout
is introduced in android's design support library. There are many views available in design support library which requires CoordinatorLayout
to work properly. CoordinatorLayout is much like FrameLayout
in android but what makes CoordinatorLayout
different from other layouts is its behavior attribute.
The main appeal of the CoordinatorLayout
is its ability to coordinate the animations and transitions of the views within it. Using xml only, you can describe a layout where a FAB
moves out of the way of an incoming Snackbar
, for example, or have a FAB
(or any other View
really) that is apparently attached to another widget, and moves on screen with the widget.
Without using behavior attribute, CoordinatorLayout
will be same as FrameLayout
in android.
Behavior attribute is used in direct child of CoordinatorLayout
. Once behavior is attached to any child view of CoordinatorLayout
then only that child view will have a specific behavior in your application. Behavior in CoordinatorLayout
's child view can be attached in three ways programmatically, in XML or via annotation.
Currently, there are several ways provided in this framework that allow it to work without needing to write your own custom animation code. These effects include:
<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">
This should be the top level element in your fragment or activity. The CoordinatorLayout
is a new type of element introduced in the Design Library – basically it "coordinates" behavior between its child views. The behavior is specified by attributes set in its child views that are specific to the CoordinatorLayout
.
AppBarLayout
<android.support.design.widget.AppBarLayout android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="200dp" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
Basically, an AppBarLayout
is a LinearLayout
with steroids, their children are placed vertically, with certain parameters the children can manage their behavior when the content is scrolled.
This is another new element from the Design Library that declares a layout for the Toolbar. AppBarLayout
is introduced from Lollipop onwards, but through Design Support Library you can add this feature for Android 2.1 or above devices. The children of this element will define the behavior of the elements within the Toolbar
.
AppBarLayout
is actually required to be children of CoordinatorLayout
, and are also required to have a sibling element that has the layout_behavior
attribute set to AppBarLayout.ScrollingViewBehavior
. Setting this attribute on the sibling element binds that element to the AppBarLayout
, which lets it know when it should scroll. We bind the AppBarLayout
to the NestedScrollView
, which is discussed farther below.
AppBarLayout
allows your Toolbar
and other views (like TabLayout
) to react to scroll events in a sibling view.
CollapsingToolbarLayout
<android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:contentScrim="?attr/colorPrimary">
This element is the child of the AppBarLayout
and declares the "collapsing" behavior we want. The children of this element will declare how the toolbar is collapsing. Toolbar
title will automatically appear larger when the layout is fully visible, then transition to its default size as it is collapsed.
This layout consists of:
app:layout_scrollFlags
. The scroll flags of this layout is typically set to scroll|exitUntilCollapsed
.app:collapsedTitleGravity
. Specifies the gravity of title in the container when collapsed.app:contentScrim
. This requires specifying a drawable or color value of the CollapsingToolbarLayouts
content when it has been scrolled sufficiently off screen eg. ?attr/colorPrimary
.app:scrimAnimationDuration
. Specifies the duration used for scrim visibility animations. This requires an integer value such as 100.The layout_scrollFlags attribute is a requirement of the CollapsingToolbarLayout
, because it needs to know how to scroll its children, depending on the scroll events it receives.
scroll
- it will be shown and hidden when the specified view is scrolled.enterAlways
- when we scroll up, the view appears immediately, instead of waiting until the scroll finishes.snap
- if the view remains semi-visible when scroll stops, it will be animated until it's completely shown or hidden.enterAlwaysCollapsed
– When set on a view, that view will not expand from the collapsed state until the downward scrolling motion reaches the limit of the list. If the minHeight
property is set, the view will appear during the initial scrolling motion but only until the minimum height is reached. It will then remain at that height and will not expand fully until the top of the list is reached. Note this option only works when used in conjunction with both the enterAlways
and scroll options. For example:app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed" android:minHeight="20dp"
exitUntilCollapsed
– When set, the view will collapse during an upward scrolling motion until the minHeight
threshold is met, at which point it will remain at that height until the scroll direction changes.Once app:layout_scrollFlags
added to CollapsingToolbarLayout
, the content view (either a NestedScrollView
or RecyclerView
) needs to have app:layout_behavior
tag.
ImageView
<ImageView android:id="@+id/ivParallax" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/city" android:scaleType="centerCrop" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7" />
Just a normal ImageView
, with a few exceptions: we are setting the layout_collapseMode
to parallax
, which causes the ImageView
to move as the user scrolls at a specific ratio. You can set this ratio with the (optional) layout_collapseParallaxMultiplier
setting.
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" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> </android.support.v7.widget.Toolbar>
Again, this is just a normal element you’ve seen before, except for the layout behavior set by the layout_collapseMode
property. For this element we are setting it to pin
which causes it to stick to the top when the user scrolls the view up.
NestedScrollView
The official widget to make a scroll layout in Android is ScrollView
. Basically, adding a ScrollView
inside ScrollView
can be difficult . Most of the times it won’t end well. You will end up adding few workarounds and a person maintaining your code will probably hate you for the rest of his life. Fortunately, with the appearance of Material Design technology, NestedScrollView
was released and this becomes much easier. NestedScrollView
supports acting as both a nested scrolling parent and child on both new and old versions of Android.
For example, NestedScrollView
can be used as a ListView
item, so with this design, each row can be scrolled.
<android.support.v4.widget.NestedScrollView android:id="@+id/nestedScrollView" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" app:layout_behavior="@string/appbar_scrolling_view_behavior">
This is the sibling element to the AppBarLayout
mentioned above. This is just a normal NestedScrollView
, with the exception of the layout_behavior
attribute set. As mentioned above, setting this attribute binds it to its sibling element, the AppBarLayout
. In another words, you should specify which view you want to observe the scroll from. It can be a NestedScrollView
, a RecyclerView
or any other view that implements NestedScrollingChild
.
Next, you have to add different method to make collapsing/animated/parallax header view and for listview. Following is the complete code snippet of java activity file.
public class MainActivity extends AppCompatActivity { Toolbar toolbar; RecyclerView rvItems; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); toolbar = (Toolbar) findViewById(R.id.toolbar); rvItems = (RecyclerView) findViewById(R.id.rvItems); setSupportActionBar(toolbar); getSupportActionBar().setHomeButtonEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true); RecyclerAdapter adapter = new RecyclerAdapter(this); rvItems.setAdapter(adapter); rvItems.setHasFixedSize(true); rvItems.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); } }
Let's define simple adapter for RecyclerView.
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.RecyclerViewHolder>{ Context context; LayoutInflater inflater; ArrayList<String> items = new ArrayList<>(); public class RecyclerViewHolder extends RecyclerView.ViewHolder { TextView tv1; public RecyclerViewHolder(View itemView) { super(itemView); tv1 = (TextView) itemView.findViewById(R.id.title); } } public RecyclerAdapter(Context context) { this.context = context; inflater = LayoutInflater.from(context); for (int i = 1; i < 20; i++) { items.add("City " + i); } } @Override public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = inflater.inflate(R.layout.rv_item, parent, false); RecyclerViewHolder viewHolder = new RecyclerViewHolder(v); return viewHolder; } @Override public void onBindViewHolder(RecyclerViewHolder holder, int position) { holder.tv1.setText(items.get(position)); } @Override public int getItemCount() { return items.size(); } }
And rv_item.xml layout for item of RecyclerView.
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/cardView" android:layout_width="match_parent" android:layout_height="40dp" android:layout_margin="4dp" android:background="#C5CAE9" android:foreground="?attr/selectableItemBackground" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:gravity="center_vertical" android:text="Title" android:textColor="@android:color/black" android:textAppearance="?attr/textAppearanceListItem" android:textSize="16sp" /> </android.support.v7.widget.CardView>
Now, run your application and scroll the screen, you will see that the header image is collapsing slowly and app title text will be shown at toolbar.
Result
To know whether the CollapsingToolbarLayout
is collapsed or expanded, we can add a listener addOnOffsetChangedListener
on the AppBarLayout
instance. Depending upon the if else conditions we show or hide the toolbar info option.
@Override protected void onCreate(Bundle savedInstanceState) { ... AppBarLayout barLayout = (AppBarLayout) findViewById(R.id.appBarLayout); barLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() { boolean isShow = false; int scrollRange = -1; @Override public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { if (scrollRange == -1) { scrollRange = appBarLayout.getTotalScrollRange(); } if (scrollRange + verticalOffset == 0) { isShow = true; showOption(R.id.action_info); } else if (isShow) { isShow = false; hideOption(R.id.action_info); } } }); } ... private void hideOption(int id) { MenuItem item = menu.findItem(id); item.setVisible(false); } private void showOption(int id) { MenuItem item = menu.findItem(id); item.setVisible(true); }
Useful links