I want to achieve a Google+ profile page-like effect (check the attached GIF)
What I have achieved
So far I have achieved:When you scrolling to bottom (drag up), the header image will shrink and the SlidingTabs will pin to below the ActionBar. Just like the first half of the GIF
However I was unable to do the reverse: when the RecyclerView reaches its first child, it stops sending the onScrolled event and header view cannot be expanded.
Related Files
Heres my layout file:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:animateLayoutChanges="true"
android:fitsSystemWindows="false"
tools:context=".ProfileActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/profile_header_background_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="#+id/header"
android:layout_alignParentTop="true"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="#drawable/ic_launcher" />
<LinearLayout
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar_actionbar_layout" />
<include
android:id="#+id/profile_bio_view"
layout="#layout/profile_header" />
</LinearLayout>
<SlidingTabLayout
android:id="#+id/profile_sliding_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/header" />
<android.support.v4.view.ViewPager
android:id="#+id/profile_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_below="#+id/profile_sliding_tags" />
</RelativeLayout>
<include layout="#layout/drawer_navigation_layout" />
</android.support.v4.widget.DrawerLayout>
Inside the ViewPager's RecyclerView I did this:
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
Bus.post(new ProfileScrolledEvent(recyclerView.getChildAt(0).getTop(), state, dy));
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
state = newState;
// intercept the touch event if scroll state=0 (IDLE)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
Bus.post(new ProfileScrolledEvent(recyclerView.getChildAt(0).getTop(), state, 0));
recyclerView.getParent().getParent().requestDisallowInterceptTouchEvent(true);
} else {
recyclerView.getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
});
So that when the RecyclerView is scrolling I can post the pixels scrolled to the activity, and in my activity:
#Subscribe
public void onProfileScrolledEvent(ProfileScrolledEvent event) {
// FIXME: can scroll with shrink header, but the header will not expand as expected
int scrollState = event.getScrollState();
int pixels = event.getScrolledPixels();
int recyclerViewTop = event.getRecyclerViewTop();
if (pixels == 0 && scrollState == 0) {
//TODO intercept the touch event
shouldIntercept = true;
} else {
shouldIntercept = false;
scrollProfile(recyclerViewTop);
}
LogUtil.debug("Bus event. top: " + recyclerViewTop + " state: " + scrollState + " pixels: " + pixels);
}
private void scrollProfile(int pixels) {
ViewGroup.LayoutParams headerParams = profileBioView.getLayoutParams();
headerParams.height = profileBioView.getHeight() + pixels / 2;
currentHeaderHeight = headerParams.height;
LogUtil.debug("currentHeaderHeight: " + currentHeaderHeight +
"\n profileBioView.getHeight: " + profileBioView.getHeight() +
"\n background.getHeight: " + headerBackground.getHeight());
ViewGroup.LayoutParams imageParams = headerBackground.getLayoutParams();
imageParams.height = headerParams.height;
if (headerParams.height <= 0) {
headerBackground.setVisibility(View.INVISIBLE);
} else {
headerBackground.setVisibility(View.VISIBLE);
slidingTabLayout.setTranslationY(pixels / 2);
if (headerParams.height > origHeaderHeight) {
headerParams.height = origHeaderHeight;
imageParams.height = origHeaderHeight;
}
profileBioView.setLayoutParams(headerParams);
headerBackground.setLayoutParams(imageParams);
}
}
Question
The problem is, when the RecyclerView reaches its first child, the onScrolled event stops, the ScrollState becomes IDLE. At this point, I want to notify the parent activity to intercept the TouchEvent and carry on moving the header view. However when I tried to register a GestureListener inside the activity, it simply not get called.
So is there any better way to do this? Or is it possible to intercept a child's TouchEvent under a certain condition?
FYI: I used Otto Bus to post messages, check here: http://square.github.io/otto/
Related
I have a View and a RecyclerView housed in a LinearLayout. What I want to achieve is something like this:
https://material.google.com/patterns/scrolling-techniques.html#scrolling-techniques-behavior
Basically when I scroll the RecyclerView up, the View collapses. It expands if I scroll the RecyclerView down.
I've tried various methods but the animation stutters if the finger jerks around a scroll position. It only animates well if the finger does a deliberate scroll movement in one direction. How do I do this correctly?
You have to use Coordinator Layout with the CollapsingToolbarLayout
<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:clipToPadding="false">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="210dp"
android:stateListAnimator="#animator/appbar_always_elevated" //I put this here because I want to have shadow when is open, but you have to create the xml file.
android:background="#color/WHITE">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:collapsedTitleTextAppearance="#style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
app:expandedTitleMarginStart="72dp"
app:expandedTitleTextAppearance="#style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
app:layout_scrollFlags="scroll|exitUntilCollapsed"> //HERE you should take a look what you want your collapse bar do.
<Here you put the content for you collapse bar, like a ImageView>
<android.support.v7.widget.Toolbar //This is the size of your fixed bar when you collapse, even here you can put a back button, for example
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/main_home_list_swipe"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" >
<android.support.v7.widget.RecyclerView
android:id="#+id/main_home_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.design.widget.CoordinatorLayout >
Obs: the comments will give errors if you put on a xml file. Is on propose so you will remember to read hahahah
Try this:-
I also want this kind of animation on custom view and i have achieved it this way.
public class TestActivity extends AppCompatActivity {
private static final int HIDE_THRESHOLD = 20;
//this is you custom layout it is any thing.
LinearLayout customLayout;
private int scrolledDistance = 0;
private boolean controlsVisible = true;
private RecyclerView recyclerView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.your_layout);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
scrolledDistance = dy;
if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) {
hideViews();
controlsVisible = false;
} else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {
showViews();
controlsVisible = true;
}
}
});
}
private void hideViews() {
customLayout.animate().translationY(-customLayout.getHeight()).setInterpolator(new AccelerateInterpolator(2));
}
private void showViews() {
customLayout.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2));
}
}
Edit - 1
for ScrollView try this listener
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) {
hideViews();
controlsVisible = false;
scrolledDistance = 0;
} else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {
showViews();
controlsVisible = true;
scrolledDistance = 0;
}
}
});
Hope it also helps you...
To achieve toolbar to expand and collapse smoothly you can apply translate animation or use CoordinatorLayout with AppBarLayout and Toolbar.
Animation : First you have to detect scroll up and scroll down on your RecyclerView. To do so you can set “setOnScrollListener” on your RecyclerView. Once you have both scroll up and scroll down, simply apply animation.
Code:
rvHomeList.setOnScrollListener(new RecyclerView.OnScrollListener() {
int verticalOffset;
boolean scrollingUp;
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (scrollingUp) {
Log.e("onScrollStateChanged", "UP");
if (verticalOffset > llTop.getHeight()) {
toolbarAnimateHide();
}
} else {
Log.e("onScrollStateChanged", "down");
if (llTop.getTranslationY() < llTop.getHeight() * -0.6 && verticalOffset > llTop.getHeight()) {
toolbarAnimateHide();
} else {
toolbarAnimateShow(verticalOffset);
}
}
}
}
#Override
public final void onScrolled(RecyclerView recyclerView, int dx, int dy) {
verticalOffset += dy;
scrollingUp = dy > 0;
int toolbarYOffset = (int) (dy - llTop.getTranslationY());
llTop.animate().cancel();
if (scrollingUp) {
Log.e("onScrolled", "UP");
if (toolbarYOffset < llTop.getHeight()) {
llTop.setTranslationY(-toolbarYOffset);
} else {
llTop.setTranslationY(-llTop.getHeight());
}
} else {
Log.e("onScrolled", "down");
if (toolbarYOffset < 0) {
llTop.setTranslationY(0);
} else {
llTop.setTranslationY(-toolbarYOffset);
}
}
}
});
Animation Methods:
private void toolbarAnimateShow(final int verticalOffset) {
if (!isShowing) {
isShowing = true;
llTop.animate()
.translationY(0)
.setInterpolator(new LinearInterpolator())
.setDuration(180)
.setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationStart(Animator animation) {
llTop.setVisibility(View.VISIBLE);
isShowing = false;
}
});
}
}
private void toolbarAnimateHide() {
if (!isHidding) {
isHidding = true;
llTop.animate()
.translationY(-llTop.getHeight())
.setInterpolator(new LinearInterpolator())
.setDuration(180)
.setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
llTop.setVisibility(View.GONE);
isHidding = false;
}
});
}
}
CoordinatorLayout with AppBarLayout and Toolbar: By using coordinatorLayout with appBarLayout and toolbar, and set the scroll flag used within the attribute app:layout_scrollFlags to achieve the scroll effect. It must be enabled for any scroll effects to take into effect. This flag must be enabled along with enterAlways, enterAlwaysCollapsed, exitUntilCollapsed, or snap.
enterAlways: The view will become visible when scrolling up. This flag is useful in cases when scrolling from the bottom of a list and wanting to expose the Toolbar as soon as scrolling up takes place.
enterAlwaysCollapsed: Normally, when only enterAlways is used, the Toolbar will continue to expand as you scroll down.Assuming enterAlways is declared and you have specified a minHeight, you can also specify enterAlwaysCollapsed. When this setting is used, your view will only appear at this minimum height. Only when scrolling reaches to the top will the view expand to its full height
exitUntilCollapsed: When the scroll flag is set, scrolling down will normally cause the entire content to move.By specifying a minHeight and exitUntilCollapsed, the minimum height of the Toolbar will be reached before the rest of the content begins to scroll and exit from the screen
snap: Using this option will determine what to do when a view only has been partially reduced. If scrolling ends and the view size has been reduced to less than 50% of its original, then this view to return to its original size. If the size is greater than 50% of its sized, it will disappear completely.
Code:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/llBase"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:orientation="vertical">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<android.support.design.widget.AppBarLayout
android:id="#+id/appbarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="#dimen/_40sdp"
android:gravity="center"
android:theme="#style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways">
<include
layout="#layout/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<RelativeLayout
android:id="#+id/rlMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
You need to use CoordinatorLayout to achieve what you want.
You could find all needed information in this tutorial.
I have this layout (code is at the bottom) which contains a CollapsingToolBarLayout at the top and a NestedScrollView at the bottom.
When I scroll up, the collapsing toolbar will start to collapse, then the scroll view will scroll up with the collapsing toolbar at first and then goes behind the collapsed tool bar.
I want to have some animations (image slides left when scrolling up and slides back when scrolling down) in the collapsing toolbar. The issue now is: sometimes, when I scroll up, the image doesn't slide left. When it slid left, and I scroll down, it doesn't slide back.
I trigger these animations through onOffsetChanged for the AppBarLayout and OnTouchListener for the NestedScrollView.
// People image slide left when user scrolls up on the scroll view
mScrollView.setOnTouchListener(scrollViewTouchListener);
// People image slide back when app bar is almost expanded
mAppBar.addOnOffsetChangedListener(appBarOffsetChangedListener);
// OnOffsetChangedListener for the AppBarLayout
private AppBarLayout.OnOffsetChangedListener appBarOffsetChangedListener = new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
// If the app bar is almost expanded and people image slided left, make it slide back
if ((offset > -20 || offset == 0) && mPeopleSlidedLeft) {
mPeopleImage.animate().setDuration(animationTime)
.translationX(originalPeoplePosition[0]);
mPeopleSlidedLeft = false;
}
}
};
// Touch listener for the scroll view
private View.OnTouchListener scrollViewTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
float y = event.getY();
if (event.getAction() == MotionEvent.ACTION_MOVE) {
float dy = y - mPreviousY;
// if user scrolls up and people image hasn't slided left,
if (dy < -1 && mPeopleSlidedLeft == false) {
DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
int xDest = dm.widthPixels / 2;
xDest += mPeopleImage.getMeasuredWidth() / 2;
mPeopleImage.animate().setDuration(animationTime)
.translationX(originalPeoplePosition[0] - xDest);
}
}
mPeopleSlidedLeft = true;
mPreviousY = y;
return false;
}
};
Just note that the scrollview's setOnScrollChangeListener won't work as it's not triggered when the toolbar is collapsing.
A simplified version of the layout is below:
<?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"
xmlns:tools="http://schemas.android.com/tools"
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:fitsSystemWindows="true"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:elevation="0dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="#dimen/collapsing_toolbar_margin"
android:fitsSystemWindows="true"
android:minHeight="120dp"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
android:id="#+id/backdrop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:fitsSystemWindows="true"
android:orientation="vertical"
app:layout_collapseMode="parallax">
</LinearLayout>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="#dimen/toolbar_height"
android:layout_gravity="center_horizontal"
app:contentInsetEnd="16dp"
app:contentInsetStart="16dp"
app:elevation="0dp"
app:layout_collapseMode="pin"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="#color/white"
android:orientation="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<android.support.v4.widget.NestedScrollView
android:id="#+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
<include
layout="#layout/notification"
android:layout_width="match_parent"
android:layout_height="#dimen/active_inactive_time_height"
android:layout_gravity="bottom"
app:layout_anchorGravity="bottom|right"
android:layout_marginBottom="#dimen/bottom_navigation_bar_offset" />
</android.support.design.widget.CoordinatorLayout>
Can someone please have a look? I will really appreciate it!
I'm not sure why you're using a touch listener on the NestedScrollView, if I understand correctly you want there to be 2 states:
Toolbar is expanded and people image is visible
Toolbar is collapsed and people image is hidden
And the transition between these 2 states should be to slide the people image off the left of the screen?
This should be achievable with the AppBarLayout.OnOffsetChangedListener alone, and you can use the change of offset to "animate" the view moving instead of setting up trigger points which can result in a much smoother implementation.
Something like:
private AppBarLayout.OnOffsetChangedListener appBarOffsetChangedListener = new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
float fraction = ((float) Math.abs(verticalOffset)) / appBarLayout.getTotalScrollRange();
int peopleRange = mPeopleImage.getRight();
mPeopleImage.setTranslationX(fraction * peopleRange * -1);
}
};
I've added some configuration of the timing and speed of the slid. In this case it waits until the header is collapsed 25% before sliding and the image moves left twice as fast. You could play with these numbers to get what you're looking for.
private AppBarLayout.OnOffsetChangedListener appBarOffsetChangedListener = new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
float fraction = ((float) Math.abs(verticalOffset)) / appBarLayout.getTotalScrollRange();
int peopleRange = mPeopleImage.getRight();
float delay = 0.25f;
float speed = 2f;
fraction = Math.max(fraction - delay, 0f) * speed;
mPeopleImage.setTranslationX(fraction * peopleRange * -1);
}
};
I am creating an app with a recyclerview. And above the RV I have an image, which should get smaller, when i scroll. This works, but the RV scrolls also. I want that first the image gets smaller and then the recyclerview starts scrolling. But how can I do this? Here is my XML:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/b"
android:id="#+id/test_photo"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
app:layout_anchor="#+id/test_photo"
android:background="#color/colorPrimary"
app:layout_anchorGravity="bottom|start">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/colorWhite"
android:textSize="30sp"
android:text="username"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/user_view_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
And this is the code to resize the image:
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
float state = 0.0f;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, final int dy) {
Log.e("Y",Integer.toString(dy));
state+=dy;
LinearLayout img = (LinearLayout) findViewById(R.id.test_photo);
Log.e("STATE", Float.toString(state));
if(state >= 500){
img.getLayoutParams().height = minWidth;
img.getLayoutParams().width = minWidth;
img.requestLayout();
}
if(state <= 0){
img.getLayoutParams().height = imgHeight;
img.getLayoutParams().width = imgHeight;
img.requestLayout();
}
if(state > 0 && state < 500){
//up
img.getLayoutParams().height = (int)(imgHeight - ((float)(imgHeight-minWidth)/500)*state);
img.getLayoutParams().width = (int)(imgHeight - ((float)(imgHeight-minWidth)/500)*state);
img.requestLayout();
}
}
});
Thanks for the help!
EDIT:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="320dp"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<com.obware.alifsto.HelpClasses.CollapsingImageLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:minHeight="108dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="#+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="#drawable/sunset" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<ImageView
android:id="#+id/avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="96dp"
android:src="#drawable/logo_blau_weiss"
android:transitionName="#string/transition_userview_image"/>
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="48dp"
android:text="Title"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textStyle="bold" />
<TextView
android:id="#+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="24dp"
android:text="Subtitle "
android:transitionName="#string/transition_userview_username"
android:textAppearance="?android:attr/textAppearanceMedium" />
</com.obware.alifsto.HelpClasses.CollapsingImageLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/user_interface_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
The way you want to do this is with CoordinatorLayout and AppBarLayout and use all that Material Design scrolling goodness.
So essentially what you do is create a specialized layout similar to CollapsingToolbarLayout. For my demo, I used code from that class as inspiration to get my collapsing image layout to work.
What makes it work is adding the layout as a direct child of AppBarLayout, then creating an AppBarLayout.OnOffsetChangeListener and registering it with the AppBarLayout. When you do this, you will get notifications when the user scrolls and the layout is scrolled up.
Another big part of this is setting a minimum height. AppBarLayout uses the minimum height to determine when to stop scrolling your layout, leaving you with a collapsed layout area.
Here's a code excerpt:
class OnOffsetChangedListener implements AppBarLayout.OnOffsetChangedListener {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
final int scrollRange = appBarLayout.getTotalScrollRange();
float offsetFactor = (float) (-verticalOffset) / (float) scrollRange;
Log.d(TAG, "onOffsetChanged(), offsetFactor = " + offsetFactor);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
if (child instanceof Toolbar) {
if (getHeight() - insetTop + verticalOffset >= child.getHeight()) {
offsetHelper.setTopAndBottomOffset(-verticalOffset); // pin
}
}
if (child.getId() == R.id.background) {
int offset = Math.round(-verticalOffset * .5F);
offsetHelper.setTopAndBottomOffset(offset); // parallax
}
if (child.getId() == R.id.avatar) {
float scaleFactor = 1F - offsetFactor * .5F ;
child.setScaleX(scaleFactor);
child.setScaleY(scaleFactor);
int topOffset = (int) ((mImageTopCollapsed - mImageTopExpanded) * offsetFactor) - verticalOffset;
int leftOffset = (int) ((mImageLeftCollapsed - mImageLeftExpanded) * offsetFactor);
child.setPivotX(0);
child.setPivotY(0);
offsetHelper.setTopAndBottomOffset(topOffset);
offsetHelper.setLeftAndRightOffset(leftOffset);
}
if (child.getId() == R.id.title) {
int topOffset = (int) ((mTitleTopCollapsed - mTitleTopExpanded) * offsetFactor) - verticalOffset;
int leftOffset = (int) ((mTitleLeftCollapsed - mTitleLeftExpanded) * offsetFactor);
offsetHelper.setTopAndBottomOffset(topOffset);
offsetHelper.setLeftAndRightOffset(leftOffset);
Log.d(TAG, "onOffsetChanged(), offsetting title top = " + topOffset + ", left = " + leftOffset);
Log.d(TAG, "onOffsetChanged(), offsetting title mTitleLeftCollapsed = " + mTitleLeftCollapsed + ", mTitleLeftExpanded = " + mTitleLeftExpanded);
}
if (child.getId() == R.id.subtitle) {
int topOffset = (int) ((mSubtitleTopCollapsed - mSubtitleTopExpanded) * offsetFactor) - verticalOffset;
int leftOffset = (int) ((mSubtitleLeftCollapsed - mSubtitleLeftExpanded) * offsetFactor);
offsetHelper.setTopAndBottomOffset(topOffset);
offsetHelper.setLeftAndRightOffset(leftOffset);
}
}
}
}
The lines child.setScaleX() and child.setScaleY() are the code that actually changes the size of the image.
Demo app is on GitHub at https://github.com/klarson2/Collapsing-Image. Enjoy.
EDIT: After adding a TabLayout I realized one mistake I made in my layout, which was to make the AppBarLayout a fixed height, then make the custom collapsing component height be match_parent. This makes it so you can't see the TabLayout that is added to the app bar. I changed the layout so that AppBarLayout height was wrap_content and the custom collapsing component had the fixed height. This makes it possible to add additional components like a TabLayout to the AppBarLayout. This has been corrected in the latest revision on GitHub.
With the following code I resize the image according to the scrolling. So that you can see it collapsed in the AppBar.
Play with the values of the duration of the animation and the value of the scaling when the AppBar is collapsed.
In my case I have the Toolbar as transparent and I manage the colors of the AppBar elements at run times.
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
/**
* Collapsed
*/
if (Math.abs(verticalOffset) == appBarLayout.getTotalScrollRange()) {
myImage.animate().scaleX((float)0.4).setDuration(3000);
myImage.animate().scaleY((float)0.4).setDuration(3000);
myImage.animate().alpha(1).setDuration(0);
/**
* Expanded
*/
} else if (verticalOffset == 0) {
myImage.animate().scaleX((float)1).setDuration(100);
myImage.animate().scaleY((float)1).setDuration(100);
myImage.animate().alpha(1).setDuration(0);
/**
* Somewhere in between
*/
} else {
final int scrollRange = appBarLayout.getTotalScrollRange();
float offsetFactor = (float) (-verticalOffset) / (float) scrollRange;
float scaleFactor = 1F - offsetFactor * .5F;
myImage.animate().scaleX(scaleFactor);
myImage.animate().scaleY(scaleFactor);
}
}
PD: This works regardless of whether the image exceeds the limits of the AppBar, as if the image were a floating button.
GL
Sources
Listener
Conditionals
Some methods
I created recyclerview and header layout, when I scroll up recyclerview, the header layout should be disappeared according to the scrolling offset of recyclerview.
My layout xml file is:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:showIn="#layout/activity_main"
tools:context=".MainActivity"
android:id="#+id/main_container">
<TextView android:layout_width="match_parent"
android:layout_height="50dp"
android:id="#+id/header"
android:text="Mao Minh Tri"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/header"/>
</RelativeLayout>
and the implemented code when scrolling recyclerview
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (originalHeight == 0) {
originalHeight = myrecyclerView.getHeight();
}
ViewGroup.LayoutParams lp = myrecyclerView.getLayoutParams();
calculateLocationAndProductStickerOffset();
textview.setTranslationY(-offset);
mainView.setTranslationY(-offset);
if ((offset < textview.getHeight() && dy > 0) || (offset > 0 && dy < 0)) {
offset += dy;
}
System.out.println("original height --> " + originalHeight);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
};
myrecyclerView.addOnScrollListener(scrollListener);
}
private void calculateLocationAndProductStickerOffset() {
if (offset > textview.getHeight()) {
offset = textview.getHeight();
} else if (offset < 0) {
offset = 0;
}
}
And my result:
Here the best way to use a header with recycleview without using RecyclerView.OnScrollListener
I think you don't need to do all of that.. you just need to change your parent layout from RelativeLayout to CoordinatorLayout which supports doing that by default.
Here's a good tutorial to do that:
https://guides.codepath.com/android/Handling-Scrolls-with-CoordinatorLayout
I have a problem with smooth scrolling in CoordinatorLayout in my app.
I trying to achieve this:
http://wstaw.org/m/2015/10/02/google-scroll.gif
but my best result is:
http://wstaw.org/m/2015/10/02/my-scroll.gif
<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/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="true">
<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">
<ImageView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="#dimen/detail_image_height"
android:background="?attr/colorPrimary"
android:fitsSystemWindows="true"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
<RelativeLayout
android:id="#+id/relativeLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="#dimen/activity_horizontal_margin"
android:layout_marginRight="#dimen/activity_horizontal_margin"
android:background="?attr/colorPrimary"
android:minHeight="80dp">
(...)
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
(...)
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
What am I doing wrong? Thanks in advance.
I was not able to completely fix this behavior, but I did find something that helped with scrolling up. It's based on this answer in an SO thread about flinging with CoordinatorLayout. First, create a class that extends AppBarLayout.Behavior.
/**
* This "fixes" the weird scroll behavior with CoordinatorLayouts with NestedScrollViews when scrolling up.
* This is based on https://stackoverflow.com/questions/30923889/flinging-with-recyclerview-appbarlayout
*/
#SuppressWarnings("unused")
public class CoordinatorFlingBehavior extends AppBarLayout.Behavior {
private static final String TAG = "CoordinatorFling";
public CoordinatorFlingBehavior() {
}
public CoordinatorFlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
// Passing false for consumed will make the AppBarLayout fling everything and pull down the expandable stuff
if (target instanceof NestedScrollView && velocityY < 0) {
final NestedScrollView scrollView = (NestedScrollView) target;
int scrollY = scrollView.getScrollY();
// Note the ! in front
consumed = !(scrollY < target.getContext().getResources().getDimensionPixelSize(R.dimen.flingThreshold) // if below threshold, fling
|| isScrollingUpFast(scrollY, velocityY)); // Or if moving quickly, fling
Log.v(TAG, "onNestedFling: scrollY = " + scrollY + ", velocityY = " + velocityY + ", flinging = " + !consumed);
}
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
/**
* This uses the log of the velocity because constants make it too easy to uncouple the CoordinatorLayout - the AppBarLayout and the NestedScrollView - when scrollPosition is small.
*
* #param scrollPosition - of the NestedScrollView target
* #param velocityY - Y velocity. Should be negative, because scrolling up is negative. However, a positive value won't crash this method.
* #return true if scrolling up fast
*/
private boolean isScrollingUpFast(int scrollPosition, float velocityY) {
float positiveVelocityY = Math.abs(velocityY);
double calculation = scrollPosition * Math.log(positiveVelocityY);
return positiveVelocityY > calculation;
}
}
Then, add the following line to your AppBarLayout's xml block (replacing companyname and packages with whatever you use):
app:layout_behavior="com.companyname.packages.CoordinatorFlingBehavior"