Nested series navigation

  • 1. Basic analysis of NestedScrolling nested sliding mechanism
  • 2. Brief analysis of NestedScrolling practice of nested sliding mechanism – imitation writing ele. me business details page
  • 3. Analyze the NestedScrolling nested CoordinatorLayout sliding mechanism. The behaviors
  • 4. Brief analysis of NestedScrolling practice – custom Behavior implementation of Xiaomi music singer details

This article has been originally published on the public account Hongyang. Without permission, shall not be reproduced in any form!

An overview of the

Before the NestedScrolling nested CoordinatorLayout sliding mechanism is analysed. The behaviors of belt you know CoordinatorLayout. The behaviors of the principle and basic use, This article hand-in-hand based on the implementation of a custom Behavior xiaomi Music singer details page. Making address: github.com/pengguanmin…

Results the preview

github
Baidu Cloud channel password: FU5s

Effect analysis

topBarHeight;/ / topBar height
contentTransY;// Slide content to initialize TransY
downEndY;// The maximum value of the content dropThe content part of the sliding range = [topBarHeight contentTransY] the content part of the decline range = [contentTransY downEndY]Copy the code

Code implementation

layout

Here are layout points, focusing on the size and location of the controls. For a complete layout, see activity_main.xml

<?xml version="1.0" encoding="utf-8"? >
<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <! Part - face -- -- >
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="com.pengguanming.mimusicbehavior.behavior.FaceBehavior">

        <ImageView
            android:id="@+id/iv_face"
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/jj"
            android:tag="iv_face"
            android:translationY="@dimen/face_trans_y" />

        <View
            android:id="@+id/v_mask"
            android:layout_width="match_parent"
            android:layout_height="500dp" />
    </FrameLayout>
    <! Part - face -- -- >

    <! Part -- TopBar -- -- >
    <com.pengguanming.mimusicbehavior.widget.TopBarLayout
        android:id="@+id/cl_top_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/top_bar_height"
        app:layout_behavior="com.pengguanming.mimusicbehavior.behavior.TopBarBehavior">

        <ImageView
            android:id="@+id/iv_back"
            . />

        <TextView
            android:id="@+id/tv_top_bar_name"
            a... />

        <com.pengguanming.mimusicbehavior.widget.DrawableLeftTextView
            android:id="@+id/tv_top_bar_coll"
            ./>
    </com.pengguanming.mimusicbehavior.widget.TopBarLayout>
    <! Part -- TopBar -- -- >

    <! - the TitleBar part -- -- >
    <android.support.constraint.ConstraintLayout
        android:id="@+id/cls_title_bar"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        app:layout_behavior="com.pengguanming.mimusicbehavior.behavior.TitleBarBehavior">

        <TextView
            ./>

        <com.pengguanming.mimusicbehavior.widget.DrawableLeftTextView
           ./>
    </android.support.constraint.ConstraintLayout>
    <! - the TitleBar part -- -- >

    <! - the Content part -- -- >
    <LinearLayout
        android:id="@+id/ll_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:translationY="@dimen/content_trans_y"
        app:layout_behavior="com.pengguanming.mimusicbehavior.behavior.ContentBehavior">

        <com.flyco.tablayout.SlidingTabLayout
            android:id="@+id/stl"
            ./>

        <android.support.v4.view.ViewPager
            android:id="@+id/vp"
            . />
    </LinearLayout>
    <! - the Content part -- -- >
</android.support.design.widget.CoordinatorLayout>
Copy the code

ContentBehavior

This Behavior deals mainly with Measure, nested sliding of the Content section.

Bind the View that needs to do the effect, introduce Dimens, and measure the height of the Content section

As can be seen from the above picture: in the folded state, the height of the Content part = the height of the full screen – the height of the TopBar part

public class ContentBehavior extends CoordinatorLayout.Behavior{
    private int topBarHeight;//topBar content height
    private float contentTransY;// Slide content to initialize TransY
    private float downEndY;// End value of slide
    private View mLlContent;/ / the Content section

    public ContentBehavior(Context context) {
        this(context, null);
    }

