Xu Gong, programmer of my public account, has four years of working experience in Dachang. He replies to the dark knight and gets a copy of Android learning video. He replies to Xu Gong 666, where he can get my carefully organized resume template and lead you to Dachang.

You can use CoordinatorLayout to create all kinds of cool things

Custom Behavior – Mimicking Zhihu, FloatActionButton hidden and displayed

NestedScrolling mechanism for in-depth parsing

You can read the source code for CoordinatorLayout step by step

Custom Behavior – implementation of the discovery page imitating Sina Weibo

ViewPager, ScrollView nested ViewPager sliding conflict resolved

Custom behavior – perfect imitation QQ browser home page, Meituan business details page

preface

I remember that two years ago, I wrote an article about custom behavior. The realization of custom behavior – imitating the discovery page of Sina Weibo has been read more than 10,000 times until now.

Today, the behavior is updated to include the following features over the behavior of two years ago

  • Add listening callback in cascade sliding process, convenient external according to the sliding distance, the corresponding animation, show cool UI, through setPagerStateListener set callback listening
  • While sliding to the top, you can set whether you can slide the Head down using setCouldScroollOpen
  • Add the Fling callback to slide the Content list using setOnHeaderFlingListener
  • HeaderBehavior, ContentBehavior code optimization, and business logic separation, easy reuse.

Directions for use

rendering

Let’s first take a look at the effect of sina Weibo discovery page:

Let’s take a look at what we did two years ago with Sina Weibo

Imitation QQ browser

Imitation of Meituan merchant details page:

Analysis:

There are two states, open and close.

  • The open state is when Tab+ViewPager has not been swiped to the top and the header has not been completely removed from the screen
  • The close state is when Tab+ViewPager slides to the top and the Header is removed from the screen

From the renderings, we can see that in the open state, when we slide up the RecyclerView inside the ViewPager, RecyclerView will not move up (the sliding event of RecyclerView is handed to the external container for processing, The entire layout (Header + Tab +ViewPager) is offset upwards. When Tab slides to the top, the RecyclerView inside the ViewPager slides up, and the RecyclerView slides up normally, that is, the external container does not intercept the sliding event.

In open state, we set the SwipeRefreshLayout setEnabled to false so that events will not be blocked. When the page is closed, set the SwipeRefreshLayout setEnabled to TRUE to support the pull-down refresh.

Based on the above analysis, we can divide the whole effect into three parts here

The first part of the Header: When the Header has not been slid to the top (i.e., when open), follow the finger to slide the second part of the Content: When we slide up, when the Header is open, the Header slides up, the recyclerView of the Content part will not slide, when the Header is close, the content part slides up, RecyclerView slides up. When we slide down, the header will not slide, it will only slide the Content part of recyclerView 3 search: When we slide up, the search part will slide, and eventually stay in a fixed position.

We define this three-part relationship to mean that Content depends on headers. When the Header moves, the Content moves with it. Therefore, when dealing with sliding events, we only need to deal with the Behavior of the Header part, and the Behavior of the Content part does not need to deal with sliding events. We only need to rely on the Header and move accordingly. The behavior in the Search part also doesn’t need to handle the sliding event. It just relies on the Header and moves accordingly.

As for the specific implementation, you can see the implementation of custom behavior-imitating Sina Weibo discovery page. The core idea is similar, and it will not be repeated here.

Directions for use

Here we have simulated QQ browser demo to illustrate:

Let’s take a look at how to use it: In a nutshell, there are only two steps:

  1. The first step is to specify our corresponding behaviors in the header and content sections respectively in the XML file
  2. In the second part, set some configuration parameters in the code

Step 1: Write the XML file and specify the appropriate behavior


      
<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:background="@android:color/holo_blue_light"
    android:fitsSystemWindows="true">

    <! -- Header -->
    <FrameLayout
        android:id="@+id/id_uc_news_header_pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/behavior_qq_browser_header_pager">


        <com.xj.qqbroswer.behavior.base.NestedLinearLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/header_height"
            android:orientation="vertical">

            <TextView
                android:id="@+id/news_tv_header_pager"
                style="@style/TextAppearance.AppCompat.Title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical"
                android:gravity="center"
                android:text="QQBrowser Header"
                android:textColor="@android:color/white" />


        </com.xj.qqbroswer.behavior.base.NestedLinearLayout>
    </FrameLayout>

    <! ContentProvide -->
    <LinearLayout
        android:id="@+id/behavior_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_behavior="@string/behavior_contents">

        <android.support.design.widget.TabLayout
            android:id="@+id/id_uc_news_tab"
            android:layout_width="match_parent"
            android:layout_height="@dimen/tabs_height"
            android:background="@color/colorPrimary"
            app:tabGravity="fill"
            app:tabIndicatorColor="@color/colorPrimaryLight"
            app:tabSelectedTextColor="@color/colorPrimaryLight"
            app:tabTextColor="@color/colorPrimaryIcons" />

        <android.support.v4.view.ViewPager
            android:id="@+id/id_uc_news_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#F0F4C3">

        </android.support.v4.view.ViewPager>
    </LinearLayout>

    <! Part - search - >
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/header_title_height"
        app:layout_behavior="@string/behavior_search">

        <android.support.v7.widget.SearchView
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="50dp"
            android:background="@android:color/white"
            app:defaultQueryHint="Search"
            app:queryHint="Search">

        </android.support.v7.widget.SearchView>

        <android.support.v7.widget.AppCompatImageView
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            android:src="@mipmap/camera"
            android:tint="@android:color/white" />

    </RelativeLayout>


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

Copy the code

Step 2: Dynamically set some parameters in the code

private void initBehavior(a) {
    Resources resources = DemoApplication.getAppContext().getResources();
    mHeaderBehavior = (QQBrowserHeaderBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.id_uc_news_header_pager).getLayoutParams()).getBehavior();
    mHeaderBehavior.setPagerStateListener(new QQBrowserHeaderBehavior.OnPagerStateListener() {
        @Override
        public void onPagerClosed(a) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "onPagerClosed: ");
            }
            Snackbar.make(mNewsPager, "pager closed", Snackbar.LENGTH_SHORT).show();
            setFragmentRefreshEnabled(true);
            setViewPagerScrollEnable(mNewsPager, true);
        }

        @Override
        public void onScrollChange(boolean isUp, int dy, int type) {}@Override
        public void onPagerOpened(a) {
            Snackbar.make(mNewsPager, "pager opened", Snackbar.LENGTH_SHORT).show();
            setFragmentRefreshEnabled(false); }});// Set it to the negative of header height
    mHeaderBehavior.setHeaderOffsetRange(-resources.getDimensionPixelOffset(R.dimen.header_height));
    // Set header close to open by sliding
    mHeaderBehavior.setCouldScroollOpen(false);
    
    mContentBehavior = (QQBrowserContentBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.behavior_content).getLayoutParams()).getBehavior();
    // Set the id to depend on. In this case, set it to Header Layout ID
    mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager);
    // Sets the final resting position of the Content section
    mContentBehavior.setFinalY(resources.getDimensionPixelOffset(R.dimen.header_title_height));
}

