Android imitates homepage custom Behavior

Android custom Behavior implements sliding with gestures, showing hidden Title bars, bottom navigation bars and suspension buttons

Coordinator Layout under the Android Design package is a very important control that makes many animation implementations possible and easier. According to the official explanation, Coordinator Layout is the parent view used to coordinate the interaction of child View. Behavior can be seen as a component of Coordinator Layout's child view to realize interaction.
The main purpose of this blog is to realize the sliding nested animation of the analogous Android client's homepage. Some time ago, we used idle time to build a dry goods concentration camp's client. When doing this, we used custom Behavior to realize the whole nested sliding, and extracted it as a lib for easy use.

Let's start with a wave of renderings:

Ideas for achieving results:

  1. Judging gestures

  2. Calculating distance

  3. Trigger animation

Catalogue of articles:

  1. Introduction to Coordinator Layout and Behavior
  2. Custom Behavior
  3. Realization and Individualization of Imitated Effect Animation

Introduction to Coordinator Layout and Behavior

The principle of Android sliding nesting and Behavior analysis have been explained by many gods and recommended by Loader god. See Coordinator Layout. Behavior Principle from Source Code.

Here is a brief introduction. The interaction between parent View (Nested Scrolling Parent interface) and child View (Nested Scrolling Child interface) is controlled by Nested Scrolling. Nested Scrolling Parent Helper and Nested Scrolling Child Helper two auxiliary classes deal with the size of parent layout and child View respectively. Quantitative logic.

The simple process of sliding nesting is: controlling the onInterceptTouchEvent of the child View (such as Recycler View) and the event distribution of onTouchEvent - > calling different methods of NestedScrolling ChildHelper - > handling the logic of interaction with NestedScrolling Parent - > implementing NestedScrolling Parent handling specific logic in the parent layout (such as Coordinator Layout).
(-> Behavior's event handling method is mainly invoked by Coordinator Layout's various event handling methods, and the return value controls the event consumption of the parent layout.

The specific method calls you can read Loader God's blog again. Here's a brief introduction to the implementation of custom Behavior Behavior website.

1.layoutDependsOn

Determine whether the provided subview has another specific sibling view as a layout dependency. That is to say, it is used to determine dependencies. If a control needs to depend on a control, it overrides the method.
For example, AppBarLayout

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof AppBarLayout;
    }

2.onDependentViewChanged

This method is invoked when the size and position of the view changes. Rewriting this method can handle the child's response. For example, the commonly used AppBarLayout, when it changes, the child View responds by rewriting.

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        offsetChildAsNeeded(parent, child, dependency);
        return false;
    }

3.onStartNestedScroll

This method is triggered when the Coordinator Layout's sub-View begins to slide nested (where the sliding View must implement the NestedScrollingChild interface). Adding Behavior controls requires a direct subview of Coordinator Layout, otherwise the process will not continue.

    //Judgment of vertical slip
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

4.onNestedPreScroll

