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"
Related
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 am currently experimenting with CoordinatorLayout + AppbarLayout + CollapsingToolbarLayout in a way such that:
1) Scroll down the Appbar using "Toolbar" [ No Nested ScrollView / RecyclerView ].
2) The content below the appbar should move along with the appbar scrolling.
3) Multiple images kept under ViewPager.
4) The last item in the ViewPager would be an textview.
I have achieved 1) and 2) using the following layout :
<?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:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<android.support.design.widget.AppBarLayout
android:id="#+id/flexible.example.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/flexible.example.collapsing"
android:layout_width="match_parent"
android:layout_height="300dp"
app:expandedTitleMarginBottom="94dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:contentScrim="?colorPrimary"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:id="#+id/text_sample"
android:scrollbars="vertical"
android:scrollIndicators="right"
app:layout_collapseMode="parallax"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_gravity="center"
android:nestedScrollingEnabled="true"
/>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.v7.widget.Toolbar
android:id="#+id/ioexample.toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/PM01"
android:elevation="4dp"
app:layout_collapseMode="pin"
app:layout_anchor="#id/flexible.example.collapsing"
app:layout_anchorGravity="bottom"
app:theme="#style/ThemeOverlay.AppCompat.Light"
style="#style/ToolBarWithNavigationBack"
app:layout_scrollFlags="scroll|enterAlways|snap"
>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/recyclerviewcontainer"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
What I am trying to achieve now is to make the textview inside collpasingtoolbarlayout is to be scrollable (#4 above). Since my search till now has made me believe that the Appbar is handling all the touch events by itself, this doesn't seems to be easy. But since it is a requirement, I would be more than happy to have a guidance / pointers to help me complete this.
Can someone please let me know what and where to look for achieving this functionality.
After a lot of research and some more searching through SO, I got an idea what I need to do in order to achieve the desired effect:
1) Implement a custom behavior for appbarlayout :
public class AppBarLayoutCustomBehavior extends AppBarLayout.Behavior {
private boolean setIntercept = false;
private boolean lockAppBar = false;
DragCallback mDragCallback = new DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return !lockAppBar;
}
};
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
super.onInterceptTouchEvent(parent, child, ev);
return setIntercept;
}
public void setInterceptTouchEvent(boolean set) {
setIntercept = set;
}
public AppBarLayoutCustomBehavior() {
super();
setDragCallback(mDragCallback);
}
public AppBarLayoutCustomBehavior(Context ctx, AttributeSet attributeSet) {
super(ctx, attributeSet);
setDragCallback(mDragCallback);
}
public void lockAppBar() {
lockAppBar = true;
}
public void unlockAppBar() {
lockAppBar = false;
}
}
2) Use this custom behavior with app bar :
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appBarLayout.getLayoutParams();
final AppBarLayoutCustomBehavior mBehavior = new AppBarLayoutCustomBehavior();
lp.setBehavior(mBehavior);
/// use toolbar to enable/disable dragging on the appbar behavior. This way
/// out toolbar acts as a drag handle for the app bar.
Toolbar toolbar = (Toolbar) activity.findViewById(R.id.main_toolbar);
toolbar.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
mBehavior.setInterceptTouchEvent(true);
return true;
case MotionEvent.ACTION_CANCEL:
mBehavior.setInterceptTouchEvent(false);
return true;
}
return false;
}
});
3) Set movement method on the textview to make it scrollable
textView.setMovementMethod(ScrollingMovementMethod.getInstance());
I'm trying to get a fullscreen CollapsingToolbar but when I set match_parent to the height of AppBarLayout I'm not able to scroll the ImageView which is inside CollapsingToolbarLayout. I have to leave some space so that I can touch the "white" of the activity (in AppBarLayout I added android:layout-marginBottom:"16dp" ) and only then, after I touched it, I can scroll the ImageView otherwise I can't.
This happens everytime I run the app and touch the layout for the first time. So I have to touch the white first and then scroll the image.
Could you help me?
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="#+id/drawer">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp">
<ImageView
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"
android:contentDescription="#null"
android:src="#drawable/background" />
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin"
app:theme="#style/ToolbarTheme"
android:id="#+id/toolbar"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:id="#+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</FrameLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
<com.myapplication.ScrimInsetsFrameLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="304dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:insetForeground="#4000"
android:clickable="true"
android:background="#ffffff">
...
</com.myapplication.ScrimInsetsFrameLayout>
</android.support.v4.widget.DrawerLayout>
EDIT #PPartisan I've done what you said but here's what I got:
This isn't a nice solution, but it does work on my test device. It kick starts the scrolling process by explicitly assigning a touch listener to the AppBar that triggers a nested scroll.
First, create a custom class that extends NestedScrollView and add the following method so it look something like this:
public class CustomNestedScrollView extends NestedScrollView {
private int y;
public CustomNestedScrollView(Context context) {
super(context);
}
public CustomNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchHandlerScroll(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
y = (int) e.getY();
startNestedScroll(2);
break;
case MotionEvent.ACTION_MOVE:
int dY = y - ((int)e.getY());
dispatchNestedPreScroll(0, dY, null, null);
dispatchNestedScroll(0, 0, 0, dY, null);
break;
case MotionEvent.ACTION_UP:
stopNestedScroll();
break;
}
return true;
}
}
Then, inside your Activity class, assign a TouchListener to your AppBarLayout:
appBarLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return customNestedScrollView.dispatchHandlerScroll(event);
}
});
and remove it when the AppBar collapses fully for the first time:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Math.abs(appBarLayout.getY()) == appBarLayout.getTotalScrollRange()) {
appBarLayout.setOnTouchListener(null);
}
return super.dispatchTouchEvent(ev);
}
Edit
Take the following steps to get it up and running:
Replace the NestedScrollView in your xml(android.support.v4.widget.NestedScrollView) with the CustomNestedScrollView (which will take the form of com.something.somethingelse.CustomNestedScrollView, depending on where it is in your project).
Assign it to a variable in your Activity onCreate()(i.e. CustomScrollView customScrollView = (CustomScrollView) findViewById(R.id.custom_scroll_view_id);)
Set up the TouchListener on your appBarLayout as you have done in your edit. Now when you call dispatchHandlerScroll(), it will be on your customNestedScrollView instance.
dispatchTouchEvent() is a method you override in your Activity class, so it should be outside the TouchListener
So, for example:
public class MainActivity extends AppCompatActivity {
private AppBarLayout appBarLayout;
private CustomNestedScrollView customNestedScrollView;
//...
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
customNestedScrollView = (CustomNestedScrollView) findViewById(R.id.scroll);
appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
appBarLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return customNestedScrollView.dispatchHandlerScroll(event);
}
});
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Math.abs(appBarLayout.getY()) == appBarLayout.getTotalScrollRange()) {
appBarLayout.setOnTouchListener(null);
}
return super.dispatchTouchEvent(ev);
}
}
Hope that's cleared things up.
Try to add a tiny margin so the white space below will be almost invisible (you might also want to change white to accent color so the space will not be visible)
Another approach is to set the height of app bar layout dynamically by getting the height of the screen.
EDIT:
This might be a focus problem, try to add dummy layout to your main content, that will be focused automatically
<LinearLayout
android:layout_width="0px"
android:layout_height="0px"
android:focusable="true"
android:focusableInTouchMode="true" />
or even just add these attributes to your content layout
android:focusable="true"
android:focusableInTouchMode="true"
I'm trying to implement the CollapsingToolbarLayout with a custom view, but I'm unable to do it :
What I want to do (sorry I can't post images so it's on imgur) :
Expanded, the header is a profile screen with image and title
Not expanded (on scroll), the image and title will be on the toolbar
But everything I saw wasn't working as I expected
I'm new to this and lollipop animations so if someone could help me I'll be very grateful !
(I don't post sample code because I don't have something relevant to post)
My Solution
I had the same scenario to implement so I started with the dog example and made some changes for it to work exactly like you describe. My code can be found as a fork on that project, see https://github.com/hanscappelle/CoordinatorBehaviorExample
Most important changes are in the layout:
<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.support.design.widget.AppBarLayout
android:id="#+id/main.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/main.collapsing"
android:layout_width="match_parent"
android:layout_height="#dimen/expanded_toolbar_height"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
>
<FrameLayout
android:id="#+id/main.framelayout.title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
>
<LinearLayout
android:id="#+id/main.linearlayout.title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:orientation="vertical"
android:paddingBottom="#dimen/spacing_small"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="bottom|center_horizontal"
android:text="#string/tequila_name"
android:textColor="#android:color/white"
android:textSize="#dimen/textsize_xlarge"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="#dimen/spacing_xxsmall"
android:text="#string/tequila_tagline"
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"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="#dimen/spacing_xsmall"
android:padding="#dimen/spacing_normal"
android:text="#string/lorem"
android:textSize="#dimen/textsize_medium"
/>
</android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.Toolbar
android:id="#+id/main.toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/primary"
app:layout_anchor="#id/main.collapsing"
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_final_width"
android:layout_height="#dimen/image_final_width"
/>
<TextView
android:id="#+id/main.textview.title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:gravity="center_vertical"
android:text="#string/tequila_title"
android:textColor="#android:color/white"
android:textSize="#dimen/textsize_large"
/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="#dimen/image_width"
android:layout_height="#dimen/image_width"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="#dimen/spacing_normal"
android:src="#drawable/ninja"
app:border_color="#android:color/white"
app:border_width="#dimen/border_width"
app:finalHeight="#dimen/image_final_width"
app:finalXPosition="#dimen/spacing_small"
app:finalYPosition="#dimen/spacing_small"
app:finalToolbarHeight="?attr/actionBarSize"
app:layout_behavior="saulmm.myapplication.AvatarImageBehavior"
/>
</android.support.design.widget.CoordinatorLayout>
And in the AvatarImageBehaviour class that I optimised for only moving the avatar from the original position to the position configured in the attributes. So if you want the image to move from another location just move it within the layout. When you do so make sure the AppBarLayout is still a sibling of it or it won't be found in code.
package saulmm.myapplication;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import de.hdodenhof.circleimageview.CircleImageView;
public class AvatarImageBehavior extends CoordinatorLayout.Behavior<CircleImageView> {
// calculated from given layout
private int startXPositionImage;
private int startYPositionImage;
private int startHeight;
private int startToolbarHeight;
private boolean initialised = false;
private float amountOfToolbarToMove;
private float amountOfImageToReduce;
private float amountToMoveXPosition;
private float amountToMoveYPosition;
// user configured params
private float finalToolbarHeight, finalXPosition, finalYPosition, finalHeight;
public AvatarImageBehavior(
final Context context,
final AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
finalXPosition = a.getDimension(R.styleable.AvatarImageBehavior_finalXPosition, 0);
finalYPosition = a.getDimension(R.styleable.AvatarImageBehavior_finalYPosition, 0);
finalHeight = a.getDimension(R.styleable.AvatarImageBehavior_finalHeight, 0);
finalToolbarHeight = a.getDimension(R.styleable.AvatarImageBehavior_finalToolbarHeight, 0);
a.recycle();
}
}
#Override
public boolean layoutDependsOn(
final CoordinatorLayout parent,
final CircleImageView child,
final View dependency) {
return dependency instanceof AppBarLayout; // change if you want another sibling to depend on
}
#Override
public boolean onDependentViewChanged(
final CoordinatorLayout parent,
final CircleImageView child,
final View dependency) {
// make child (avatar) change in relation to dependency (toolbar) in both size and position, init with properties from layout
initProperties(child, dependency);
// calculate progress of movement of dependency
float currentToolbarHeight = startToolbarHeight + dependency.getY(); // current expanded height of toolbar
// don't go below configured min height for calculations (it does go passed the toolbar)
currentToolbarHeight = currentToolbarHeight < finalToolbarHeight ? finalToolbarHeight : currentToolbarHeight;
final float amountAlreadyMoved = startToolbarHeight - currentToolbarHeight;
final float progress = 100 * amountAlreadyMoved / amountOfToolbarToMove; // how much % of expand we reached
// update image size
final float heightToSubtract = progress * amountOfImageToReduce / 100;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
lp.width = (int) (startHeight - heightToSubtract);
lp.height = (int) (startHeight - heightToSubtract);
child.setLayoutParams(lp);
// update image position
final float distanceXToSubtract = progress * amountToMoveXPosition / 100;
final float distanceYToSubtract = progress * amountToMoveYPosition / 100;
float newXPosition = startXPositionImage - distanceXToSubtract;
//newXPosition = newXPosition < endXPosition ? endXPosition : newXPosition; // don't go passed end position
child.setX(newXPosition);
child.setY(startYPositionImage - distanceYToSubtract);
return true;
}
private void initProperties(
final CircleImageView child,
final View dependency) {
if (!initialised) {
// form initial layout
startHeight = child.getHeight();
startXPositionImage = (int) child.getX();
startYPositionImage = (int) child.getY();
startToolbarHeight = dependency.getHeight();
// some calculated fields
amountOfToolbarToMove = startToolbarHeight - finalToolbarHeight;
amountOfImageToReduce = startHeight - finalHeight;
amountToMoveXPosition = startXPositionImage - finalXPosition;
amountToMoveYPosition = startYPositionImage - finalYPosition;
initialised = true;
}
}
}
Sources
Most common example is the one with the dog listed at https://github.com/saulmm/CoordinatorBehaviorExample . It's a good example but indeed has the toolbar in the middle of the expanded view with a backdrop image that also moves. All that was removed in my example.
Another explanation is found at http://www.devexchanges.info/2016/03/android-tip-custom-coordinatorlayout.html but since that cloud/sea backdrop image referenced there is also found in the dog example one is clearly build on top of the other.
I also found this SO question with a bounty awarded but couldn't really find out what the final solution was Add icon with title in CollapsingToolbarLayout
And finally this should be a working library that does the work. I've checked it out but the initial image wasn't centered and I rather worked on the dog example that I had looked at before. See https://github.com/datalink747/CollapsingAvatarToolbar
More to read
http://saulmm.github.io/mastering-coordinator
http://www.androidauthority.com/using-coordinatorlayout-android-apps-703720/
https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html
https://guides.codepath.com/android/handling-scrolls-with-coordinatorlayout