Copy the code

MHeaderBehavior setHeaderOffsetRange set the Header part of the offset, we are implemented through translationY, so we usually opposite can be set to the Header height. MHeaderBehavior. SetCouldScroollOpen (false), set the header when the close whether can open by sliding.

mContentBehavior.setDependsLayoutId(R.id.id_uc_news_header_pager); Set which ID to rely on, in this case the Header Layout ID. MContentBehavior. SetFinalY sets the position of the content part eventually stop.

Let’s look at the OnPagerStateListener callback

/** * callback for HeaderPager 's state */
public interface OnPagerStateListener {
    /** * do callback when pager closed */
    void onPagerClosed();

    /**
     * when scrooll, it would call back
     *
     * @param isUp  isScroollUp
     * @param dy   child.getTanslationY
     * @param type touch or not touch, TYPE_TOUCH, TYPE_NON_TOUCH
     */
    void onScrollChange(boolean isUp, int dy, @ViewCompat.NestedScrollType int type);

    /** * do callback when pager opened */
    void onPagerOpened();
}

Copy the code

There are three main methods. The first method, onPagerClosed, will call back when the header is close, and the second method, onScrollChange, will call back when the header slide distance changes. It has three parameters: isUp for sliding up, dy for header offset, and type for touch or non-touch.

If you want to do something really cool, you can animate each View in the onScrollChange method, depending on how far it slides.

Copy meitu business details page

The steps are the same as the steps of the imitation QQ browser above, and the same steps are not repeated here. Several key points are said: first: The page header when close, we can through the slide open the header, this is by calling mHeaderBehavior. SetCouldScroollOpen (true); The implementation. The second: When you slide the header to fling, you can see that the recyclerView of the Content part also slides. This is done by the Fling event of the header. On the onFlingStart manually call RecyclerView smoothScrollBy to slide.

mHeaderBehavior.setOnHeaderFlingListener(new HeaderFlingRunnable.OnHeaderFlingListener() {
    @Override
    public void onFlingFinish() {

    }

    @Override
    public void onFlingStart(View child, View target, float velocityX, float velocityY) {
        Log.i(TAG, "onFlingStart: velocityY =" + velocityY);
        if (velocityY < 0) {
            mRecyclerView.smoothScrollBy(0, (int) Math.abs(velocityY), new AccelerateDecelerateInterpolator());
        }

    }

    @Override
    public void onHeaderClose() {

    }

    @Override
    public void onHeaderOpen(){}});Copy the code

Run into the pit of

The header section cannot respond to the slide event

We customize a NestedLinearLayout, rewrite its onTouchEvent, and send events to NestedScrollingParent via the NestedScrolling mechanism. That’s CoordinatorLayout, and the NestedScrollingParent gives it to the behavior of the child View.