    public ContentBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Introduce size values
        int resourceId = context.getResources().getIdentifier("status_bar_height"."dimen"."android");
        int statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        topBarHeight= (int) context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;
        contentTransY= (int) context.getResources().getDimension(R.dimen.content_trans_y);
        downEndY= (int) context.getResources().getDimension(R.dimen.content_trans_down_end_y); . }@Override
    public boolean onMeasureChild(@NonNull CoordinatorLayout parent, View child,
                                  int parentWidthMeasureSpec, int widthUsed, 
                                  int parentHeightMeasureSpec,int heightUsed) {
        final int childLpHeight = child.getLayoutParams().height;
        if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
            For the CoordinatorLayout, the measurement specifications are obtained. If the height is not specified, the Height is used
            int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
            if (availableHeight == 0) {
                availableHeight = parent.getHeight();
            }
            // Set the height of the Content section
            final int height = availableHeight - topBarHeight;
            final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
                    childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                            ? View.MeasureSpec.EXACTLY
                            : View.MeasureSpec.AT_MOST);
            // Perform the measurement of the specified height, and return true to proxy the subview using Behavior
            parent.onMeasureChild(child, parentWidthMeasureSpec,
                    widthUsed, heightMeasureSpec, heightUsed);
            return true;
        }
        return false;
    }

    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
        boolean handleLayout = super.onLayoutChild(parent, child, layoutDirection);
        // Bind the Content View
        mLlContent=child;
        returnhandleLayout; }}Copy the code

Realistic NestedScrollingParent2 interface

onStartNestedScroll()

The ContentBehavior only handles vertical sliding of the slideable View within the Content section.

    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                       @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        // Only vertical sliding of content View is accepted
        return directTargetChild.getId() == R.id.ll_content
                &&axes== ViewCompat.SCROLL_AXIS_VERTICAL;
    }
Copy the code

onNestedPreScroll()

The next step is to handle sliding, as mentioned in the effect analysis above:

The Content part of the sliding range = [topBarHeight contentTransY], the decline range = [contentTransY downEndY] the sliding range for [topBarHeight downEndY]; ElemeNestedScrollLayout controls the TransitionY value of the Content section to be in range as shown below. In the Content section, you can slide View up:

  • 1, If the current Content part of the TransitionY+View slide dy > topBarHegiht, Set the Content section’s TransitionY to the Content section’s current TransitionY+ the View’s sliding dy to consume the View’s dy.
  • 2. If the dy = topBarHegiht of the current Content section TransitionY+View slide, do the same.
  • 3, If the Content part of the current TransitionY+View slide dy < topBarHegiht, only part of the dy is consumed (i.e. the Content part of the current TransitionY+View slide dy to topBarHeight difference), the rest of the DY is consumed by the View slide.

In the Content section, you can slide the View to the top and the View can’t slide to the top.

  • 1, If the Content part of the current TransitionY+View slide dy >= topBarHeight and Content part of the current TransitionY+View slide dy <= downEndY, Set the Content section’s TransitionY to the Content section’s current TransitionY+ the View’s sliding dy to consume the View’s dy.
  • 2, the Content part of the current TransitionY+View slide dy > DownEndY, consumes only part of dy(the Content part’s current TransitionY to downEndY difference) and stops View scrolling for NestedScrollingChild2.
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                  @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        float transY = child.getTranslationY() - dy;

        // Handle slippage
        if (dy > 0) {
            if (transY >= topBarHeight) {
                translationByConsume(child, transY, consumed, dy);
            } else{ translationByConsume(child, topBarHeight, consumed, (child.getTranslationY() - topBarHeight)); }}if (dy < 0 && !target.canScrollVertically(-1)) {
            // Handle the slide
            if (transY >= topBarHeight && transY <= downEndY) {
                translationByConsume(child, transY, consumed, dy);
            } else{ translationByConsume(child, downEndY, consumed, (downEndY-child.getTranslationY())); stopViewScroll(target); }}}private void stopViewScroll(View target){
        if (target instanceof RecyclerView) {
            ((RecyclerView) target).stopScroll();
        }
        if (target instanceof NestedScrollView) {
            try {
                Class<? extends NestedScrollView> clazz = ((NestedScrollView) target).getClass();
                Field mScroller = clazz.getDeclaredField("mScroller");
                mScroller.setAccessible(true);
                OverScroller overScroller = (OverScroller) mScroller.get(target);
                overScroller.abortAnimation();
            } catch(NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }}}private void translationByConsume(View view, float translationY, int[] consumed, float consumedDy) {
        consumed[1] = (int) consumedDy;
        view.setTranslationY(translationY);
    }
