CoordinatorLayout Custom Behavior with LinearLayout as child - android

I want to implement a custom behavior on LinearLayout.
This is the structure of my xml:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout.../>
<android.support.v4.widget.DrawerLayout.../>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior="com.xxx.BottomBarBehavior"
android:layout_gravity="bottom|center_horizontal".../>
</android.support.design.widget.CoordinatorLayout>
The LinearLayout is inside CoordinatorLayout, below the DrawerLayout.
The LinearLayout is not moving up when the snackbar is shown. I just can't get my head around it. The layout is such that the DrawerLayout contains a ViewPager and this viewPager is populated by another xml by a fragment. And the snackbar is generated by an element in the recyclerView of this fragment.
This is how my BottomBarBehavior custom behavior class looks like:
public class BottomBarBehavior extends CoordinatorLayout.Behavior<LinearLayout> {
public BottomBarBehavior(Context context, AttributeSet attrs) {}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, LinearLayout child, View dependency) {
return ( dependency instanceof Snackbar.SnackbarLayout ) ||
( dependency instanceof DrawerLayout );
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency) {
if( dependency instanceof Snackbar.SnackbarLayout ) {
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
child.setTranslationY(translationY);
}
return true;
}
}

I think you are just missing a super(); in the constructor of BottomBarBehavior.

Related

How to use Behavior for nested classes?

I am making behavior to hide view when AppToolBar hides. This one works if I place Cardview with behavior in same xml where CoordinatorLayout is defined. However I need that behviour deeper in hierarchy and whe I use it in Fragments it does not work any more.
public class BottomBehaviour extends CoordinatorLayout.Behavior<CardView> {
int totalHeigh;
float dencity;
int statusBarHeigh;
public BottomBehaviour() {
}
public BottomBehaviour(Context context, AttributeSet attrs) {
super(context, attrs);
dencity=context.getResources().getDisplayMetrics().density;
int height = context.getResources().getDisplayMetrics().heightPixels;
statusBarHeigh = getStatusBarHeight(context);
totalHeigh=height;
//totalHeigh = (int) (context.getResources().getDisplayMetrics().heightPixels - (56 * dencity));
Log.e("mcheck", "BottomBehaviour: ");
}
public int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, CardView child, View dependency) {
return dependency instanceof AppBarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, CardView child, View dependency) {
int top = (int)(totalHeigh-child.getHeight()-(dependency.getY()));
child.setTranslationY(top);
Log.e("mcheck", "onDependentViewChanged: "+(dependency.getY()-statusBarHeigh)+" height "+dependency.getHeight()/dencity);
return super.onDependentViewChanged(parent, child, dependency);
}
}
Some fragment xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/tools"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="56dip"
app:layout_behavior="ua.miui.forum.widget.BottomBehaviour"
card_view:cardCornerRadius="0dip"
card_view:cardElevation="8dip">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
CoordinatorLayout.Behaviors work only if the target view, i.e the view that holds app:layout_behavior="ua.miui.forum.widget.BottomBehaviour" is direct descendant of CoordinatorLayout. Otherwise it will not receive the nested scroll events.
If your LinearLayout is direct child of CoordinatorLayout, add the behavior to it, and then you should probably tweak you to look (for view id e.g) for yourCardView and act on nested scrolling.

Using Coordinator Layout to move floating action button when a custom popup animates

