Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate both top and bottom Toolbars(or any other view) enter/exit screen on scroll in CoordinatorLayout?

My activity contains 2 AppBarLayouts in a CoordinatorLayout, one on the top of screen, and the other bottom. I want to make both of the 2 AppBarLayouts hide on scroll of a RecyclerView.

When there is only the top one, it's easy to make it hide on scroll by adding app:layout_scrollFlags="scroll|enterAlways" to the Toolbar, and app:layout_behavior="@string/appbar_scrolling_view_behavior" to the container of RecyclerView.

When there is only the bottom AppBarLayout which hide on scroll, it was realized by making a custom behavior BottomAppBarLayoutBehavior extends AppBarLayout.Behavior.

However, when both of them are made hide on scroll, they succeed to hide but the RecyclerView shakes on scroll. And the area of top Toolbar leaves empty white space after hiding. Below is the xml:

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/topToolbar"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways" />

    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:layout_width="fill_parent"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:id="@+id/RecyclerViewContainer"
        android:layout_height="fill_parent"/>

    <android.support.design.widget.AppBarLayout 
        android:layout_width="match_parent"
        android:layout_gravity="bottom"
        app:layout_behavior="myPackage.BottomAppBarLayoutBehavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:orientation="horizontal"
            android:layout_height="match_parent">
            <!-- some Views -->
        </LinearLayout>

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

Below is the code of BottomAppBarLayoutBehavior:

public class BottomAppBarLayoutBehavior extends AppBarLayout.Behavior {
    private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
    private boolean mIsAnimatingOut = false;

    public BottomAppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super();
    }

    @Override
    public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final AppBarLayout child,
                                       final View directTargetChild, final View target, final int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final AppBarLayout child,
                               final View target, final int dxConsumed, final int dyConsumed,
                               final int dxUnconsumed, final int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
            animateOut(child);
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            animateIn(child);
        }
    }

    private void animateOut(final AppBarLayout appBarLayout) {
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(appBarLayout).translationY(168F).alpha(0.0F).setInterpolator(INTERPOLATOR).withLayer()
                    .setListener(new ViewPropertyAnimatorListener() {
                        public void onAnimationStart(View view) {
                            BottomAppBarLayoutBehavior.this.mIsAnimatingOut = true;
                        }

                        public void onAnimationCancel(View view) {
                            BottomAppBarLayoutBehavior.this.mIsAnimatingOut = false;
                        }

                        public void onAnimationEnd(View view) {
                            BottomAppBarLayoutBehavior.this.mIsAnimatingOut = false;
                            view.setVisibility(View.GONE);
                        }
                    }).start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(appBarLayout.getContext(), R.anim.fab_out);
            anim.setInterpolator(INTERPOLATOR);
            anim.setDuration(200L);
            anim.setAnimationListener(new Animation.AnimationListener() {
                public void onAnimationStart(Animation animation) {
                    BottomAppBarLayoutBehavior.this.mIsAnimatingOut = true;
                }

                public void onAnimationEnd(Animation animation) {
                    BottomAppBarLayoutBehavior.this.mIsAnimatingOut = false;
                    appBarLayout.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(final Animation animation) {
                }
            });
            appBarLayout.startAnimation(anim);
        }
    }

    private void animateIn(AppBarLayout appBarLayout) {
        appBarLayout.setVisibility(View.VISIBLE);
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(appBarLayout).scaleX(1.0F).scaleY(1.0F).alpha(1.0F)
                    .setInterpolator(INTERPOLATOR).withLayer().setListener(null)
                    .start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(appBarLayout.getContext(), R.anim.fab_in);
            anim.setDuration(200L);
            anim.setInterpolator(INTERPOLATOR);
            appBarLayout.startAnimation(anim);
        }
    }
}
like image 618
ProtossShuttle Avatar asked Dec 06 '22 20:12

ProtossShuttle


1 Answers

I have found the solution by myself. The AppBarLayout wrapper for the bottom LinearLayout is redundant. Remove the AppBarLayout wrapper and make the Behavior class extends CoordinatorLayout.Behavior<LinearLayout>. Finally add the behavior to LinearLayout, then the top Toolbar and bottom LinearLayout enter and exit screen on scroll correctly.

The correct activity xml:

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/top_toolbar"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways" />

    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:layout_width="fill_parent"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:id="@+id/recycler_view_container"
        android:layout_height="fill_parent"/>

    <LinearLayout
        android:id="@+id/bottom_bar"
        android:layout_width="match_parent"
        android:orientation="horizontal"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:layout_behavior="*THE_FULL_PACKAGE_NAME*.LinearLayoutBehavior ">
        <!-- child views -->
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

The correct Behavior class:

public class LinearLayoutBehavior extends CoordinatorLayout.Behavior<LinearLayout> {
    private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
    private boolean mIsAnimatingOut = false;

    public LinearLayoutBehavior(Context context, AttributeSet attrs) {
        super();
    }

    @Override
    public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final LinearLayout child,
                                       final View directTargetChild, final View target, final int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final LinearLayout child,
                               final View target, final int dxConsumed, final int dyConsumed,
                               final int dxUnconsumed, final int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            animateOut(child);
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            animateIn(child);
        }
    }

    private void animateOut(final LinearLayout linearLayout) {
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(linearLayout).translationY(168F).alpha(0.0F).setInterpolator(INTERPOLATOR).withLayer()
                    .setListener(new ViewPropertyAnimatorListener() {
                        public void onAnimationStart(View view) {
                            LinearLayoutBehavior.this.mIsAnimatingOut = true;
                        }

                        public void onAnimationCancel(View view) {
                            LinearLayoutBehavior.this.mIsAnimatingOut = false;
                        }

                        public void onAnimationEnd(View view) {
                            LinearLayoutBehavior.this.mIsAnimatingOut = false;
                            view.setVisibility(View.GONE);
                        }
                    }).start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(linearLayout.getContext(), R.anim.fab_out);
            anim.setInterpolator(INTERPOLATOR);
            anim.setDuration(200L);
            anim.setAnimationListener(new Animation.AnimationListener() {
                public void onAnimationStart(Animation animation) {
                    LinearLayoutBehavior.this.mIsAnimatingOut = true;
                }

                public void onAnimationEnd(Animation animation) {
                    LinearLayoutBehavior.this.mIsAnimatingOut = false;
                    linearLayout.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(final Animation animation) {
                }
            });
            linearLayout.startAnimation(anim);
        }
    }

    private void animateIn(LinearLayout linearLayout) {
        linearLayout.setVisibility(View.VISIBLE);
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(linearLayout).translationY(0).scaleX(1.0F).scaleY(1.0F).alpha(1.0F)
                    .setInterpolator(INTERPOLATOR).withLayer().setListener(null)
                    .start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(linearLayout.getContext(), R.anim.fab_in);
            anim.setDuration(200L);
            anim.setInterpolator(INTERPOLATOR);
            linearLayout.startAnimation(anim);
        }
    }
}
like image 178
ProtossShuttle Avatar answered Jan 17 '23 08:01

ProtossShuttle