Copy the code

onStopNestedScroll()

In the sliding Content section, during the transition from the initial state to the expanded state, releasing the hand will execute the folded animation. This logic will be implemented in onStopNestedScroll(), but note that if the animation is not finished and the finger slides down again, The currently executing animation should be cancelled in onNestedScrollAccepted().

    private static final long ANIM_DURATION_FRACTION = 200L;
    private ValueAnimator restoreAnimator;// The animation executed when the content is folded up

    public ContentBehavior(Context context, AttributeSet attrs) {... restoreAnimator =new ValueAnimator();
        restoreAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                translation(mLlContent, (float) animation.getAnimatedValue()); }}); }public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                       @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        if(restoreAnimator.isStarted()) { restoreAnimator.cancel(); }}public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) {
        // The collapse animation is triggered if the transition from the initial state to the expanded state is performed
        if(child.getTranslationY() > contentTransY) { restore(); }}private void restore(a){
        if (restoreAnimator.isStarted()) {
            restoreAnimator.cancel();
            restoreAnimator.removeAllListeners();
        }
        restoreAnimator.setFloatValues(mLlContent.getTranslationY(), contentTransY);
        restoreAnimator.setDuration(ANIM_DURATION_FRACTION);
        restoreAnimator.start();
    }

    private void translation(View view, float translationY) {
        view.setTranslationY(translationY);
    }
Copy the code

Handle inertial slip

Scenario 1: Sliding the Content slideable View up quickly creates an inertial slide, which is exactly the same as the onNestedPreScroll() slide up, so the logic can be reused.

Scenario 2: A quick slide from initialization to expansion is exactly the same as the previous slide from onNestedPreScroll(), so the logic can be reused.

Scenario 3: Rapid slide from the collapsed state to the initialization state, as shown below, looks like the effect of a rapid slide pause.

private boolean flingFromCollaps=false;// Whether the fling occurs from the folded state

public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, 
                                    @NonNull View target, float velocityX, float velocityY) {
        flingFromCollaps=(child.getTranslationY()<=contentTransY);
        return false;
    }

public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                  @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        floattransY = child.getTranslationY() - dy; .if (dy < 0 && !target.canScrollVertically(-1)) {
            Scroll down Recycler(or NestedScrollView) to contentTransY stop scrolling
            if (type == ViewCompat.TYPE_NON_TOUCH&&transY >= contentTransY&&flingFromCollaps) {
                flingFromCollaps=false;
                translationByConsume(child, contentTransY, consumed, dy);
                stopViewScroll(target);
                return; }... }}Copy the code

Release resources

When the ContentBehavior is removed, the action to stop the animation and release the listener is performed.

    public void onDetachedFromLayoutParams(a) {
        if (restoreAnimator.isStarted()) {
            restoreAnimator.cancel();
            restoreAnimator.removeAllUpdateListeners();
            restoreAnimator.removeAllListeners();
            restoreAnimator = null;
        }
        super.onDetachedFromLayoutParams();
    }
Copy the code

FaceBehavior

This Behavior mainly deals with the displacement of the ImageView in the Face part and the change of the transparency of the mask. Here, for reasons of space, only the key method is explained. See FaceBehavior.class for the specific source code

public class FaceBehavior extends CoordinatorLayout.Behavior {
    private int topBarHeight;//topBar content height
    private float contentTransY;// Slide content to initialize TransY
    private float downEndY;// End value of slide
    private float faceTransY;// The upward displacement of the image

    public FaceBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Introduce size values
        int resourceId = context.getResources().getIdentifier("status_bar_height"."dimen"."android");
        int statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        topBarHeight= (int) context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;
        contentTransY= (int) context.getResources().getDimension(R.dimen.content_trans_y);
        downEndY= (int) context.getResources().getDimension(R.dimen.content_trans_down_end_y); faceTransY= context.getResources().getDimension(R.dimen.face_trans_y); . }public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        // Rely on Content View
        return dependency.getId() == R.id.ll_content;
    }

    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        // Calculate the slippage percentage of Content
        float upPro = (contentTransY- MathUtils.clamp(dependency.getTranslationY(), topBarHeight, contentTransY)) / (contentTransY - topBarHeight);
        float downPro = (downEndY- MathUtils.clamp(dependency.getTranslationY(), contentTransY, downEndY)) / (downEndY - contentTransY);

        ImageView iamgeview = child.findViewById(R.id.iv_face);
        View maskView =  child.findViewById(R.id.v_mask);

        if (dependency.getTranslationY()>=contentTransY){
            // Move the image TransitionY according to the Content slide percentage
            iamgeview.setTranslationY(downPro*faceTransY);
        }else {
            // Move the image TransitionY based on the Content slide percentage
            iamgeview.setTranslationY(faceTransY+4*upPro*faceTransY);
        }
        // Set the opacity of the image and mask according to the Content slide percentage
        iamgeview.setAlpha(1-upPro);
        maskView.setAlpha(upPro);
        // Return true because the position of child was changed
        return true; }}Copy the code