Long story short, can you get the floatingActionButton to move up and down when a custom view is moved up and down in the same way as it does with the snackbar?
I have a Floating Action Button in a coordinator layout that moves up when the snackbar is shown. I've even managed to customize the FABs "Behaviour" through tutorials to make it shrink and grow rather than move up and down.
I also have a custom made popup (LinearLayout) that animates up from the bottom and back down again when a button is pressed and that currently just pops up behind the floating action button which doesn't move. Both are in a CoordinatorLayout I would like the floating action button to go up and down when the custom popup animates up and down...in the same way it does for the snackbar.
I've kind of managed to get it to work by adding my custom popup as a dependency on calls to the layoutDependsOn method of the FloatingActionButton behaviour class. However my implementation for the onDependentViewChanged method to actually get the FAB to animate up and down just feels wrong as i'm effectively having to animate the floatingActionButton seperately with the a coordinated animation to match the custom popups animation.
Can anybody point me at how to do this better and more easily?
I'll try to provide meaningful code snippets....
custom popup:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:id="#+id/selected_item_popup_linear_layout"
android:elevation="4dp"
android:visibility="gone"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/selectedItemPopupBackground" android:elevation="4dp"
android:id="#+id/selected_item_popup_content">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/selected_item_popup_delete_button"
android:layout_gravity="center"
android:padding="14dp"
android:src="#drawable/ic_delete_black_24dp"
android:background="#color/selectedItemPopupBackground"
android:tint="#color/selectedItemPopupDeleteTint"
/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/selected_item_popup_edit_button"
android:layout_gravity="center"
android:padding="14dp"
android:src="#drawable/ic_mode_edit_black_24dp"
android:background="#color/selectedItemPopupBackground"
android:tint="#color/selectedItemPopupEditTint"/>
</LinearLayout>
Floating action button:
<android.support.design.widget.FloatingActionButton
android:id="#+id/floating_action_button_add_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_add_white_24dp"
android:visibility="visible"
android:layout_marginBottom="#dimen/fab_margin_bottom"
android:layout_marginRight="#dimen/fab_margin_right"
app:layout_anchor="#id/pager"
app:layout_anchorGravity="bottom|right|end"
app:layout_behavior=".FloatingActionButtonBehaviour"
app:rippleColor="#color/boardItemBackgroundSelected"/>
Floating Action Button Behaviour:
public class FloatingActionButtonBehaviour extends CoordinatorLayout.Behavior<FloatingActionButton> {
private Context mContext;
public FloatingActionButtonBehaviour(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
if (dependency.equals(parent.findViewById(R.id.selected_item_popup_linear_layout))) {
return true;
}
if (dependency instanceof Snackbar.SnackbarLayout) {
return true;
}
return false;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, final FloatingActionButton child, View dependency) {
if(dependency instanceof Snackbar.SnackbarLayout) {
float translationY = getFabTranslationYForSnackbar(parent, child);
float percentComplete = -translationY / dependency.getHeight();
float scaleFactor = 1 - percentComplete;
child.setScaleX(scaleFactor);
child.setScaleY(scaleFactor);
}
if (dependency.equals(parent.findViewById(R.id.selected_item_popup_linear_layout))) {
View contentView = dependency.findViewById(R.id.selected_item_popup_content);
int contentViewHeight = contentView.getHeight();
if (dependency.getVisibility() == View.VISIBLE) {
Animation bottomUp = AnimationUtils.loadAnimation(mContext, R.anim.fab_move_up);
child.startAnimation(bottomUp);
child.setVisibility(View.VISIBLE);
float translationY = Math.min(0, dependency.getTranslationY() - contentViewHeight);
child.setTranslationY(translationY);
} else {
Animation bottomDown = AnimationUtils.loadAnimation(mContext, R.anim.fab_move_down);
child.startAnimation(bottomDown);
child.setVisibility(View.VISIBLE);
child.setTranslationY(dependency.getTranslationY());
}
}
return true;
}
private float getFabTranslationYForSnackbar(CoordinatorLayout parent, FloatingActionButton fab) {
float minOffset = 0;
final List<View> dependencies = parent.getDependencies(fab);
for (int i = 0, z = dependencies.size(); i < z; i++) {
final View view = dependencies.get(i);
if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - view.getHeight());
}
}
return minOffset;
}
}
Code to Animate the custom popup:
Animation bottomUp = AnimationUtils.loadAnimation(mActivity, R.anim.bottom_up);
final ViewGroup selectedItemPopupLinearLayout = (ViewGroup)mActivity.findViewById(R.id.selected_item_popup_linear_layout);
selectedItemPopupLinearLayout.startAnimation(bottomUp);
selectedItemPopupLinearLayout.setVisibility(View.VISIBLE);
I think I found my answer in BottomSheets/ModalSheets....
https://guides.codepath.com/android/Handling-Scrolls-with-CoordinatorLayout#bottom-sheets
Not tried it yet but it looks the business

Stop AppBarLayout scrolling off screen when NestedScrollView is empty