@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    final int action = MotionEventCompat.getActionMasked(event);
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
                    | ViewCompat.SCROLL_AXIS_VERTICAL);

            break;
        case MotionEvent.ACTION_MOVE:
            int dy = (int) (event.getRawY() - lastY);
            lastY = (int) event.getRawY();
            // dy < 0 slide up, dy>0 pull down
            if (dy < 0) { // The parent class handles the slide
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // If a parent class that supports nested scrolling is found
                        && dispatchNestedPreScroll(0, -dy, consumed, offset)) {//
                    // The parent class does some scrolling}}else {
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // If a parent class that supports nested scrolling is found
                        && dispatchNestedScroll(0.0.0, -dy, offset)) {//
                    // The parent class does some scrolling}}break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            stopNestedScroll();
            break;

    }
    return super.onTouchEvent(event);
}


Copy the code

When we set the click event to the header’s child View, we can’t slide the header

If you’re familiar with the Android event distribution mechanism, you know that the default event delivery mechanism in Android is like this,

When a TouchEvent occurs, the Activity first passes the TouchEvent to the topmost View, and the TouchEvent first reaches the dispatchTouchEvent of the topmost View. It is then distributed by the dispatchTouchEvent method.

  • If dispatchTouchEvent returns true consumption event, the event is terminated.

  • If dispatchTouchEvent returns false, the onTouchEvent handler is passed back to the parent View;

    The onTouchEvent event returns true, the event terminates, returns false, and is handled by the parent View’s onTouchEvent method

  • If dispatchTouchEvent returns super, it calls its own onInterceptTouchEvent method by default

    By default, the interceptTouchEvent calls the super method, which by default returns false, If the interceptTouchEvent returns true, it is intercepted, If the interceptTouchEvent returns false, it is passed to the child view, whose dispatchTouchEvent starts the event distribution.

So, when we set a click event for the child View, the default parent does not block the event and goes to the child View’s onToucheEvent event, which is consumed because of the click event. So the ACTION_MOVE event in the parent View onTouchEvent is not called back.

Solutions: Rewrite the onInterceptToucheEvent of NestedLinearLayout to return true when it is an ACTION_MOVE event, which will call its own onTouchEvent event to ensure that it can slide.

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDownY = (int) event.getRawY();
            // Tell the parent view when it starts sliding
            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
                    | ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        case MotionEvent.ACTION_MOVE:
            // Make sure that ACTION_DOWN events are not consumed
            if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {
                logD("onInterceptTouchEvent: ACTION_MOVE mScaledTouchSlop =" + mScaledTouchSlop);
                return true; }}return super.onInterceptTouchEvent(event);
}


Copy the code

A click event triggers ACTION_DOWN, ACTION_MOVE, and ACTION_UP. If we return true in ACTION_MOVE, it will invalidate the onClick event of the child View.

Solutions:

final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mScaledTouchSlop = configuration.getScaledTouchSlop();
if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {
    return true; }Copy the code

For slide resolution, see my previous blog :ViewPager, ScrollView nested ViewPager slide resolution

How do I determine whether the header is a Fling action

We do this with the gesture processor GestureDetector, but you can also do it with VelocityTracker, which is just a little bit more cumbersome

public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
}

        GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {

            @Override
            public boolean onDown(MotionEvent e) {
                return false; } -- -- -- -- --// Omit some code

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "onFling: velocityY =" + velocityY);
// fling((int) velocityY);
                getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
                return false; }}; mGestureDetector =new GestureDetector(getContext(), onGestureListener);



Copy the code

digression

Sometimes, it’s really important to take notes.

This time I write this blog, because I want to do a similar effect in the project. At first, I didn’t have any ideas. But I remember clearly that I wrote a similar article two years ago, and the specific implementation principle has long been forgotten. I looked at blogs from two years ago, did some brainstorming, moved the code into the project, and found a few bugs. Tinker around and fill in the holes.

Think about it, if the principle had not been written down, this effect is really difficult to achieve. If you’re not familiar with Coordinatorlayout, behavior,NestedScroll, you can’t do it. Two years ago, WHEN I wrote this blog, I received a lot of private letters. Some feedback said that they could not achieve this effect after more than two weeks. Thank you very much for writing this blog. So try taking more notes from now on. Really, a good memory is better than a bad pen.

The second thing that struck me was, at first, I looked at the code THAT I had written two years earlier, and my initial reaction was, oh, shit. Indeed, many places are poorly written. Behavior coupled business logic is difficult to reuse and maintain. So, this time, I’m going to pull behavior out in my free time, and then I’m going to achieve a similar effect in the future, easily. Biu biu biu.

With all that said, here’s the summary

  • Take notes when you don’t, especially when it comes to principles
  • Be in awe of the code, don’t say much, and learn it yourself
  • Keep a humble heart

CoordinatorLayoutExample

If you think the effect is good, you can scan and follow my wechat official account, or go to star on my Github, thank you

Recommended reading

How should a programmer write his resume, a 5 years big factory elder brother talk to you

My 5 years of Android learning road, those years together stepped on the pit

  1. Public number programmer Xu Gong reply dark horse, get Android learning video
  2. Public number programmer Xu Gong replied xu Gong 666, get resume template, teach you how to optimize your resume, into the factory
  3. Public number programmer Xu Gong replied to the interview, you can get the interview common algorithm, sword refers to the offer question solution
  4. Public number programmer Xu Gong replies horse soldier, can obtain horse soldier study video