Flexible space with image
This topic describes how to create flexible space with image, which are implemented in the following examples.
- FlexibleSpaceWithImageListViewActivity
- FlexibleSpaceWithImageRecyclerViewActivity
- FlexibleSpaceWithImageScrollViewActivity
First, please check the "Flexible space on the Toolbar" tutorial, if you haven't.
Using ScrollView
Layout with ScrollView
Basic structure
<FrameLayout>
<ImageView android:id="@+id/image"/>
<View android:id="@+id/overlay"/>
<ObservableScrollView android:id="@+id/scroll">
<LinearLayout android:orientation="vertical">
<View/>
<TextView/>
</LinearLayout>
</ObservableScrollView>
<LinearLayout android:orientation="vertical">
<TextView android:id="@+id/title"/>
<View/>
</LinearLayout>
<FloatingActionButton android:id="@+id/fab"/>
</FrameLayout>
The root FrameLayout
is used for moving children separately.
ImageView
(@id/image
) is the image that will be translated with parallax effect.
View
(@id/overlay
) is a overlay view as the name suggests.
If you try this Activity in the demo app, you can see the image is fading in and out.
This view overlaps with the image and its opacity is changed by scroll position.
LinearLayout
and its chlidren are the real title views.
You would have read the former tutorial, so I will not explain it so much.
FloatingActionButton
is a widget from the simple and awesome FloatingActionButton library.
But this is optional, so you can remove it if you are not going to place any buttons.
I added it just because I think it's a very symbolic widget of the Material Design
and some of you might be interested in it.
To confirm other attributes,
please see res/layout/activity_flexiblespacewithimagescrollview.xml
in the example app.
Initialization
Most of the codes are easy and not related to this pattern.
Just write the following initialization codes:
Copy the title to the title view (TextView
) and set null to the original title:
mTitleView.setText(getTitle());
setTitle(null);
Get the dimension values and save them to fields (to simplify animation codes):
mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height);
mFlexibleSpaceShowFabOffset = getResources().getDimensionPixelSize(R.dimen.flexible_space_show_fab_offset);
mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard);
mActionBarSize = getActionBarSize();
Get the views which has ID to fields (to simplify animation codes), and initialize them if necessary:
mImageView = findViewById(R.id.image);
mOverlayView = findViewById(R.id.overlay);
mScrollView = (ObservableScrollView) findViewById(R.id.scroll);
mScrollView.setScrollViewCallbacks(this);
mTitleView = (TextView) findViewById(R.id.title);
mFab = findViewById(R.id.fab);
Although this is not so related to the scroll animation,
you should scale the floating action button (FAB) to 0 in onCreate()
,
because we'd like to hide it at the beginning and gradually show (scale) it
by scrolling.
ViewHelper.setScaleX(mFab, 0);
ViewHelper.setScaleY(mFab, 0);
You should also add implements ObservableScrollViewCallbacks
to the Activity
and implement those methods as always.
Animation
We use onScrollChanged()
to create animation.
We'll write the following codes:
- Translate the overlay view and the image view
- Change the alpha of the overlay view
- Translate and scale the title view
- Translate the FAB
- Show/hide the FAB
Let's see one by one.
Translate the overlay view and the image view
As we implemented in the former tutorials,
to move ImageView
which is outside the ScrollView,
we must use -scrollY
and divide it by 2 to create "parallax" effect.
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
float flexibleRange = mFlexibleSpaceImageHeight - mActionBarSize;
int minOverlayTransitionY = mActionBarSize - mOverlayView.getHeight();
ViewHelper.setTranslationY(mOverlayView, ScrollUtils.getFloat(-scrollY, minOverlayTransitionY, 0));
ViewHelper.setTranslationY(mImageView, ScrollUtils.getFloat(-scrollY / 2, minOverlayTransitionY, 0));
Although we want to move the overlay view with the image,
we don't have to make the scroll speed of the overlay to the same as the image view.
So translate mOverlayView
to -scrollY
(not -scrollY / 2
).
Change the alpha of the overlay view
Calculating the alpha value is easy, just convert the scrollY
to range between 0 and 1.
To do this, we divide scrollY
by flexibleRange
(which we assigned above),
and limit the value range from 0 to 1 by using ScrollUtils.getFloat()
.
ViewHelper.setAlpha(mOverlayView, ScrollUtils.getFloat((float) scrollY / flexibleRange, 0, 1));
Translate and scale the title view
This is almost the same as the "Flexible space on the Toolbar" pattern. The differences are how to calculate the scale and the translationY.
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
// Codes that are already explained above are omitted
float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA);
ViewHelper.setPivotX(mTitleView, 0);
ViewHelper.setPivotY(mTitleView, 0);
ViewHelper.setScaleX(mTitleView, scale);
ViewHelper.setScaleY(mTitleView, scale);
int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale);
int titleTranslationY = maxTitleTranslationY - scrollY;
ViewHelper.setTranslationY(mTitleView, titleTranslationY);
Translate the FAB
Translating the FAB is actually not related to this topic, but I'll explain for your reference.
The basic idea is to change the translationY
property of the FAB,
but on pre-Honeycomb devices, this doesn't work when you use setOnClickListener
.
To fix the issue, we'll set the margins of the FrameLayout and lay it out again by calling requestLayout()
.
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
// Codes that are already explained above are omitted
int maxFabTranslationY = mFlexibleSpaceImageHeight - mFab.getHeight() / 2;
float fabTranslationY = ScrollUtils.getFloat(
-scrollY + mFlexibleSpaceImageHeight - mFab.getHeight() / 2,
mActionBarSize - mFab.getHeight() / 2,
maxFabTranslationY);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// On pre-honeycomb, ViewHelper.setTranslationX/Y does not set margin,
// which causes FAB's OnClickListener not working.
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFab.getLayoutParams();
lp.leftMargin = mOverlayView.getWidth() - mFabMargin - mFab.getWidth();
lp.topMargin = (int) fabTranslationY;
mFab.requestLayout();
} else {
ViewHelper.setTranslationX(mFab, mOverlayView.getWidth() - mFabMargin - mFab.getWidth());
ViewHelper.setTranslationY(mFab, fabTranslationY);
}
The expression - mFab.getHeight() / 2
in the calculation of maxFabTranslationY
means
that the half of the FAB overlaps to the image.
And about the fabTranslationY
calculation,
you might think that the expression mActionBarSize - mFab.getHeight() / 2
for the min value
is meaningless, but this is required when you scroll the view fast.
Because if it scrolls faster than the FAB scaling to 0, it looks as if it just moved away.
Show/hide the FAB
Showing or hiding the FAB is easy. If the translationY of the FAB exceeds the threshold, then hide it. Otherwise, show it.
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
// Codes that are already explained above are omitted
if (fabTranslationY < mFlexibleSpaceShowFabOffset) {
hideFab();
} else {
showFab();
}
}
hideFab()
and showFab()
methods can be implemented like this:
private boolean mFabIsShown;
private void showFab() {
if (!mFabIsShown) {
ViewPropertyAnimator.animate(mFab).cancel();
ViewPropertyAnimator.animate(mFab).scaleX(1).scaleY(1).setDuration(200).start();
mFabIsShown = true;
}
}
private void hideFab() {
if (mFabIsShown) {
ViewPropertyAnimator.animate(mFab).cancel();
ViewPropertyAnimator.animate(mFab).scaleX(0).scaleY(0).setDuration(200).start();
mFabIsShown = false;
}
}
We need a state variable to indicate whether the FAB is shown or not.
That's all.