I have a fairly typical List functionality using a CoordinatorLayout, AppBarLayout, SwipeRefreshLayout and RecyclerView -
When the RecyclerView has enough content to scroll, the page seems fine. When the RecyclerView is empty or doesn't have enough content to scroll however, the behavior is that the AppBarLayout children with app:layout_scrollFlags="scroll|enterAlwaysCollapsed" will continue to scroll - which looks odd.
Is there a way to stop the AppBarLayout children scrolling when the NestedScrollView is empty?
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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.CoordinatorLayout
android:id="#+id/coordinatorLayout"
android:background="#android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:elevation="4dp">
<LinearLayout
android:id="#+id/eventHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="#color/green"
android:orientation="horizontal"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="scroll|enterAlwaysCollapsed"
android:textColor="#color/white"
android:textSize="15sp"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/swipeToRefresh"
android:layout_width="match_parent"
android:layout_gravity="fill_vertical"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
android:dividerHeight="0dp"
android:layout_gravity="fill_vertical"
android:drawSelectorOnTop="true"
android:listSelector="#drawable/selector_ripple_grey_transparent"
android:scrollbars="vertical"/>
</android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>
<TextView
android:id="#+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
android:text="#string/no_data_available"
android:textSize="17sp"/>
</FrameLayout>
Not sure how elegant a solution this is but, I overrode the onStartNestedScroll() event to only fire if the NestedScrollView is scrollable (In this case a RecyclerView)
in onCreate():
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams();
layoutParams.setBehavior(new AppBarLayoutNoEmptyScrollBehavior(mAppBarLayout, mRecyclerView));
Behavior:
public class AppBarLayoutNoEmptyScrollBehavior extends AppBarLayout.Behavior {
AppBarLayout mAppBarLayout;
RecyclerView mRecyclerView;
public AppBarLayoutNoEmptyScrollBehavior(AppBarLayout appBarLayout, RecyclerView recyclerView) {
mAppBarLayout = appBarLayout;
mRecyclerView = recyclerView;
}
public boolean isRecylerViewScrollable(RecyclerView recyclerView) {
int recyclerViewHeight = recyclerView.getHeight(); // Height includes RecyclerView plus AppBarLayout at same level
int appCompatHeight = mAppBarLayout != null ? mAppBarLayout.getHeight() : 0;
recyclerViewHeight -= appCompatHeight;
return recyclerView.computeVerticalScrollRange() > recyclerViewHeight;
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
if (isRecylerViewScrollable(mRecyclerView)) {
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
return false;
}
}
EDIT
Edited solution as RecyclerView gives height as visible RecyclerView height and AppBarLayout height (which is the CoordinatorLayout height).
However, if your scroll gesture starts on the visible AppBarLayout area, a scroll will still take place, even if you add this Behavior to the AppBarLayout as well. This answer therefore is not a fix for the problem.
(Based on : Reference)
(1) Create this class.
public class AppBarLayoutBehaviorForEmptyRecyclerView extends AppBarLayout.Behavior
{
private boolean canRecyclerViewBeScrolled = false;
public AppBarLayoutBehaviorForEmptyRecyclerView()
{
}
public AppBarLayoutBehaviorForEmptyRecyclerView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev)
{
return canRecyclerViewBeScrolled && super.onInterceptTouchEvent(parent, child, ev);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes)
{
updateScrollable(target);
return canRecyclerViewBeScrolled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
#Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed)
{
return canRecyclerViewBeScrolled && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
private void updateScrollable(View targetChild)
{
if(targetChild instanceof RecyclerView)
{
RecyclerView.Adapter adapter = ((RecyclerView) targetChild).getAdapter();
canRecyclerViewBeScrolled = adapter != null && adapter.getItemCount() > 0;
}
else
{
canRecyclerViewBeScrolled = true;
}
}
}
(2) Add to your AppBarLayout XML element the following attribute:
app:layout_behavior="com.xxxx.xxxxxx.AppBarLayoutBehaviorForEmptyRecyclerView"
Graeme answer is ok but I also added in constructor
public AppBarLayoutOnEmptyRecyclerViewScrollBehavior(#NonNull AppBarLayout appBarLayout, #NonNull RecyclerView recyclerView) {
this.appBarLayout = checkNotNull(appBarLayout);
this.recyclerView = checkNotNull(recyclerView);
setDragCallback(new DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return isRecylerViewScrollable(recyclerView);
}
});
}
so when RecyclerView is empty I also disable drag from AppBarLayout
After loading data to your RecyclerView, if it's empty, just disalbe the scroll flag manually:
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mEventHeader.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mEventHeader.setLayoutParams(toolbarLayoutParams);
And if not empty, set back the scroll flag:
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mEventHeader.getLayoutParams();
toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
mEventHeader.setLayoutParams(toolbarLayoutParams);

