Previous articles

[Android] ObservableScrollView ObservableScrollView

[Android] ObservableScrollView — Samples and Basic/Advanced Techniques

[Android] ObservableScrollView — Show/hide the Toolbar

[Android] ObservableScrollView Analysis (4) — Parallax image realization

[Android] ObservableScrollView [Android] ObservableScrollView

[Android] ObservableScrollView Analysis (6) — Elastic whitespace on the Toolbar

[Android] ObservableScrollView Analysis (7) – Using elastic blank layout of images

An overview of the

In the previous chapter, we introduced how to use the open source library ObservableScrollView to implement a variety of rolling effect instances and implementation code, but has not analyzed the implementation process used in a series of ObservablexxxView source code, today we will take a look. ObservableRecyclerView source code in the open source library.

ObservableRecyclerView

ObservableRecyclerView inherits RecyclerView and implements Scrollable interface. At the same time, ObservableRecyclerView also holds four class references, which are: ObservableScrollViewCallbacks, ScrollState, ViewGroup and MotionEvent, including:

ObservableScrollViewCallbacks is open source libraries defined a callback interface;

ScrollState is an enumerated class that represents the three states of sliding: STOP, UP, and DOWN;

ViewGroup is used in Touch event interception process, specify the parent class View;

A MotionEvent object is generated when the user touches the screen, and the MotionEvent parameter is passed in when the onTouchEvent() and onInterceptTouchEvent() methods are overridden.

Scrollable

Interface methods

Let’s start with the Scrollable interface, which is defined by the open source library as an interface for all the next steps needed to implement observable, Scrollable controls (RecyclerView, ScrollView, ListView, WebView and GridView) provide a general application program interface, the interface must achieve the following methods:

  • SetScrollViewCallbacks (ObservableScrollViewCallbacks listener) : set a callback listener
  • AddScrollViewCallbacks (ObservableScrollViewCallbacks listener) : add a callback listener
  • RemoveScrollViewCallbacks (ObservableScrollViewCallbacks listener) : to delete a callback listener
  • ClearScrollViewCallbacks () : Clears all callback listeners
  • ScrollVerticallyTo (int y) : scrollVerticallyTo y (y is absolute)
  • GetCurrentScrollY () : Returns the current y coordinate
  • SetTouchInterceptionViewGroup (ViewGroup ViewGroup) : The ObservableRecyclerView class has a reference to the ViewGroup class. This is why ObservableRecyclerView has a reference to the ViewGroup class.

Summary of Scrollable interface, mainly to complete the rollable control callback interface Settings, increase, delete and clean up the work, but also to achieve the function of scrolling, return the current vertical direction of the coordinate, and the interception of the event processing related Settings.

The specific implementation

In ObservableRecyclerView, how are these methods implemented respectively? Let’s take a look:

@Override public void setScrollViewCallbacks(ObservableScrollViewCallbackslistener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbackslistener) { if (mCallbackCollection == null)  { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbackslistener) { if (mCallbackCollection ! = null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection ! = null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroupviewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { ViewfirstVisibleChild = getChildAt(0); if (firstVisibleChild ! = null) { int baseHeight = firstVisibleChild.getHeight(); int position = y / baseHeight; scrollVerticallyToPosition(position); } } @Override public int getCurrentScrollY() { return mScrollY; }Copy the code

ObservableRecyclerView holds an ArrayList. If multiple callback interfaces need to be added, the added interfaces are put into this container, and the deletion and cleanup operations are based on this container. Pay attention to the scrollVerticallyTo () method of implementation, need to combine scrollVerticallyToPosition () method to read together:

public void scrollVerticallyToPosition(int position) {
    LayoutManagerlm = getLayoutManager();
 
    if (lm != null && lminstanceof LinearLayoutManager) {
      ((LinearLayoutManager) lm).scrollToPositionWithOffset(position, 0);
    } else {
      scrollToPosition(position);
    }
  }
 
Copy the code

Among them, the scrollVerticallyToPosition () method, in view of the LayoutManager types will have a judgment, if LayoutManager is LinearLayoutManager, Perform scrollToPositionWithOffset () method, otherwise do scrollToPosition () method, complete the final rolling operation.

ObservableScrollViewCallbacks

Interface methods

ObservableRecyclerView holds ObservableScrollViewCallbacks references, the interface is also open source libraries, as defined by the interface, when using ObservableRecyclerView, The user defines the callback function for scrolling. The method in this interface is as follows:

  • OnScrollChanged (int scrollY, Boolean firstScroll, Boolean Dragging): Called when the scrolling state has changed, but not when the page is first loaded, if the layout needs to be initialized in this method, You have to call it manually.
  • OnDownMotionEvent (): callback function when the finger-pressed event occurs.
  • OnUpOrCancelMotionEvent (ScrollState ScrollState): callback function when the finger lift event occurs or the scroll event is cancelled.

The specific implementation

In general, setting up the listener and overwriting the callback interface is enough to achieve the desired scrolling listener effect when used. Remember the simplest ActionBar show/hide example? First, let the Activity implement this interface:

Public class ActionBarControlRecyclerViewActivityextends BaseActivityimplements ObservableScrollViewCallbacks...Copy the code

We then initialize ObservableRecyclerView in onCreate and set the scroll listener for it:

ObservableRecyclerViewrecyclerView = (ObservableRecyclerView) findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setHasFixedSize(true);
        recyclerView.setScrollViewCallbacks(this);
 
Copy the code

After setting up the listener, the above three methods must be implemented. In order to achieve the show/hide effect of ActionBar, the implementation method needs to be as follows:

@Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollStatescrollState) { ActionBarab = getSupportActionBar(); if (ab == null) { return; } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); } } else if (scrollState == ScrollState.DOWN) { if (! ab.isShowing()) { ab.show(); }}}Copy the code

So we’re done with this effect.

Note that since the code for this effect is in the onUpOrCancelMotionEvent() method, the show/hide effect does not occur when you swipe the page, but only when your finger is lifted. If you want to show/hide the effect while scrolling, The code that implements this effect should be placed in the onScrollChanged() method.

ObservableRecyclerView Other important methods

Let’s look at other important ObservableRecyclerView methods, some of which are overridden superclass methods, some of which are self-defined methods.

Methods to override the parent class are:

– onRestoreInstanceState (Parcelable state)

– onSaveInstanceState ()

— onScrollChanged(int l, int t, int oldl, int oldt)

– onInterceptTouchEvent (MotionEvent ev)

OnTouchEvent (MotionEvent ev)

– getChildAdapterPosition (child) View

Own method:

The init ()

– dispatchOnDownMotionEvent ()

— dispatchOnScrollChanged(int scrollY, Boolean firstScroll, Boolean dragging)

– dispatchOnUpOrCancelMotionEvent (ScrollState ScrollState)

– hasNoCallbacks ()

And an inner class: SavedState

Save the state

OnRestoreInstanceState () and onSaveInstanceState(), as well as the inner SavedState class, do some temporary state saving. The following attributes need to be saved:

private int mPrevFirstVisiblePosition;
private int mPrevFirstVisibleChildHeight = -1;
private int mPrevScrolledChildrenHeight;
private int mPrevScrollY;
private int mScrollY;
private SparseIntArraymChildrenHeights;
 
Copy the code

At some point the Activity will be killed because the system is reclaiming resources, and onSaveInstanceState will have the opportunity to save its user interface state, The interface state can be restored by onCreate(Bundle) or onRestoreInstanceState(Bundle) when the user returns to the Activity in the future.

onScrollChanged

OnScrollChanged (int l, int t, int oldl, int oldt)

@Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (hasNoCallbacks()) { return; } if (getChildCount() > 0) { int firstVisiblePosition = getChildAdapterPosition(getChildAt(0)); int lastVisiblePosition = getChildAdapterPosition(getChildAt(getChildCount() - 1)); for (int i = firstVisiblePosition, j = 0; i <= lastVisiblePosition; i++, j++) { int childHeight = 0; Viewchild = getChildAt(j); if (child ! = null) { if (mChildrenHeights.indexOfKey(i) < 0 || (child.getHeight() ! = mChildrenHeights.get(i))) { childHeight = child.getHeight(); } } mChildrenHeights.put(i, childHeight); } ViewfirstVisibleChild = getChildAt(0); if (firstVisibleChild ! = null) {if (mPrevFirstVisiblePosition < firstVisiblePosition) {/ / slide down int skippedChildrenHeight = 0; if (firstVisiblePosition - mPrevFirstVisiblePosition ! = 1) { for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else {// Approximate the height of each item to the height of the first visible child View // This calculation may not be correct, but if not, Error when upward from the bottom sliding scrollY skippedChildrenHeight + = firstVisibleChild. GetHeight (). } } } mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition < mPrevFirstVisiblePosition) {/ / sliding upwards int skippedChildrenHeight = 0; if (mPrevFirstVisiblePosition - firstVisiblePosition ! = 1) { for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else {// Approximate the height of each item to the height of the first visible child View // This calculation may not be correct, but if not, Error when upward from the bottom sliding scrollY skippedChildrenHeight + = firstVisibleChild. GetHeight (). } } } mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition == 0) { mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); mPrevScrolledChildrenHeight = 0; } if (mPrevFirstVisibleChildHeight < 0) { mPrevFirstVisibleChildHeight = 0; } mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop() + getPaddingTop(); mPrevFirstVisiblePosition = firstVisiblePosition; dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < mScrollY) {// down mScrollState = scrollState.up; } else if (mScrollY < mPrevScrollY) {// mScrollState = scrollState.down; } else { mScrollState = ScrollState.STOP; } mPrevScrollY = mScrollY; }}}Copy the code

From the above code we know:

1. If no callback is set, return directly without distributing the touch events;

2. If the number of sub-layouts is greater than 0, get firstVisiblePosition of the first sub-layout and lastVisiblePosition of the last sub-layout;

3. Loop through all sub-layouts. If the SparseIntArray container does not save the height of the sub-layout, or if the saved value is inconsistent with the current height of the sub-layout, the value is stored in the container.

4, compare mPrevFirstVisiblePosition and firstVisiblePosition difference, judge the sliding direction (UP or DOWN), MPrevScrolledChildrenHeight and mPrevFirstVisibleChildHeight size respectively, finally obtaining the value of the mScrollY call dispatchOnScrollChanged (), Let the callback method implement the final onScrollChanged() method.

Dispatching events

OnInterceptTouchEvent () and onTouchEvent () method of rewrite the ViegGroup, the former to intercept touch events, if a finger press is detected, then call methods dispatchOnDownMotionEvent (). In the method dispatchOnDownMotionEvent (), to deal with events, handling as follows: If the callback is set, the onDownMotionEvent() method in the callback method is called (the user needs to implement the operation when using it). If the container containing the callback interface is not zero, every interface in the container is iterated. Call the onDownMotionEvent() method for each interface.

OnTouchEvent () method, if the detected touch events were canceled, call the dispatchOnUpOrCancelMotionEvent () method, keep up with the dispatchOnDownMotionEvent () method, The onUpOrCancelMotionEvent() method in the callback method is also called, or the container is traversed.

The situation is much more complicated when the touch event is detected as MOVE. Let’s look at the code:

case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { if (mIntercepted) { return false; } final ViewGroupparent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } float offsetX = 0; float offsetY = 0; for (View v = this; v ! = null && v ! = parent; v = (View) v.getParent()) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); } final MotionEventevent = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; event.setAction(MotionEvent.ACTION_DOWN); post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); }}); return false; } return super.onTouchEvent(ev); } break;Copy the code

In this method, the mTouchInterceptionViewGroup will set up a good intercept View assigned to the parent, if not set, the automatic assignment of the current View of the parent class to the parent. With parent, we can convert physical coordinates to logical coordinates:

event.offsetLocation(offsetX, offsetY);
 
Copy the code

If the parent class has already intercepted the event, false is returned and the thread is started to invoke the parent’s dispatchTouchEvent() method.

GetChildAdapterPosition and init

Finally, the getChildAdapterPosition() method is overridden from the View method, According to the value judgment of recyclerViewLibraryVersion is called getChildAdapterPosition () or getChildPosition (), while the init () method in the constructor is called, It creates a new SparseIntArray (which is more efficient than HashMap and can improve performance) and calls checkLibraryVersion() to check the version number of the RecyclerView library.

ObservableRecyclerView source code analysis to here is almost the same, through reading the source code of ObservableRecyclerView, and learned a lot of knowledge, especially the consolidation of the previous learning is not very clear View event interception and event processing this part of the content, I benefit a lot from it.