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
Copy meitu merchant details page:
Analysis:
There are two states, open and close.
1) 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
2) The close state refers to 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:
The first step is to specify our corresponding behaviors in the header and content sections respectively in the XML file
In the second part, set some configuration parameters in the code
Step 1: Write the XML file and specify the appropriate behavior
1 <? The XML version = "1.0" encoding = "utf-8"? > 2<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent" 6 android:background="@android:color/holo_blue_light" 7 android:fitsSystemWindows="true"> 8 9 <! Android :id="@+id/ ID_uc_news_header_pager "12 Android :layout_width="match_parent"13 android:layout_height="wrap_content"14 app:layout_behavior="@string/behavior_qq_browser_header_pager">151617 <com.xj.qqbroswer.behavior.base.NestedLinearLayout18 android:layout_width="match_parent"19 android:layout_height="@dimen/header_height"20 android:orientation="vertical">2122 <TextView23 android:id="@+id/news_tv_header_pager"24 style="@style/TextAppearance.AppCompat.Title"25 android:layout_width="match_parent"26 android:layout_height="match_parent"27 android:layout_gravity="center_vertical"28 android:gravity="center"29 android:text="QQBrowser Header"30 android:textColor="@android:color/white" />313233 </com.xj.qqbroswer.behavior.base.NestedLinearLayout>34 </FrameLayout>3536 <! ContentProvide part -->37 <LinearLayout38 Android: ID ="@+ ID /behavior_content"39 Android: layout_Width ="match_parent"40 android:layout_height="wrap_content"41 android:orientation="vertical"42 app:layout_behavior="@string/behavior_contents">4344 <android.support.design.widget.TabLayout45 android:id="@+id/id_uc_news_tab"46 android:layout_width="match_parent"47 android:layout_height="@dimen/tabs_height"48 android:background="@color/colorPrimary"49 app:tabGravity="fill"50 app:tabIndicatorColor="@color/colorPrimaryLight"51 app:tabSelectedTextColor="@color/colorPrimaryLight"52 app:tabTextColor="@color/colorPrimaryIcons" />5354 <android.support.v4.view.ViewPager55 android:id="@+id/id_uc_news_content"56 android:layout_width="match_parent"57 android:layout_height="match_parent"58 android:background="#F0F4C3">5960 </android.support.v4.view.ViewPager>61 </LinearLayout>6263 <! <RelativeLayout65 Android :layout_width="match_parent"66 android:layout_height="@dimen/header_title_height"67 app:layout_behavior="@string/behavior_search">6869 <android.support.v7.widget.SearchView70 android:layout_width="match_parent"71 android:layout_height="30dp"72 android:layout_centerVertical="true"73 android:layout_marginLeft="10dp"74 android:layout_marginRight="50dp"! [Android Technical people] (HTTP: / / http://upload-images.jianshu.io/upload_images/2050203-bf3eca3c1cf265e4.jpg?imageMogr2/auto-orient/strip%7CimageView 2/2/w/1240)7576 Android :background="@android:color/white"77 app:queryHint=" search </android.support.v7.widget.SearchView>8182 <android.support.v7.widget.AppCompatImageView83 android:layout_width="30dp"84 android:layout_height="30dp"85 android:layout_alignParentRight="true"86 android:layout_centerVertical="true"87 android:layout_marginRight="10dp"88 android:src="@mipmap/camera"89 android:tint="@android:color/white" />9091 </RelativeLayout>929394</android.support.design.widget.CoordinatorLayout>
Copy the code
Step 2: Dynamically set some parameters in the code
1private void initBehavior() { 2 Resources resources = DemoApplication.getAppContext().getResources(); 3 mHeaderBehavior = (QQBrowserHeaderBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.id_uc_news_header_pager).getLayoutParams()).getBehavior(); 4 mHeaderBehavior.setPagerStateListener(new QQBrowserHeaderBehavior.OnPagerStateListener() { 5 @Override 6 public void onPagerClosed() { 7 if (BuildConfig.DEBUG) { 8 Log.d(TAG, "onPagerClosed: "); 9 }10 Snackbar.make(mNewsPager, "pager closed", Snackbar.LENGTH_SHORT).show(); 11 setFragmentRefreshEnabled(true); 12 setViewPagerScrollEnable(mNewsPager, true); 13 }1415 @Override16 public void onScrollChange(boolean isUp, int dy, int type) {1718 }1920 @Override21 public void onPagerOpened() {22 Snackbar.make(mNewsPager, "pager opened", Snackbar.LENGTH_SHORT).show(); 23 setFragmentRefreshEnabled(false); 24}}); 26 // Set the negative of header height to 27 mHeaderBehavior.setHeaderOffsetRange(-resources.getDimensionPixelOffset(R.dimen.header_height)); 28 / / set the header when the close whether can by sliding open 29 mHeaderBehavior setCouldScroollOpen (false); 3031 mContentBehavior = (QQBrowserContentBehavior) ((CoordinatorLayout.LayoutParams) findViewById(R.id.behavior_content).getLayoutParams()).getBehavior(); 32 / / set which depends on the id, it must be set to the Header layout id33 mContentBehavior. SetDependsLayoutId (R.i d.i d_uc_news_header_pager); 34 / / set the content part finally stay. The location of the 35 mContentBehavior setFinalY (resources) getDimensionPixelOffset (R.d imen. Header_title_height)); 36}
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
1/** 2 * callback for HeaderPager 's state 3 */ 4public interface OnPagerStateListener { 5 /** 6 * do callback when pager closed 7 */ 8 void onPagerClosed(); 910 /**11 * when scrooll, it would call back12 *13 * @param isUp isScroollUp14 * @param dy child.getTanslationY15 * @param type touch or not touch, TYPE_TOUCH, TYPE_NON_TOUCH16 */17 void onScrollChange(boolean isUp, int dy, @ViewCompat.NestedScrollType int type); 1819 /**20 * do callback when pager opened21 */22 void onPagerOpened(); 23}
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.
1mHeaderBehavior.setOnHeaderFlingListener(new HeaderFlingRunnable.OnHeaderFlingListener() { 2 @Override 3 public void onFlingFinish() { 4 5 } 6 7 @Override 8 public void onFlingStart(View child, View target, float velocityX, float velocityY) { 9 Log.i(TAG, "onFlingStart: velocityY =" + velocityY); 10 if (velocityY < 0) {11 mRecyclerView.smoothScrollBy(0, (int) Math.abs(velocityY), new AccelerateDecelerateInterpolator()); 12 }1314 }1516 @Override17 public void onHeaderClose() {1819 }2021 @Override22 public void onHeaderOpen() {2324 }25});
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.
1@Override 2public boolean onTouchEvent(MotionEvent event) { 3 mGestureDetector.onTouchEvent(event); 4 final int action = MotionEventCompat.getActionMasked(event); 5 switch (action) { 6 case MotionEvent.ACTION_DOWN: 7 startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL 8 | ViewCompat.SCROLL_AXIS_VERTICAL); 910 break; 11 case MotionEvent.ACTION_MOVE:12 int dy = (int) (event.getRawY() - lastY); 13 lastY = (int) event.getRawY(); 14 // dy < 0 slide up, 16 if (startNestedScroll(viewcompat.scroll_axis_vertical) // If (startNestedScroll(viewcompat.scroll_axis_vertical) // if (startNestedScroll(viewcompat.scroll_axis_vertical) // if (startNestedScroll(ViewCompat dispatchNestedPreScroll(0, -dy, consumed, Else {22 if (startNestedScroll(viewcompat.scroll_axis_vertical) // DispatchNestedScroll (0, 0, 0, -dy, offset) {//24 // the parent is partially scrolling 2526}27}28 break; 29 case MotionEvent.ACTION_CANCEL:30 case MotionEvent.ACTION_UP:31 stopNestedScroll(); 32 break; 3334 }35 return super.onTouchEvent(event); 36}
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.
1) If dispatchTouchEvent returns true consumption event, the event is terminated.
2) 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
3) If dispatchTouchEvent returns super, it calls its own onInterceptTouchEvent method by default
By default, the interceptTouchEvent returns a call to the super method. The super method returns false by default, so it will be handled by the child View’s onDispatchTouchEvent method. If the interceptTouchEvent returns false, it is passed to the child view, whose dispatchTouchEvent initiates 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.
1@Override 2public boolean onInterceptTouchEvent(MotionEvent event) { 3 switch (event.getAction()) { 4 case MotionEvent.ACTION_DOWN: 5 mDownY = (int) event.getRawY(); 6 / / when start to slide, tell the parent view 7 startNestedScroll (ViewCompat. 8 | SCROLL_AXIS_HORIZONTAL ViewCompat. SCROLL_AXIS_VERTICAL); 9 break; 12 if (math.abs (event.getrawy () -mdowny) > mScaledTouchSlop) {13 logD("onInterceptTouchEvent: ACTION_MOVE mScaledTouchSlop =" + mScaledTouchSlop); 14 return true; 15 }16 }17 return super.onInterceptTouchEvent(event); 18}
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:
1 if (Math.abs(event.getRawY() - mDownY) > mScaledTouchSlop) {2 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 3 mScaledTouchSlop = configuration.getScaledTouchSlop(); 4 return true; 5}
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
1public boolean onTouchEvent(MotionEvent event) { 2 mGestureDetector.onTouchEvent(event); 3} 4 5 GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() { 6 7 @Override 8 public boolean onDown(MotionEvent e) { 9 return false; 1314 @override15 Public Boolean onFling(MotionEvent E1, MotionEvent e2, float velocityX, float velocityY) {16 Log.d(TAG, "onFling: velocityY =" + velocityY); 17// fling((int) velocityY); 18 getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); 19 return false; 20}}; 2223 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
1. Take notes when you encounter something you can’t, especially when it comes to principles
Be in awe of the code. Don’t say much
Keep a humble heart
Recommended reading
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
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