The logic is very simple: layoutDependsOn() and onDependentViewChanged() to calculate the percentage of the Content moving up and down to handle the shift and transparency of the image and mask.

TopBarBehavior

This Behavior mainly deals with the transparency changes of the TopBar’s two child views, but I won’t go into details because the logic is very similar to FaceBehavior.

public class TopBarBehavior extends CoordinatorLayout.Behavior {
    private float contentTransY;// Slide content to initialize TransY
    private int topBarHeight;//topBar content height.public TopBarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Introduce size values
        contentTransY= (int) context.getResources().getDimension(R.dimen.content_trans_y);
        int resourceId = context.getResources().getIdentifier("status_bar_height"."dimen"."android");
        int statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        topBarHeight= (int) context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;
    }

    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        / / rely on the Content
        return dependency.getId() == R.id.ll_content;
    }

    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        // Calculate the percentage of Content slide and set the transparency of the child view
        float upPro = (contentTransY- MathUtils.clamp(dependency.getTranslationY(), topBarHeight, contentTransY)) / (contentTransY - topBarHeight);
        View tvName=child.findViewById(R.id.tv_top_bar_name);
        View tvColl=child.findViewById(R.id.tv_top_bar_coll);
        tvName.setAlpha(upPro);
        tvColl.setAlpha(upPro);
        return true; }}Copy the code

TitleBarBehavior

This Behavior deals with changes in the transparency of the TitleBar section in the layout position near the top of the Content and the associated View.

public class TitleBarBehavior extends CoordinatorLayout.Behavior {
    private float contentTransY;// Slide content to initialize TransY
    private int topBarHeight;//topBar content height

    public TitleBarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Introduce size values
        contentTransY= (int) context.getResources().getDimension(R.dimen.content_trans_y);
        int resourceId = context.getResources().getIdentifier("status_bar_height"."dimen"."android");
        int statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        topBarHeight= (int) context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;
    }

    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        / / rely on the content
        return dependency.getId() == R.id.ll_content;
    }

    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        // Adjust the TitleBar layout close to the top of the Content
        adjustPosition(parent, child, dependency);
        // Only the percentage of half of the slider on Content is calculated here
        float start=(contentTransY +topBarHeight)/2;
        float upPro = (contentTransY-MathUtils.clamp(dependency.getTranslationY(), start, contentTransY)) / (contentTransY - start);
        child.setAlpha(1-upPro);
        return true;
    }

    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
        // Find the dependent reference for Content
        List<View> dependencies = parent.getDependencies(child);
        View dependency = null;
        for (View view : dependencies) {
            if (view.getId() == R.id.ll_content) {
                dependency = view;
                break; }}if(dependency ! =null) {
            // Adjust the TitleBar layout close to the top of the Content
            adjustPosition(parent, child, dependency);
            return true;
        } else {
            return false; }}private void adjustPosition(@NonNull CoordinatorLayout parent, @NonNull View child, View dependency) {
        final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        int left = parent.getPaddingLeft() + lp.leftMargin;
        int top = (int) (dependency.getY() - child.getMeasuredHeight() + lp.topMargin);
        int right = child.getMeasuredWidth() + left - parent.getPaddingRight() - lp.rightMargin;
        int bottom = (int) (dependency.getY() - lp.bottomMargin); child.layout(left, top, right, bottom); }}Copy the code

conclusion

Compared with the NestedScrolling mechanism achieved by the custom View, the behaviors are more able to decouple the logic, but have more constraints at the same time. Since MY level is limited, I only provide references for you, hoping to inspire others. If you have any questions to discuss, please leave them in the comments section or contact me.