In this method, consumed refers to the rolling distance consumed by the parent layout, consumed[0] is the distance consumed in the horizontal direction, and consumed[1] is the distance consumed in the vertical direction. This parameter can be adjusted accordingly.
For vertical sliding, if consumed[1]=dy is set, it means that the parent layout consumes all the sliding distance. Similar to AppBarLayout, when it transits from unfolding to folding, the nested sliding is controlled by consumed.

    /**
     * Method invoked before triggering sliding nested scroll
     *
     * @param coordinatorLayout coordinatorLayout Parent Layout
     * @param child             Use Behavior's SubView
     * @param target            Triggering Sliding Nested View (Implementing NestedScrollingChild Interface)
     * @param dx                X-Axis Distance of Sliding
     * @param dy                Sliding Y-Axis Distance
     * @param consumed          The sliding distance of parent layout consumption, consumed[0] and consumed[1] represent the distance of parent layout consumption in X and Y directions, default is 0.
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, 
        int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

5.onNestedScroll

In this method, dyConsumed represents the distance consumed by TargetView, such as the distance sliding by RecyclerView. Some animations can be specified by controlling the sliding of NestScrolling Child.
The main effect of this blog is to rewrite this method. If we judge by dy in onNested PreScroll, when there are few RecyclerView entries, it will trigger logical code, so we choose to rewrite this method.

    /**
     * Method of triggering when sliding nested rolling
     *
     * @param coordinatorLayout coordinatorLayout Parent Layout
     * @param child             Use Behavior's SubView
     * @param target            Triggering Sliding Nested View
     * @param dxConsumed        TargetView X-Axis Distance of Consumption
     * @param dyConsumed        TargetView Y-Axis Distance of Consumption
     * @param dxUnconsumed      X-Axis Distance Not Consumed by TargetView
     * @param dyUnconsumed      Y-axis distances not consumed by TargetView (e.g. RecyclerView has reached the top or bottom,
     *              While the user continues to slide, the value of dyUnconsumed is not zero at this time, which can handle some cross-border events.
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, 
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, 
            dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

Custom Behavior

There are two main ways to customize Behavior:

The first is layoutDependsOn and onDependentViewChanged. Children need to depend on dependency. When dependency View changes, onDependentViewChanged is called and the child can respond.
The second is on Start Nested Scroll and other nested sliding processes. First, the onStart Nested Scroll method is used to determine whether the vertical sliding is true or not, and then the effect is achieved in onNested PreScroll, onNested Scroll and other methods.
Because the first way will result in the child having to rely on a particular View, which leads to less flexibility, this paper uses the second way of implementation.

Specific realization

Before the nested sliding starts, you can determine whether the sliding is vertical or not and do some initialization work, such as getting the initial coordinates of the child View.

    //Judging vertical slip
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        if (isInit) {// Setting tags to prevent parent and child coordinate changes caused by new Anim
            mCommonAnim = new LTitleBehaviorAnim(child);
            isInit = false;
        }
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

Before triggering nested slides, you can determine some sliding gestures here, as well as the consumption of the parent layout. Because judging by dy in this method, when RecyclerView entries are few, logic code will also be triggered, so this paper only does some custom operations for animation in this method.

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        if (mCommonAnim != null) {
            mCommonAnim.setDuration(mDuration);
            mCommonAnim.setInterpolator(mInterpolator);
        }
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

The method of triggering when sliding nested scroll is to take Title(Toolbar) as an example. If sliding upward, the Toolbar is hidden and displayed conversely.

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed < 0) {
            if (isHide) {
                mCommonAnim.show();
                isHide = false;
            }
        } else if (dyConsumed > 0) {
            if (!isHide) {
                mCommonAnim.hide();
                isHide = true;
            }
        }
    }

Realization and Individualization of Imitated Effect Animation

Everyone knows that all kinds of animations on the client are very elegant, and blogs copying their animations on the Internet are also emerging one after another, which used the free time before. Dry Cargo Concentration Camp Client Suddenly I thought of using the knowledgeable homepage effect, and then I picked up the keyboard, copy and paste it.
It's a joke, in fact, it's relatively easy to roughly achieve the effect. Here we mainly share the ideas of implementation and the details that need to be paid attention to.

First of all, the general process is as described in the above several methods. The implementation of animation effect is very simple. Here, take the display and hiding of BottomView as an example to directly code.

    public LBottomBehaviorAnim(View bottomView) {
        mBottomView = bottomView;
        mOriginalY = mBottomView.getY();//Because the Y value changes with the animation, the initial coordinates are recorded before the nested sliding starts.
    }

    @Override
    public void show() {//display
        ValueAnimator animator = ValueAnimator.ofFloat(mBottomView.getY(), mOriginalY);
        animator.setDuration(getDuration());
        animator.setInterpolator(getInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mBottomView.setY((Float) valueAnimator.getAnimatedValue());
            }
        });
        animator.start();
    }

    @Override
    public void hide() {//hide
        ValueAnimator animator = ValueAnimator.ofFloat(mBottomView.getY(), mOriginalY + mBottomView.getHeight());
        animator.setDuration(getDuration());
        animator.setInterpolator(getInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mBottomView.setY((Float) valueAnimator.getAnimatedValue());
            }
        });
        animator.start();
    }

In fact, the whole process is over, but it can not achieve the desired results. Open the client again and slide at a very slow speed, then you will find that there is no trigger animation, OK, first record the problem; then slide down at a very slow speed, and suddenly trigger the animation again. Overall, knowing animation has a layered nesting effect.

Firstly, the first problem is solved by adding only one line of code, that is, the dynamic Consumed distance is larger than a certain value before sliding is allowed.

    if(Math.abs(dyConsumed) > minScrollY){
        ...//Logic code inside onNestedScroll
    }

For the second question, I began to think about whether to trigger animation by judging the sliding distance of RecyclerView. In fact, the idea is correct, but we can no longer implement a series of methods of addOnScrollListener. At this time, think about nested sliding. DyConsumed is not the distance recycler View consumes. Think of it here, it's very good to achieve. Just add dyConsumed together, and add more than a certain value, trigger animation. The code is very simple, combined with the first problem, the effect of knowing is achieved.

    mTotalScrollY += dyConsumed;//Distance of cumulative consumption
    if (Math.abs(dyConsumed) > minScrollY || Math.abs(mTotalScrollY) > scrollYDistance) {
        ...//Logic code inside onNestedScroll
        mTotalScrollY = 0;//Reset after animation execution
    }

Next, we can customize some property values. The first step is to get the Behavior object.

    public static CommonBehavior from(View view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
            throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
        }
        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
        if (!(behavior instanceof CommonBehavior)) {
            throw new IllegalArgumentException("The view's behavior isn't an instance of CommonBehavior. Try to check the [app:layout_behavior]");
        }
        return (CommonBehavior) behavior;
    }

Then you can set the properties of the object:

    public CommonBehavior setDuration(int duration) {
        mDuration = duration;
        return this;
    }

    public CommonBehavior setInterpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
        return this;
    }

    public CommonBehavior setMinScrollY(int minScrollY) {
        this.minScrollY = minScrollY;
        return this;
    }

    public CommonBehavior setScrollYDistance(int scrollYDistance) {
        this.scrollYDistance = scrollYDistance;
        return this;
    }

So far, the whole process has been implemented, and other TitleView and suspension button animations are similar rules. I set up the Common class for Behavior and animation to eliminate some duplicate code, which is not posted here. Specific reference My Github

Animation has been implemented, but the pits seem to never be filled in when writing code.

When I use the animation, I find a problem. Because Coordinator Layout is the root layout, the item at the top of Recycler View is obscured by toolbar.
Let's take another look at Zhiyou, slip a little distance, and find that the place covered by his top Toolbar is actually blank. We can find that this problem actually exists, but people handle it well, so users will not find it basically.
However, this problem can still be solved, for example, when judging item as the first, you can add a View Fill, personal use of custom ItemDecoration, judgment if the first item, outRect.set(0, titleHeight, 0, 0), set the size of titleHeight. Bottom View is the same, and there are many solutions.

Another problem I found when writing demo was that I used Linear Layout as Bottom View and found that the floating button actually performed various animations in the upper layer of Linear Layout, which seemed incongruous. Later, it was found that if the elevation of Floating Action Button was larger than that of Bottom View, the Floating Action Button animation covered Bott. The omView is at the top, and vice versa at the bottom. I haven't noticed it before.

In addition, the Bottom View will automatically display when the known Recycler View slides to the bottom. Personally, I think it can be processed in onStop Nested Scroll by judging whether the Recycler View slides to the bottom. This article does not specifically implement it, but provides ideas.

This is how the whole process is implemented, encapsulated, and then extracted and submitted to Github.

    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

    dependencies {
        compile 'com.github.Lauzy:LBehavior:1.0.1'
    }

It's easy to use.

parameter Explain
@string/title_view_behavior Top Title Bar
@string/bottom_view_behavior Bottom navigation bar
@string/fab_scale_behavior Floating button (zoom)
@string/fab_vertical_behavior Floating button (sliding up and down)

Customization (with default values, not used):

Method parameter Explain
setMinScrollY int y Set the minimum sliding distance to trigger the animation, such as setMinScrollY(10) sliding 10 pixels to trigger the animation, default is 5.
setScrollYDistance int y Set the sliding distance of trigger animation to prevent the user from sliding slowly and the minimum sliding distance is always less than setMinScrollY. If this value is set to 100, the user can trigger animation even if the sliding distance is 100. The default is 40.
setDuration int duration Set the animation duration. The default is 400 ms.
setInterpolator Interpolator interpolator Set up the animation interpolator to modify the animation effect. The default mode is LinearOutSlowIn Interpolator. Interpolator Official Documents
    CommonBehavior.from(mFloatingActionButton)
        .setMinScrollY(20)
        .setScrollYDistance(100)
        .setDuration(1000)
        .setInterpolator(new LinearOutSlowInInterpolator());

Finally, please don't stint on star. My Github .

Keywords: Android github less Maven

Added by thepriest on Sun, 07 Jul 2019 22:24:51 +0300