CoordinatorLayout
in Material Design style to make some special effects with Toolbar/Action Bar like auto hide Toolbar when scroll, expanding/collapsing it, style with a "header" image,...It would be remiss if I did not mention to custom the Behavior
of CoordinatorLayout
. It isn't as difficult as it may seem, to begin we must take into account two core elements: child and dependency:The child is the view that enhances behavior, dependency who will serve as a trigger to interact with the child element. In this example, the child is the
ImageView
and the dependency is the Toolbar
, in that way, if the Toolbar
moves, the ImageView
will move too.Please see this DEMO VIDEO, you can realize this effect appeared in a lot of applications in Google Play:
Custom the Behavior
CoordinatorLayout.Behavior<T>
, been T
is your child class, for example: ImageView
. These are methods we must override:layoutDependsOn()
: called every time that something happens in the layout, what we must do to return true once we identify the dependency, in the example, this method is automatically fired when the user scrolls (because theToolbar
will move), in that way we can make our child sight react accordingly.onDependentViewChanged()
: Called whenlayoutDependsOn()
return true. Here is where you must to implement our animations, translations or movements always related with the provided dependency.
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, SimpleDraweeView child, View dependency) {
return dependency instanceof Toolbar;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, SimpleDraweeView child, View dependency) {
maybeInitProperties(child, dependency);
final int maxScrollDistance = (int) (mStartToolbarPosition - getStatusBarHeight());
float expandedPercentageFactor = dependency.getY() / maxScrollDistance;
float distanceYToSubtract = ((mStartYPosition - mFinalYPosition)
* (1f - expandedPercentageFactor)) + (child.getHeight()/2);
float distanceXToSubtract = ((mStartXPosition - mFinalXPosition)
* (1f - expandedPercentageFactor)) + (child.getWidth()/2);
float heightToSubtract = ((mStartHeight - finalHeight) * (1f - expandedPercentageFactor));
child.setY(mStartYPosition - distanceYToSubtract);
child.setX(mStartXPosition - distanceXToSubtract);
int proportionalAvatarSize = (int) (mAvatarMaxSize * (expandedPercentageFactor));
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
lp.width = (int) (mStartHeight - heightToSubtract);
lp.height = (int) (mStartHeight - heightToSubtract);
child.setLayoutParams(lp);
return true;
}
Calculating the Toolbar
height to placing the child view, we have full code of the custom Behavior
class:
ImageBehavior.java
package info.devexchanges.customcoordiantorbehavior;
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.View;
import com.facebook.drawee.view.SimpleDraweeView;
public class ImageBehavior extends CoordinatorLayout.Behavior {
private final static float MIN_AVATAR_PERCENTAGE_SIZE = 0.3f;
private final static int EXTRA_FINAL_AVATAR_PADDING = 80;
private final static String TAG = "behavior";
private final Context mContext;
private float mAvatarMaxSize;
private float mFinalLeftAvatarPadding;
private float mStartPosition;
private int mStartXPosition;
private float mStartToolbarPosition;
public ImageBehavior(Context context, AttributeSet attrs) {
mContext = context;
init();
mFinalLeftAvatarPadding = context.getResources().getDimension(R.dimen.activity_horizontal_margin);
}
private void init() {
bindDimensions();
}
private void bindDimensions() {
mAvatarMaxSize = mContext.getResources().getDimension(R.dimen.image_width);
}
private int mStartYPosition;
private int mFinalYPosition;
private int finalHeight;
private int mStartHeight;
private int mFinalXPosition;
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, SimpleDraweeView child, View dependency) {
return dependency instanceof Toolbar;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, SimpleDraweeView child, View dependency) {
maybeInitProperties(child, dependency);
final int maxScrollDistance = (int) (mStartToolbarPosition - getStatusBarHeight());
float expandedPercentageFactor = dependency.getY() / maxScrollDistance;
float distanceYToSubtract = ((mStartYPosition - mFinalYPosition)
* (1f - expandedPercentageFactor)) + (child.getHeight()/2);
float distanceXToSubtract = ((mStartXPosition - mFinalXPosition)
* (1f - expandedPercentageFactor)) + (child.getWidth()/2);
float heightToSubtract = ((mStartHeight - finalHeight) * (1f - expandedPercentageFactor));
child.setY(mStartYPosition - distanceYToSubtract);
child.setX(mStartXPosition - distanceXToSubtract);
int proportionalAvatarSize = (int) (mAvatarMaxSize * (expandedPercentageFactor));
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
lp.width = (int) (mStartHeight - heightToSubtract);
lp.height = (int) (mStartHeight - heightToSubtract);
child.setLayoutParams(lp);
return true;
}
@SuppressLint("PrivateResource")
private void maybeInitProperties(SimpleDraweeView child, View dependency) {
if (mStartYPosition == 0)
mStartYPosition = (int) (dependency.getY());
if (mFinalYPosition == 0)
mFinalYPosition = (dependency.getHeight() /2);
if (mStartHeight == 0)
mStartHeight = child.getHeight();
if (finalHeight == 0)
finalHeight = mContext.getResources().getDimensionPixelOffset(R.dimen.image_small_width);
if (mStartXPosition == 0)
mStartXPosition = (int) (child.getX() + (child.getWidth() / 2));
if (mFinalXPosition == 0)
mFinalXPosition = mContext.getResources().getDimensionPixelOffset(R.dimen.abc_action_bar_content_inset_material) + (finalHeight / 2);
if (mStartToolbarPosition == 0)
mStartToolbarPosition = dependency.getY() + (dependency.getHeight()/2);
}
public int getStatusBarHeight() {
int result = 0;
int resourceId = mContext.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = mContext.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
}
Coding for the UI
Activity
to "test" this custom Behavior
. Declaring it's layout with CoordinatorLayout
as root, AppBarLayout
and CollapsingToolbarLayout
:
activity_main.xml
In programatically code, the <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="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:orientation="vertical"
tools:ignore="HardcodedText">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
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/collapsing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/imageview_placeholder"
android:layout_width="match_parent"
android:layout_height="300dp"
android:contentDescription="@string/app_name"
android:scaleType="centerCrop"
android:tint="#11000000"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.9" />
<FrameLayout
android:id="@+id/framelayout_title"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@color/colorPrimary"
android:orientation="vertical"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.3">
<LinearLayout
android:id="@+id/linearlayout_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="bottom|center"
android:text="Grumpy Cat"
android:textColor="@android:color/white"
android:textSize="25sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:text="The famous meme"
android:textColor="@android:color/white" />
</LinearLayout>
</FrameLayout>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:layout_marginTop="@dimen/activity_horizontal_margin"
app:behavior_overlapTop="30dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="8dp"
app:contentPadding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="8dp"
android:text="@string/lorem"
android:textSize="18sp" />
</android.support.v7.widget.CardView>
</android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:layout_anchor="@id/framelayout_title"
app:theme="@style/ThemeOverlay.AppCompat.Dark"
app:title="">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<Space
android:layout_width="@dimen/image_small_width"
android:layout_height="@dimen/image_small_width" />
<TextView
android:id="@+id/textview_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:gravity="center_vertical"
android:text="Grumpy Cat information"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>
</android.support.v7.widget.Toolbar>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="@dimen/image_width"
android:layout_height="@dimen/image_width"
android:layout_gravity="center"
app:layout_behavior="info.devexchanges.customcoordiantorbehavior.ImageBehavior"
fresco:roundAsCircle="true" />
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
Activity
should implements AppBarLayout.OnOffsetChangedListener
to defining for a callback to be invoked when an AppBarLayout
's vertical offset changes:
MainActivity.java
package info.devexchanges.customcoordiantorbehavior;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.view.SimpleDraweeView;
public class MainActivity extends AppCompatActivity implements AppBarLayout.OnOffsetChangedListener {
private static final float PERCENTAGE_TO_SHOW_TITLE_AT_TOOLBAR = 0.9f;
private static final float PERCENTAGE_TO_HIDE_TITLE_DETAILS = 0.3f;
private static final int ALPHA_ANIMATIONS_DURATION = 200;
final Uri imageUri = Uri.parse("http://i.imgur.com/VIlcLfg.jpg");
private boolean mIsTheTitleVisible = false;
private boolean mIsTheTitleContainerVisible = true;
private AppBarLayout appbar;
private CollapsingToolbarLayout collapsing;
private ImageView coverImage;
private FrameLayout framelayoutTitle;
private LinearLayout linearlayoutTitle;
private Toolbar toolbar;
private TextView textviewTitle;
private SimpleDraweeView avatar;
/**
* Find the Views in the layout
* Auto-created on 2016-03-03 11:32:38 by Android Layout Finder
* (http://www.buzzingandroid.com/tools/android-layout-finder)
*/
private void findViews() {
appbar = (AppBarLayout)findViewById( R.id.appbar );
collapsing = (CollapsingToolbarLayout)findViewById( R.id.collapsing );
coverImage = (ImageView)findViewById( R.id.imageview_placeholder );
framelayoutTitle = (FrameLayout)findViewById( R.id.framelayout_title );
linearlayoutTitle = (LinearLayout)findViewById( R.id.linearlayout_title );
toolbar = (Toolbar)findViewById( R.id.toolbar );
textviewTitle = (TextView)findViewById( R.id.textview_title );
avatar = (SimpleDraweeView)findViewById(R.id.avatar);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Fresco.initialize(this);
setContentView(R.layout.activity_main);
findViews();
toolbar.setTitle("");
appbar.addOnOffsetChangedListener(this);
setSupportActionBar(toolbar);
startAlphaAnimation(textviewTitle, 0, View.INVISIBLE);
//set avatar and cover
avatar.setImageURI(imageUri);
coverImage.setImageResource(R.drawable.cover);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
int maxScroll = appBarLayout.getTotalScrollRange();
float percentage = (float) Math.abs(offset) / (float) maxScroll;
handleAlphaOnTitle(percentage);
handleToolbarTitleVisibility(percentage);
}
private void handleToolbarTitleVisibility(float percentage) {
if (percentage >= PERCENTAGE_TO_SHOW_TITLE_AT_TOOLBAR) {
if(!mIsTheTitleVisible) {
startAlphaAnimation(textviewTitle, ALPHA_ANIMATIONS_DURATION, View.VISIBLE);
mIsTheTitleVisible = true;
}
} else {
if (mIsTheTitleVisible) {
startAlphaAnimation(textviewTitle, ALPHA_ANIMATIONS_DURATION, View.INVISIBLE);
mIsTheTitleVisible = false;
}
}
}
private void handleAlphaOnTitle(float percentage) {
if (percentage >= PERCENTAGE_TO_HIDE_TITLE_DETAILS) {
if(mIsTheTitleContainerVisible) {
startAlphaAnimation(linearlayoutTitle, ALPHA_ANIMATIONS_DURATION, View.INVISIBLE);
mIsTheTitleContainerVisible = false;
}
} else {
if (!mIsTheTitleContainerVisible) {
startAlphaAnimation(linearlayoutTitle, ALPHA_ANIMATIONS_DURATION, View.VISIBLE);
mIsTheTitleContainerVisible = true;
}
}
}
public static void startAlphaAnimation (View v, long duration, int visibility) {
AlphaAnimation alphaAnimation = (visibility == View.VISIBLE)
? new AlphaAnimation(0f, 1f)
: new AlphaAnimation(1f, 0f);
alphaAnimation.setDuration(duration);
alphaAnimation.setFillAfter(true);
v.startAnimation(alphaAnimation);
}
}
Some necessary files
res/enu/main.xml
The strings resource:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/setting"
android:title="@string/app_name"
app:showAsAction="never"/>
<item android:id="@+id/menu_share"
android:icon="@drawable/like"
app:showAsAction="ifRoom"
android:title="@string/app_name" />
</menu>
strings.xml
Styles resource, make sure that you use
<resources>
<string name="app_name">Custom Coordiantor Behavior</string>
<string name="lorem">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula sem a malesuada rhoncus. Pellentesque ut dolor a dui porttitor porta lacinia non libero. Nunc volutpat arcu quis quam convallis molestie. Etiam ac tristique sem, id commodo justo. Phasellus congue tincidunt lectus, at dignissim ligula maximus eu. Quisque interdum nunc eget tellus bibendum suscipit. Phasellus feugiat ultricies posuere. Nullam porta accumsan velit, ut rutrum massa fermentum eu. Nunc ac bibendum nunc. Mauris eu ultricies ipsum. Ut id dolor dui. Pellentesque dictum dui vel tempus maximus. Vivamus non nisi quis libero scelerisque pretium. Ut eu tristique justo. Sed pellentesque placerat quam, ut ultricies turpis feugiat a. Aliquam a volutpat risus.
</string>
</resources>
Theme.AppCompat.Light.NoActionBar
for this example:
styles.xml
The dimensions resource:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
dimens.xml
Running app, you will see this output, like Facebook profile page:<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="image_width">110dp</dimen>
<dimen name="image_small_width">32dp</dimen>
</resources>
Note: In this example, the view class use in custom Behavior is
SimpleDraweeView
, a sub class of ImageView
in Facebook Fresco library. So, in order to use it, you must put this dependency to app/build.gradle
:
compile 'com.facebook.fresco:fresco:0.9.0'
Because of getting "avatar" from an URL
, so you must provide Internet permission in AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
Conclusions
Reference to official docs:
- CoordinatorLayout.
- Toolbar.
- CollapsingToolbarLayout.
- AppBarLayout.