I am trying to achieve a similar behavior to that of Telegram, on the settings page, that is, there is a CircleImage that when scrolling up goes to the left of the Topbar title, and when scrolling down goes to the middle of the expanded AppBarLayout.
I was basing my work on this example:
https://github.com/saulmm/CoordinatorBehaviorExample
But in this case the original coder is recreating the Topbar twice. I dont want to do that, the default behavior of the topbar is what I need and also I want to take advantage of the hamburger menu and the options menu that come out of the box.
This is my view hierarchy:
DrawerLayout
|
|---CoordinatorLayout
|--AppBarLayout
| |-CollapsingToolbarLayout
| |-ImageView (backdrop image)
| |-Toolbar
|--NestedScrollView
|--ImageView (circleimage avatar)
As you can see I cannot make the Toolbar layout a sibling of my CircleImage so I cannot bind them together on the layoutDependsOn method. I tried binding to the AppBarLayout basing my code off the one on the github repo but to be honest I cannot make much sense of what's happening in the original code.
My behavior was implemented in much the same manner as Saul's. The biggest difference is that I like to put a non-visible view like a Space where I wanted the circle image to end up, then use that view's bounds to determine how to move & size the circle image.
public class CollapsingImageBehavior extends CoordinatorLayout.Behavior<View> {
private final static int X = 0;
private final static int Y = 1;
private final static int WIDTH = 2;
private final static int HEIGHT = 3;
private int mTargetId;
private int[] mView;
private int[] mTarget;
public CollapsingImageBehavior() {
}
public CollapsingImageBehavior(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CollapsingImageBehavior);
mTargetId = a.getResourceId(R.styleable.CollapsingImageBehavior_collapsedTarget, 0);
a.recycle();
}
if (mTargetId == 0) {
throw new IllegalStateException("collapsedTarget attribute not specified on view for behavior");
}
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
setup(parent, child);
AppBarLayout appBarLayout = (AppBarLayout) dependency;
int range = appBarLayout.getTotalScrollRange();
float factor = -appBarLayout.getY() / range;
int left = mView[X] + (int) (factor * (mTarget[X] - mView[X]));
int top = mView[Y] + (int) (factor * (mTarget[Y] - mView[Y]));
int width = mView[WIDTH] + (int) (factor * (mTarget[WIDTH] - mView[WIDTH]));
int height = mView[HEIGHT] + (int) (factor * (mTarget[HEIGHT] - mView[HEIGHT]));
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
lp.width = width;
lp.height = height;
child.setLayoutParams(lp);
child.setX(left);
child.setY(top);
return true;
}
private void setup(CoordinatorLayout parent, View child) {
if (mView != null) return;
mView = new int[4];
mTarget = new int[4];
mView[X] = (int) child.getX();
mView[Y] = (int) child.getY();
mView[WIDTH] = child.getWidth();
mView[HEIGHT] = child.getHeight();
View target = parent.findViewById(mTargetId);
if (target == null) {
throw new IllegalStateException("target view not found");
}
mTarget[WIDTH] += target.getWidth();
mTarget[HEIGHT] += target.getHeight();
View view = target;
while (view != parent) {
mTarget[X] += (int) view.getX();
mTarget[Y] += (int) view.getY();
view = (View) view.getParent();
}
}
}
And here's the layout. One important thing I found out is that the circle image view needed to have an elevation set so that it would lay out atop the toolbar in collapsed mode, otherwise it would be behind the toolbar and not shown.
<?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:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.krislarson.customcoordinatorlayoutbehavior.ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="280dp"
android:minHeight="108dp"
android:fitsSystemWindows="true"
app:title="Abby"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleGravity="center_horizontal"
app:expandedTitleMarginTop="140dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="#+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#drawable/sunset"
app:layout_collapseMode="parallax"
android:scaleType="centerCrop"/>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay">
<Space
android:id="#+id/circle_collapsed_target"
android:layout_width="40dp"
android:layout_height="40dp"/>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_scrolling"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="#+id/circle_image_view"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="#drawable/abby"
android:layout_marginTop="220dp"
android:layout_gravity="top|center_horizontal"
android:elevation="8dp"
app:border_color="#android:color/black"
app:border_width="2dp"
app:collapsedTarget="#id/circle_collapsed_target"
app:layout_behavior="com.krislarson.customcoordinatorlayoutbehavior.CollapsingImageBehavior"/>
</android.support.design.widget.CoordinatorLayout>
You can see the entire demo project at https://github.com/klarson2/CustomCoordinatorLayoutBehavior
One possibility would be to create a custom view for your ToolBar and hide the red dot in the ToolBar if it is expanded and show an ImageView with the red dot instead (which is hidden when the toolbar is collapsed).
You can see how to add a custom view to a ToolBar at this answer: https://stackoverflow.com/a/27859966/5052976
After doing this just create a ImageView that is visible when the ToolBar is expanded.
final CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsingToolbarLayout);
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appBarLayout);
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
boolean isShow = false;
int scrollRange = -1;
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (scrollRange == -1) {
scrollRange = appBarLayout.getTotalScrollRange();
}
if (scrollRange + verticalOffset == 0) {
//show toolbar dot and hide imageview dot
isShow = true;
} else if(isShow) {
//hide toolbar dot and show imageview dot
isShow = false;
}
}
});
Unfortunately I can't test this right now but I think it should work ;-)
Related
I need this type of behavior to implement.Image should be scroll and set into center with text like wtsapp. but in wtsapp it set into left alignment, i need to set into center. how can i achieve this?
after scrolled image will show like that with text in toolbar.(mentioned)
1. Behavior for CoordinatorLayout and AppBarLayout
public class AvatarImageBehavior extends CoordinatorLayout.Behavior<ImageView> {
// 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;
private boolean onlyVerticalMove;
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);
onlyVerticalMove = a.getBoolean(R.styleable.AvatarImageBehavior_onlyVerticalMove, false);
a.recycle();
}
}
#Override
public boolean layoutDependsOn(#NotNull final CoordinatorLayout parent, #NotNull final ImageView child, #NotNull final View dependency) {
return dependency instanceof AppBarLayout; // change if you want another sibling to depend on
}
#Override
public boolean onDependentViewChanged(#NotNull final CoordinatorLayout parent, #NotNull final ImageView child, #NotNull 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 = Math.max(currentToolbarHeight, finalToolbarHeight);
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
if (!onlyVerticalMove) {
child.setX(newXPosition);
}
child.setY(startYPositionImage - distanceYToSubtract);
return true;
}
private void initProperties(
final ImageView 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;
}
}
}
```java
public class AppBarScrollWatcher implements AppBarLayout.OnOffsetChangedListener {
private int scrollRange = -1;
private OffsetListener listener;
public AppBarScrollWatcher(OffsetListener listener) {
this.listener = listener;
}
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (scrollRange == -1) {
scrollRange = appBarLayout.getTotalScrollRange();
}
int appbarHeight = scrollRange + verticalOffset;
float alpha = (float) appbarHeight / scrollRange;
if (alpha < 0) {
alpha = 0;
}
float alphaZeroOnCollapsed = shrinkAlpha(alpha);
float alphaZeroOnExpanded = Math.abs(alphaZeroOnCollapsed - 1);
int argbZeroOnExpanded = (int) Math.abs((alphaZeroOnCollapsed * 255) - 255);
int argbZeroOnCollapsed = (int) Math.abs(alphaZeroOnCollapsed * 255);
listener.onAppBarExpanding(alphaZeroOnExpanded <= 0, alphaZeroOnCollapsed <= 0, argbZeroOnExpanded, argbZeroOnCollapsed, alphaZeroOnCollapsed, alphaZeroOnExpanded);
}
private float shrinkAlpha(float alpha) {
NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault());
formatter.setMaximumFractionDigits(2);
formatter.setMinimumFractionDigits(2);
formatter.setRoundingMode(RoundingMode.HALF_DOWN);
return Float.parseFloat(formatter.format(alpha));
}
public interface OffsetListener {
void onAppBarExpanding(boolean expanded, boolean collapsed, int argbZeroOnExpanded, int argbZeroOnCollapsed, float alphaZeroOnCollapsed, float alphaZeroOnExpanded);
}
}
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AvatarImageBehavior">
<attr name="finalXPosition" format="dimension" />
<attr name="finalYPosition" format="dimension" />
<attr name="finalHeight" format="dimension" />
<attr name="finalToolbarHeight" format="dimension" />
<attr name="onlyVerticalMove" format="boolean" />
</declare-styleable>
</resources>
2. Implementation in Activity/Fragment
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="#color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<LinearLayout
android:id="#+id/header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_orange_light"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="160dp"
android:paddingEnd="24dp"
android:paddingBottom="56dp">
</LinearLayout>
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:layout_gravity="bottom"
app:contentInsetStart="0dp"
app:layout_collapseMode="pin"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:titleMarginStart="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="#+id/still_photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:contentDescription="#string/app_name"
android:scaleType="fitCenter"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_ph_person_male_80dp" />
<ImageView
android:id="#+id/ic_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.7"
android:clickable="true"
android:contentDescription="#string/app_name"
android:focusable="true"
android:padding="8dp"
android:tint="#android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_more_vert_black_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="#+id/v_sections"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_overlapTop="24dp"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<View
android:layout_width="match_parent"
android:layout_height="1000dp" />
</androidx.cardview.widget.CardView>
</androidx.core.widget.NestedScrollView>
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/moving_photo"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="64dp"
android:contentDescription="#string/app_name"
android:scaleType="fitCenter"
app:finalHeight="48dp"
app:finalToolbarHeight="?android:attr/actionBarSize"
app:finalYPosition="4dp"
app:layout_behavior=".custom.AvatarImageBehavior"
app:onlyVerticalMove="true"
app:srcCompat="#drawable/ic_ph_person_male_80dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
private lateinit var appBarScrollListener: AppBarScrollWatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launcher)
setupAppBar()
}
private fun setupAppBar() {
appBarScrollListener =
AppBarScrollWatcher(AppBarScrollWatcher.OffsetListener { _, collapsed, _, _, _, _ ->
still_photo.visibility = if (collapsed) View.VISIBLE else View.INVISIBLE
})
app_bar.addOnOffsetChangedListener(appBarScrollListener)
}
override fun onDestroy() {
app_bar.removeOnOffsetChangedListener(appBarScrollListener)
super.onDestroy()
}
Note that you should put two ImageView in the layout.
AppCompatImageView directly inside the CoordinatorLayout so that we can
use CoordinatorLayout.Behavior on it, it would be the moving photo.
The important prop here is app:onlyVerticalMove="true", that make
your moving photo scrolled vertically. I made the default value to
false, it will move the photo to the start point of CoordinatorLayout
(top left).
Put another ImageView inside the Appbar layout as the final photo displayed
in the Appbar. Init this with invisible state, then use AppBarLayout behavior to show the photo when the collapsing toolbar is being collapsed.
If you want to exclude Toolbar from moving elements, just remove android:layout_gravity="bottom"
I am trying to implement auto-hiding feature for my toolbar using CoordinateLayout. But I also want to tweaks it a bit, so that the hide/show only works after I scroll past my imageview.
I am currently able to "turn on/off" the hide/show availability for the toolbar via AppBarLayout.LayoutParams. So right now it won't hide the toolbar if I scroll within the imageview's height and will hide if I scroll pass the imageview. The only problem is, if I scroll the layout very fast and instantly pull my finger before it pass the imageview's height the toolbar won't hide. In my opinion it is because the scrolling listener's function is called before the parameter of the toolbar's show/hide is changed to enable.
Here's the code:
ArticleActivity.java
public class ArticleActivity extends AppCompatActivity {
private Toolbar toolbar;
private ImageView imageView;
private NestedScrollView scrollView;
private Matrix matrix;
private int imageHeight = 0;
private float scale = 0;
private boolean hideable = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_article);
imageView = (ImageView) findViewById(R.id.imageView);
scrollView = (NestedScrollView) findViewById(R.id.scrollView);
toolbar = (Toolbar) findViewById(R.id.toolbar);
matrix = new Matrix();
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
scale = (float) displayMetrics.widthPixels / ((float) imageView.getDrawable().getIntrinsicWidth());
matrix.postScale(scale, scale);
imageView.setImageMatrix(matrix);
if(imageView.getViewTreeObserver().isAlive()) {
imageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
imageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
imageHeight = imageView.getHeight();
}
});
}
if(scrollView.getViewTreeObserver().isAlive()) {
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
int scrollY = scrollView.getScrollY();
if(scrollY <= imageHeight) {
matrix.setTranslate(0, scrollY / 4);
matrix.postScale(scale, scale);
imageView.setImageMatrix(matrix);
if(hideable) {
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(0);
toolbar.setLayoutParams(params);
hideable = false;
}
} else {
if(!hideable) {
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
// 1 = scroll, 4 = enterAlways, 16 = snap
params.setScrollFlags(21);
toolbar.setLayoutParams(params);
hideable = true;
}
}
}
});
}
}}
activity_article.xml:
<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/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="matrix"
android:src="#drawable/download"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/imageView"
android:text="What is Lorem Ipsum?"/>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
What should I do to fix this? Or is there another way to achieve this? Thank you
Using BottomSheetBehavior from the google design library, it looks like the default behavior is for the bottom sheet to "cover" other views in the same CoordinatorLayout as it expands. I can anchor something like a FAB (or other view with an appropriately defined CoordinatorLayout.Behavior) to the top of the sheet and have it slide up as the sheet expands, which is nice, but what I want is to have a view "collapse" as the bottom sheet expands, showing a parallax effect.
This effect in Google Maps is similar to what I'm looking for; it starts as a parallax effect, and then switches back to just having the bottom sheet "cover" the map once a certain scroll position is reached:
One thing I tried (though I suspected from the start it wouldn't work), was setting the upper view's height programmatically in the onSlide call of my BottomSheetBehavior.BottomSheetCallback. This was somewhat successful, but the movement wasn't nearly as smooth as in Google Maps.
If anyone has an idea how the effect is accomplished I would appreciate it a lot!
After a bit more experimenting/research I realized from this post
How to make custom CoordinatorLayout.Behavior with parallax scrolling effect for google MapView? that a big part of my problem was not understanding the parallax effect, which translates views rather than shrinking them. Once I realized that, it was trivial to create a custom behavior that would apply the parallax to my main view when the bottom sheet expanded:
public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V>{
public CollapseBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
if (isBottomSheet(dependency)) {
BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior());
int peekHeight = behavior.getPeekHeight();
// The default peek height is -1, which
// gets resolved to a 16:9 ratio with the parent
final int actualPeek = peekHeight >= 0 ? peekHeight : (int) (((parent.getHeight() * 1.0) / (16.0)) * 9.0);
if (dependency.getTop() >= actualPeek) {
// Only perform translations when the
// view is between "hidden" and "collapsed" states
final int dy = dependency.getTop() - parent.getHeight();
ViewCompat.setTranslationY(child, dy/2);
return true;
}
}
return false;
}
private static boolean isBottomSheet(#NonNull View view) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
return ((CoordinatorLayout.LayoutParams) lp)
.getBehavior() instanceof BottomSheetBehavior;
}
return false;
}
}
Then in my layout XML, I set the app:layout_behavior of my main view to be com.mypackage.CollapseBehavior and the app:layout_anchor to be my bottom sheet view so that the onDependentViewChanged callback would trigger. This effect was much smoother than trying to resize the view. I suspect returning to my initial strategy of using a BottomSheetBehavior.BottomSheetCallback would also work similarly to this solution.
Edit: per request, the relevant XML is below. I add a MapFragment into #+id/map_container at runtime, though this should also work with anything you drop into that container like a static image. The LocationListFragment could likewise be replaced with any view or fragment, so long as it still has the BottomSheetBehavior
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/fragment_coordinator">
<FrameLayout
android:id="#+id/map_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_anchor="#+id/list_container"
app:layout_behavior="com.mypackage.behavior.CollapseBehavior"/>
<fragment
android:name="com.mypackage.fragment.LocationListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/list_container"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>
</android.support.design.widget.CoordinatorLayout>
Patrick Grayson's post was very helpful. In my case though, I did need something that resized the map. I adopted the solution above to resize instead of translate. Perhaps others may be looking for a similar solution.
public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {
private int pixels = NO_RESIZE_BUFFER; // default value, in case getting a value from resources, bites the dust.
private static final int NO_RESIZE_BUFFER = 200; //The amount of dp to not have the bottom sheet ever push away.
public CollapseBehavior(Context context, AttributeSet attrs)
{
super(context, attrs);
pixels = (int)convertDpToPixel(NO_RESIZE_BUFFER,context);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
// child is the map
// dependency is the bottomSheet
if(isBottomSheet(dependency))
{
BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior());
int peekHeight;
if (behavior != null) {
peekHeight = behavior.getPeekHeight();
}
else
return true;
if(peekHeight > 0) { // Dodge the case where the sheet is hidden.
if (dependency.getTop() >= peekHeight) { // Otherwise we'd completely overlap the map
if(dependency.getTop() >= pixels) { // On resize when we have more than our NO_RESIZE_BUFFER of dp left.
if(dependency.getTop() > 0) { // Don't want to map to be gone completely.
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
params.height = dependency.getTop();
child.setLayoutParams(params);
}
return true;
}
}
}
}
return false;
}
private static float convertDpToPixel(float dp, Context context)
{
float densityDpi = context.getResources().getDisplayMetrics().densityDpi;
return dp * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
private static boolean isBottomSheet(#NonNull View view)
{
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if(lp instanceof CoordinatorLayout.LayoutParams)
{
return ((CoordinatorLayout.LayoutParams) lp).getBehavior() instanceof BottomSheetBehavior;
}
return false;
}
}
And the layout...
<FrameLayout
android:id="#+id/flMap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
android:layout_margin="0dp"
app:layout_anchor="#+id/persistentBottomSheet"
app:layout_behavior="com.yoursite.yourapp.CollapseBehavior">
<fragment
android:id="#+id/mapDirectionSummary"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.yoursite.yourapp.YourActivity" />
</FrameLayout>
<android.support.constraint.ConstraintLayout
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="wrap_content"
android:id="#+id/persistentBottomSheet"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height"
app:behavior_hideable="false"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
tools:context="com.yoursite.yourapp.YourActivity">
<!-- Whatever you want on the bottom sheet. -->
</android.support.constraint.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="8dp"
app:cardBackgroundColor="#324">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/colorPrimary"
android:theme="#style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="#style/Theme.AppCompat.Light">
<EditText
android:id="#+id/txtSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:ems="10"
android:inputType="text"
android:maxLines="1" />
</android.support.v7.widget.Toolbar>
</android.support.v7.widget.CardView>
</LinearLayout>
In my application I have a collapsing Toolbar. If I start a specific Fragment I want to collapse the Toolbar so it behaves like a "normal" one and that the user can not expend it by himself. Here is my layout which I use for my Toolbar:
<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:titleEnabled="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:src="#drawable/drawer_background"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
android:layout_width="match_parent"
android:layout_height="172dp"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:minHeight="100dp" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
I also can collapse the layout from code like so:
appBarLayout.setExpanded(false, false);
final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams();
params.setScrollFlags(0);
collapsingToolbarLayout.setLayoutParams(params);
But this does not help. If the user swipes down from the Toolbar it will expand.
Do you have any ideas why?
The core problem is that there is no CollapsingToolbarLayout.lock(); method up until now (v23.2.1 of support design). Hopefully, this feature will be included in a future version. Until then, I'm suggesting the following workaround:
You can collapse and lock your toolbar with:
appbar.setExpanded(false,false);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appbar.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_height);
and unlock it with:
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appbar.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.nav_header_height);
However, with the above code a problem occurs:
when forcefully collapsing CoordinatorLayout the title of our Toolbar is gone and CollapsingToolbarLayout.setTitle(CharSequence title); no longer works. To fix that we add a TextView in our Toolbar and manipulate its visibility accordingly. (we want it "gone" in a fragment that has its toolbar 'unlocked' and "visible" in a fragment that has its toolbar 'locked'.
We have to set TextView's android:textAppearance to be the same with CollapsingToolbarLayout's app:collapsedTitleTextAppearance to be consistent and avoid disrupting the user with different toolbar text sizes and colors.
In summary, with an interface like this:
public interface ToolbarManipulation {
void collapseToolbar();
void expandToolbar();
void setTitle(String s);
}
implemantations like these:
#Override
public void collapseToolbar(){
appbar.setExpanded(false,false);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appbar.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_height);
toolbarCollapsedTitle.setVisibility(View.VISIBLE);
}
#Override
public void expandToolbar() {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appbar.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.nav_header_height);
toolbarCollapsedTitle.setVisibility(View.GONE);
}
#Override
public void setTitle(String s) {
collapsingToolbarLayout.setTitle(s);
toolbarCollapsedTitle.setText(s);
collapsingToolbarLayout.invalidate();
toolbarCollapsedTitle.invalidate();
}
and main xml like this:
....
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:collapsedTitleTextAppearance="#style/CollapsedAppBar"
>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay"
>
<TextView
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:visibility="gone"
android:layout_gravity="center_vertical|start"
android:gravity="center_vertical|start"
android:id="#+id/toolbar_collapsed_title"
android:textAppearance="#style/CollapsedAppBar"
/>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
......
you can lock and unlock your Toolbar as you like. Your fragments should call these functions in their onResume();
I've uploaded an example implementation here.
Note: This is just a workaround and obviously not the cleanest solution. We're waiting for a newer version of com.android.support to resolve this.
I'm not exactly sure if this is what you are looking for
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return false;
}
});
Try to change the height of the AppBar. It works for me.
public void lockAppBar() {
int appbarHeight = (int)getResources().getDimension(R.dimen.your_fixed_appbar_height);
getView().findViewById(R.id.my_appbar).getLayoutParams().height = appbarHeight;
}
Do findViewById(R.id.image_view).setVisibility(View.GONE); where image_view is the id of the imageView in the collapsing toolbar. But if you want to do it for a specific fragment I suggest calling the same using fragment- activity communication.
None of the provided solutions worked for me except this one. With this solution, i can easily manage the state of collapsing toolbar. This will prevent expanding/enable collapsing of collapsing toolbar and set title for it.
public void lockAppBar(boolean locked,String title) {
if(locked){
appBarLayout.setExpanded(false, true);
int px = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80, getResources().getDisplayMetrics());
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appBarLayout.getLayoutParams();
lp.height = px;
appBarLayout.setLayoutParams(lp);
collapsingToolbarLayout.setTitleEnabled(false);
toolbar.setTitle(title);
}else{
appBarLayout.setExpanded(true, false);
appBarLayout.setActivated(true);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbarExpandHeight);
collapsingToolbarLayout.setTitleEnabled(true);
collapsingToolbarLayout.setTitle(title);
}
}
Well try setting some parameters to toolBar, tabLayout programatically, I have ViewPager changing flags on onPageSelected(int)
#Override
public void onPageSelected(int position) {
AppBarLayout.LayoutParams params = new AppBarLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (position == 3) {//listen for scrolls only for Fragment 3
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
toolBar.setLayoutParams(params);//hide as per scroll
params = new AppBarLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
tabLayout.setLayoutParams(params);//always visible
} else {
appBarLayout.setExpanded(false, true);
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED);
toolBar.setLayoutParams(params);//don't listen to scrolls
tabLayout.setLayoutParams(params);//dont listen to scrolls
}
}
Use/Set above flags as you required. AppBarLayout.LayoutParams
the simplest thing to do is to extend the class AppBarLayout.Behavior
public class AppBarBehavior extends AppBarLayout.Behavior {
private boolean locked;
public AppBarBehavior() {
super();
}
public AppBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout parent, #NonNull AppBarLayout child, #NonNull View directTargetChild, View target, int nestedScrollAxes, int type) {
if (locked) return false; // Lock when content scrolled
else return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
#Override
public boolean onTouchEvent(#NonNull CoordinatorLayout parent, #NonNull AppBarLayout child, #NonNull MotionEvent ev) {
if (locked) return false; // Lock when appBar swiped
else return super.onTouchEvent(parent, child, ev);
}
public void lock(boolean locked) {
this.locked = locked;
}
}
Then set new layout_behavior in XML
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/main.appbar"
android:layout_width="match_parent"
android:layout_height="200dp"
android:fitsSystemWindows="false"
app:layout_behavior="<full path to package>.AppBarBehavior">
And now you can manage locking with:
AppBarLayout appBar = findViewById(R.id.main_appbar);
appBar.setExpanded(false, true); // collapse appBar
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBar.getLayoutParams();
AppBarBehavior behavior = (AppBarBehavior) params.getBehavior();
behavior.lock(true); // lock appBar
I have an AppBarLayout that scrolls off screen when scrolling a RecyclerView.
Below the RecyclerView there is a RelativeLayout that is a footer.
The footer is shown only after scrolling up - it behave like it has
layout_scrollFlags="scroll|enterAlways"
but it doesn't have any scroll flags - is it a bug or am I doing something wrong? I want it to be always visible
before scroll
after scroll
Update
opened a google issue on this - it was marked 'WorkingAsIntended' this still doesn't help because I want a working solution of a footer inside a fragment.
Update 2
you can find the activity and the fragment xmls here -
note that if line 34 in activity.xml - the line containing app:layout_behavior="#string/appbar_scrolling_view_behavior" is commented out the text end is visible from the start - otherwise, it is visible only after scrolling up
I use a simplified version of Learn OpenGL ES's solution (https://stackoverflow.com/a/33396965/778951) -- which improves on Noa's solution (https://stackoverflow.com/a/31140112/1317564). It works fine for my simple quick-return toolbar above a TabLayout with footer buttons in each tab's ViewPager content.
Just set the FixScrollingFooterBehavior as the layout_behavior on the View/ViewGroup you want to keep aligned at the bottom of the screen.
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">
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:minHeight="?android:attr/actionBarSize"
app:title="Foo"
app:layout_scrollFlags="scroll|enterAlways|snap"
/>
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.spreeza.shop.ui.widgets.FixScrollingFooterBehavior"
/>
</android.support.design.widget.CoordinatorLayout>
Behavior:
public class FixScrollingFooterBehavior extends AppBarLayout.ScrollingViewBehavior {
private AppBarLayout appBarLayout;
public FixScrollingFooterBehavior() {
super();
}
public FixScrollingFooterBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (appBarLayout == null) {
appBarLayout = (AppBarLayout) dependency;
}
final boolean result = super.onDependentViewChanged(parent, child, dependency);
final int bottomPadding = calculateBottomPadding(appBarLayout);
final boolean paddingChanged = bottomPadding != child.getPaddingBottom();
if (paddingChanged) {
child.setPadding(
child.getPaddingLeft(),
child.getPaddingTop(),
child.getPaddingRight(),
bottomPadding);
child.requestLayout();
}
return paddingChanged || result;
}
// Calculate the padding needed to keep the bottom of the view pager's content at the same location on the screen.
private int calculateBottomPadding(AppBarLayout dependency) {
final int totalScrollRange = dependency.getTotalScrollRange();
return totalScrollRange + dependency.getTop();
}
}
Update
The solution below doesn't work for 5.1 as it works in 5 - instead of getTop use getTranslationY in any of the calculations you do.
layout.getTop()-->(int)layout.getTranslationY()
appbar.getTop()+toolbar.getHeight()-->(int)(appbar.getTranslationY()+toolbar.getHeight())
Update 2
with the new support library - 22.2.1 - there is no diff between 5.1 and prev versions, you should only use getTop and ignore the previous update in this answer
Original solution
After looking into many directions turns out the solution is actually simple - add paddingBottom to the fragment and adjust it as the page scrolls.
The padding is needed to cover for the changes in the toolbar y position - the coordinator layout is moving the entire page up and down as the toolbar disappears and reappears.
This can be achieved by extending AppBarLayout.ScrollingViewBehavior and setting this as the behavior of the fragment element of the activity.
Here are the basics of the code - it works for an activity with only a toolbar - you can replace it with appbar.getTop() + toolbar.getHeight() and this will work better if your appbar includes tabs.
activity.xml
<android.support.design.widget.CoordinatorLayout
android:id="#+id/main"
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:elevation="3dp"
app:elevation="3dp">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
/>
</android.support.design.widget.AppBarLayout>
<fragment
android:id="#+id/fragment"
android:name="com.example.noa.footer2.MainActivityFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.example.noa.footer2.MyBehavior"
tools:layout="#layout/fragment"/>
</android.support.design.widget.CoordinatorLayout>
fragment.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="48dp"
android:background="#android:color/holo_green_dark"
tools:context=".MainActivityFragment">
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#null"/>
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:background="#android:color/holo_red_light"/>
</RelativeLayout>
MainActivityFragment#onActivityCreated
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
CoordinatorLayout.LayoutParams lp = (LayoutParams) getView().getLayoutParams();
MyBehavior behavior = (MyBehavior) lp.getBehavior();
behavior.setLayout(getView());
}
MyBehavior
public class MyBehavior extends AppBarLayout.ScrollingViewBehavior {
private View layout;
public MyBehavior() {
}
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
boolean result = super.onDependentViewChanged(parent, child, dependency);
if (layout != null) {
layout.setPadding(layout.getPaddingLeft(), layout.getPaddingTop(), layout
.getPaddingRight(), layout.getTop());
}
return result;
}
public void setLayout(View layout) {
this.layout = layout;
}
}
I started out with Noa's solution (https://stackoverflow.com/a/31140112/1317564) and it worked for finger drags, but I was running into trouble with flings. After spending some time to trace the method calls and trying out different ideas, here is the solution I ended up with:
// Workaround for https://code.google.com/p/android/issues/detail?id=177195
// Based off of solution originally found here: https://stackoverflow.com/a/31140112/1317564
#SuppressWarnings("unused")
public class CustomScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
private AppBarLayout appBarLayout;
private boolean onAnimationRunnablePosted = false;
#SuppressWarnings("unused")
public CustomScrollingViewBehavior() {
}
#SuppressWarnings("unused")
public CustomScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
if (appBarLayout != null) {
// We need to check from when a scroll is started, as we may not have had the chance to update the layout at
// the start of a scroll or fling event.
startAnimationRunnable(child, appBarLayout);
}
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
#Override
public boolean onMeasureChild(CoordinatorLayout parent, final View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
if (appBarLayout != null) {
final int bottomPadding = calculateBottomPadding(appBarLayout);
if (bottomPadding != child.getPaddingBottom()) {
// We need to update the padding in onMeasureChild as otherwise we won't have the correct padding in
// place when the view is flung, and the changes done in onDependentViewChanged will only take effect on
// the next animation frame, which means it will be out of sync with the new scroll offset. This is only
// needed when the view is flung -- when dragged with a finger, things work fine with just
// implementing onDependentViewChanged().
child.setPadding(child.getPaddingLeft(), child.getPaddingTop(), child.getPaddingRight(), bottomPadding);
}
}
return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, final View child, final View dependency) {
if (appBarLayout == null)
appBarLayout = (AppBarLayout) dependency;
final boolean result = super.onDependentViewChanged(parent, child, dependency);
final int bottomPadding = calculateBottomPadding(appBarLayout);
final boolean paddingChanged = bottomPadding != child.getPaddingBottom();
if (paddingChanged) {
// If we've changed the padding, then update the child and make sure a layout is requested.
child.setPadding(child.getPaddingLeft(),
child.getPaddingTop(),
child.getPaddingRight(),
bottomPadding);
child.requestLayout();
}
// Even if we didn't change the padding, if onDependentViewChanged was called then that means that the app bar
// layout was changed or was flung. In that case, we want to check for these changes over the next few animation
// frames so that we can ensure that we capture all the changes and update the view pager padding to match.
startAnimationRunnable(child, dependency);
return paddingChanged || result;
}
// Calculate the padding needed to keep the bottom of the view pager's content at the same location on the screen.
private int calculateBottomPadding(AppBarLayout dependency) {
final int totalScrollRange = dependency.getTotalScrollRange();
return totalScrollRange + dependency.getTop();
}
private void startAnimationRunnable(final View child, final View dependency) {
if (onAnimationRunnablePosted)
return;
final int onPostChildTop = child.getTop();
final int onPostDependencyTop = dependency.getTop();
onAnimationRunnablePosted = true;
// Start looking for changes at the beginning of each animation frame. If there are any changes, we have to
// ensure that layout is run again so that we can update the padding to take the changes into account.
child.postOnAnimation(new Runnable() {
private static final int MAX_COUNT_OF_FRAMES_WITH_NO_CHANGES = 5;
private int previousChildTop = onPostChildTop;
private int previousDependencyTop = onPostDependencyTop;
private int countOfFramesWithNoChanges;
#Override
public void run() {
// Make sure we request a layout at the beginning of each animation frame, until we notice a few
// frames where nothing changed.
final int currentChildTop = child.getTop();
final int currentDependencyTop = dependency.getTop();
boolean hasChanged = false;
if (currentChildTop != previousChildTop) {
previousChildTop = currentChildTop;
hasChanged = true;
countOfFramesWithNoChanges = 0;
}
if (currentDependencyTop != previousDependencyTop) {
previousDependencyTop = currentDependencyTop;
hasChanged = true;
countOfFramesWithNoChanges = 0;
}
if (!hasChanged) {
countOfFramesWithNoChanges++;
}
if (countOfFramesWithNoChanges <= MAX_COUNT_OF_FRAMES_WITH_NO_CHANGES) {
// We can still look for changes on subsequent frames.
child.requestLayout();
child.postOnAnimation(this);
} else {
// We've encountered enough frames with no changes. Do a final layout request, and don't repost.
child.requestLayout();
onAnimationRunnablePosted = false;
}
}
});
}
}
I'm not a fan of rechecking the layout on every animation frame, and this solution isn't perfect as I've seen some issues if programmatically expanding/collapsing the app bar layout, but for now I haven't found a better solution. The performance is fine on a new device and acceptable on an older device. If someone else does, please feel free to take my answer as a source and repost.
package pl.mkaras.utils;
import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
public class ScrollViewBehaviorFix extends AppBarLayout.ScrollingViewBehavior {
public ScrollViewBehaviorFix() {
super();
}
public ScrollViewBehaviorFix(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
if (child.getLayoutParams().height == -1) {
List<View> dependencies = parent.getDependencies(child);
if (dependencies.isEmpty()) {
return false;
}
final AppBarLayout appBar = findFirstAppBarLayout(dependencies);
if (appBar != null && ViewCompat.isLaidOut(appBar)) {
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
availableHeight = parent.getHeight();
}
final int height = availableHeight - appBar.getMeasuredHeight();
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
int childContentHeight = getContentHeight(child);
if (childContentHeight <= height) {
updateToolbar(parent, appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed, false);
heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
return true;
} else {
updateToolbar(parent, appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed, true);
return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}
}
}
return false;
}
private static int getContentHeight(View view) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int contentHeight = 0;
for (int index = 0; index < viewGroup.getChildCount(); ++index) {
View child = viewGroup.getChildAt(index);
contentHeight += child.getMeasuredHeight();
}
return contentHeight;
} else {
return view.getMeasuredHeight();
}
}
private static AppBarLayout findFirstAppBarLayout(List<View> views) {
int i = 0;
for (int z = views.size(); i < z; ++i) {
View view = views.get(i);
if (view instanceof AppBarLayout) {
return (AppBarLayout) view;
}
}
throw new IllegalArgumentException("Missing AppBarLayout in CoordinatorLayout dependencies");
}
private void updateToolbar(CoordinatorLayout parent, AppBarLayout appBar, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed, boolean toggle) {
toggleToolbarScroll(appBar, toggle);
appBar.forceLayout();
parent.onMeasureChild(appBar, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}
private void toggleToolbarScroll(AppBarLayout appBar, boolean toggle) {
for (int index = 0; index < appBar.getChildCount(); ++index) {
View child = appBar.getChildAt(index);
if (child instanceof Toolbar) {
Toolbar toolbar = (Toolbar) child;
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
int scrollFlags = params.getScrollFlags();
if (toggle) {
scrollFlags |= AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL;
} else {
scrollFlags &= ~AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL;
}
params.setScrollFlags(scrollFlags);
}
}
}
}
This behavior basically removes scroll flag SCROLL from AppBarLayout, when scrolling content in dependent view (RecyclerView, NestedScrollView) is less than view height, ie. when scrolling is not needed. It also overrides offsetting scrolling view, which is normally done by AppBarLayout.ScrollingViewBehavior. Works well when adding footer, ie. button, to scrolling view or in ViewPager, where content length may be different in each page.
I think creating a fixed header and footer could solver your problem. I would've wrote this in the comments but I don't have 50 rep. You could figure out how to do it here
I did something along the lines of ensuring I added
android:layout_gravity="end|bottom"
to the layout in XML that I wanted at the bottom of the CoordinatorLayout
and then set in code:
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
if (mFooterView != null) {
final int height = mFooterView.getHeight();
mRecyclerView.setPadding(0, 0, 0, height);
mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
Note: that the footer View/ViewGroup needs to be higher in the z-axis (listed below the RecyclerView in XML) to function properly
Surround your elements with a linearlayout, like that:
<android.support.design.widget.CoordinatorLayout >
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout>
<android.support.v7.widget.Toolbar />
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_main" />
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Android CoordinatorLayout Bottom Layout Behaviour Example
activity_bottom.xml
<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/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimaryDark"
app:layout_scrollFlags="scroll|enterAlways"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#C0C0C0"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
<com.example.android.coordinatedeffort.widget.FooterBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom">
<TextView
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#007432"
android:gravity="center"
android:text="Footer View"
android:textColor="#android:color/white"
android:textSize="25sp" />
</com.example.android.coordinatedeffort.widget.FooterBarLayout>
</android.support.design.widget.CoordinatorLayout>
FooterBarLayout.java
FooterBarBehavior.java
There is a library for your problem. Hope this will really help for you
Here is the library
And another problem you have mentioned fixed the footer. the below one is the relative layout so use the feature android:layout_alignParentBottom="true" on your footer.
Hope you i have solved the issue