FloatingActionButton does not come down when dismissing Snackbar

I am trying to use a Snackbar. I have a FloatingActionButton wrapped in a CoordinatorLayout. When the Snackbar shows, the button is correctly moved up. When it dismisses automatically, the button moves down. But if I dismiss the Snackbar programmatically, the button does not go down. My code is simple:
mSnackbar = Snackbar.make(mCoordinatorLayout, text, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, new View.OnClickListener() {
#Override
public void onClick(View v) {
undoDeleteTasks();
}
});
mSnackbar.show();
Is there a way to make the FloatingActionButton move down when the Snackbar is dismissed programmatically?
Try this:
Snackbar mysnack = Snackbar.make( fab, "Hi, welcome to my app!", Snackbar.LENGTH_LONG );
mysnack.getView().addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow( View v ) {
}
#Override
public void onViewDetachedFromWindow( View v ) {
fab.setTranslationY( 0 );
}
});
mysnack.show();
The problem is, in an CoordinatorLayout, the FAB is moved because its behavior class's onDependentViewChanged() is called when the Snackbar animates in or out.
However, when you call Snabackbar.dismiss() no animation takes place. And therefore no onDependentViewChanged(). And thus movement of the FAB.
I want to know if it possible to either
animate the Snackbar dismiss?
create a behavior that moves the FAB when the Snackbar causes onDependentViewRemoved()?
Farzad119's answer is arguably a little brittle. My approach--although not satisfactory either--is to alter the behavior on the FAB to move the FAB down when the SnackBar is removed:
#Override
public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
super.onDependentViewRemoved(parent, child, dependency);
float translationY = Math.min(0, parent.getBottom() - child.getBottom());
child.setTranslationY(translationY);
}
To solve this issue I defined a RelativeLayout Behavior like this. This can be done for any view, just replace all RelativeLayout with desired UI element.
public class RelativeLayoutBehavior extends CoordinatorLayout.Behavior<RelativeLayout> {
public RelativeLayoutBehavior(Context context, AttributeSet attrs) {
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, RelativeLayout child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RelativeLayout child, final View dependency) {
Float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
child.setTranslationY(translationY);
return true;
}
#Override
public void onDependentViewRemoved(CoordinatorLayout parent, RelativeLayout child, View dependency) {
child.setTranslationY(0);
}
}
Then you need to add the Behavior into the RelativeLayout "android:layout_behavior" property like this. Make sure the RelativeLayout you want to slide up is inside of a CoordinateLayout also.
<?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/main_coordinate_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#323232"
tools:context="com.choch.michaeldicioccio.myapplication.MainActivity">
<RelativeLayout
android:id="#+id/bottom_navigation_relative_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom"
app:layout_behavior="com.yourPackagePath.RelativeLayoutBehavior" >
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#color/colorPrimary"
android:clickable="true"
app:elevation="8dp"
app:itemBackground="#color/colorPrimary"
app:itemIconTint="#drawable/bottom_navigation_color_selector"
app:itemTextColor="#drawable/bottom_navigation_color_selector"
app:menu="#menu/bottom_bar_menu" />
<android.support.design.widget.FloatingActionButton
android:id="#+id/add_car_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_above="#id/bottom_navigation"
android:layout_margin="#dimen/fab_margin"
android:scaleType="center"
android:src="#mipmap/ic_plus_black_36dp"
android:tint="#color/colorPrimary"
app:elevation="8dp"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

Android - footer scrolls off screen when used in CoordinatorLayout

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

Categories

Resources