Flexible space on the Toolbar
This topic describes how to create flexible space on the Toolbar, which are implemented in the following examples.
- FlexibleSpaceToolbarScrollViewActivity
- FlexibleSpaceToolbarWebViewActivity
I originally tried implementing this pattern (only the title animation):
Flexible space with image
Using ScrollView
Layout with ScrollView
Basic structure
<FrameLayout>
<ObservableScrollView android:id="@+id/scroll">
<FrameLayout android:id="@+id/body">
<TextView/>
</FrameLayout>
</ObservableScrollView>
<View android:id="@+id/flexible_space"/>
<Toolbar android:id="@+id/toolbar"/>
<RelativeLayout android:paddingLeft="@dimen/toolbar_margin_start">
<TextView android:id="@+id/title"/>
<LinearLayout android:orientation="vertical">
<View android:layout_height="?attr/actionBarSize"/>
<View android:layout_height="@dimen/flexible_space_height"/>
</LinearLayout>
</RelativeLayout>
</FrameLayout>
The root FrameLayout
is used for moving children separately.
The second FrameLayout
(@id/body
) inside the ScrollView is the main content,
and you can put any views as you like.
This time, we'll add just a TextView
.
View
(@id/flexible_space
) is for a "flexible space" which has a opaque background.
This view will be translated vertically on scrolling.
Toolbar
is a normal Toolbar, but this Toolbar will not have "title".
The next RelativeLayout
and its children are a little tricky.
The TextView
(@id/title
) is the real title view,
and other views (LinearLayout
, View
) are padding.
In this "flexible space" pattern, TextView
's text should move and its font size should change,
so it needs additional space.
We'll achieve these animations by animate TextView
itself, so paddings should be outside the TextView
.
To confirm other attributes,
please see res/layout/activity_flexiblespacetoolbarscrollview.xml
in the example app.
Initialization
At first, set the Toolbar as the ActionBar and show "homeAsUp" button.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flexiblespacetoolbarscrollview);
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
And get the Activity's title and set it to the TextView
which has the ID @id/title
.
mTitleView = (TextView) findViewById(R.id.title);
mTitleView.setText(getTitle());
setTitle(null);
And initialize other views and fields.
private View mFlexibleSpaceView;
private View mToolbarView;
private TextView mTitleView;
private int mFlexibleSpaceHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
// Codes that are already explained above are omitted
mFlexibleSpaceView = findViewById(R.id.flexible_space);
mToolbarView = findViewById(R.id.toolbar);
final ObservableScrollView scrollView = (ObservableScrollView) findViewById(R.id.scroll);
scrollView.setScrollViewCallbacks(this);
mFlexibleSpaceHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_height);
int flexibleSpaceAndToolbarHeight = mFlexibleSpaceHeight + getActionBarSize();
findViewById(R.id.body).setPadding(0, flexibleSpaceAndToolbarHeight, 0, 0);
mFlexibleSpaceView.getLayoutParams().height = flexibleSpaceAndToolbarHeight;
}
You should also add implements ObservableScrollViewCallbacks
to the Activity
and implement those methods as always.
Animation
We use onScrollChanged()
to create animation.
We must write following codes:
- Translate the flexible space view
- Translate and scale the title view
Translate the flexible space view
This is easy, just translate it using scrollY
:
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY);
}
Scale the title view
How do you change the size of the font?
At first I tried just changing the size of the font, but it didn't work. It should be scaled.
The scale should change from 1
to 1.x
. You can change .x
to fit your app.
In this case, I used the height of the flexible space and the height of the Toolbar. This calculates the maximum .x
:
float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight();
The scale (we call .x
part as "scale" from here) should change between 0 to maxScale
, so it can be written as follows.
// scrollY should be limited.
int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight);
// When scrollY is 0, scale equals to maxScale.
// When scrollY reaches to mFlexibleSpaceHeight, scale will be 0.
float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight;
When scaling the view, we need to set the center point of scaling.
You can handle this by using pivotX
and pivotY
, and we should set them to (0, 0)
like this image:
We will set pivotX
and pivotY
first, and then change the scale:
// Pivot the title view to (0, 0)
ViewHelper.setPivotX(mTitleView, 0);
ViewHelper.setPivotY(mTitleView, 0);
// Scale the title view
ViewHelper.setScaleX(mTitleView, 1 + scale);
ViewHelper.setScaleY(mTitleView, 1 + scale);
Translate the title view
And about translationY
, this is a little complicated.
Let's see the following picture.
The minimum translationY
is obviously 0, and we want to know
the maximum translationY
.
As we can see in the picture, the maximum translationY
can be calculated with ht1 + hf - ht2
, so we can write like this:
int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale));
And we should vary this value using scrollY
.
scrollY
should be limited and it's already calculated as adjustedY
:
int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight);
ViewHelper.setTranslationY(mTitleView, titleTranslationY);
Finally, we've finished translation and scaling.
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY);
// Calculate scale
int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight);
float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight();
float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight;
// Pivot the title view to (0, 0)
ViewHelper.setPivotX(mTitleView, 0);
ViewHelper.setPivotY(mTitleView, 0);
// Scale the title view
ViewHelper.setScaleX(mTitleView, 1 + scale);
ViewHelper.setScaleY(mTitleView, 1 + scale);
// Translate the title view
int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale));
int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight);
ViewHelper.setTranslationY(mTitleView, titleTranslationY);
}
Adjust the initial state of the title
It's almost finished, but maybe you will notice that when the screen is launched, the title is located at the top of the screen. It should be located at the bottom of the header view area and have larger font.
This is because onScrollChanged()
is not called.
You can fix that by calling onScrollChanged()
just after the views are laied out.
And you can handle this "laid out" event by using ViewTreeObserver#addOnGlobalLayoutListener()
.
@Override
protected void onCreate(Bundle savedInstanceState) {
// Other initialization codes are omitted
ViewTreeObserver vto = mTitleView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
updateFlexibleSpaceText(scrollView.getCurrentScrollY());
}
});
}
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
updateFlexibleSpaceText(scrollY);
}
private void updateFlexibleSpaceText(scrollY) {
// Original animation codes are omitted
}
You can replace the following ViewTreeObserver
codes
ViewTreeObserver vto = mTitleView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
updateFlexibleSpaceText(scrollView.getCurrentScrollY());
}
});
to this:
ScrollUtils.addOnGlobalLayoutListener(mTitleView, new Runnable() {
@Override
public void run() {
updateFlexibleSpaceText(scrollView.getCurrentScrollY());
}
});
That's all!