ScrollView 源码分析(二)

来源:互联网 发布:ubuntu安装wps office 编辑:程序博客网 时间:2024/06/05 05:32

ScrollView 源码分析(二)

public class ReactScrollView extends ScrollView implements ReactClippingViewGroup {.........}

这段代码清楚的向我们展示了 ReactScrollView 的继承关系,它继承了 ScrollView,实现了 ReactClippingViewGroup 接口,其中继承 ScrollView 的话,我们只需呀关注,React ScrollView 如何添加 Item 到 ScrollView 中(因为在React 中它更像Native的ListView的特性,有添加Item的能力,而不像Native的ScrollView只有容器能力)。

我们先来看看后面这个接口是干嘛的

 // 这里省去了所有的注释public interface ReactClippingViewGroup {  void updateClippingRect();  void getClippingRect(Rect outClippingRect);  void setRemoveClippedSubviews(boolean removeClippedSubviews);  boolean getRemoveClippedSubviews();}

根据注释,我们可以知道实现了这个接口的 View 组件类,就能够在 JS 中支持 removeClippedSubviews 的属性。如果一个 ViewGroup 组件类具有这个属性,就可以移除被这个 ViewGroup 边界切割的子 View。具体的区分一个子 View 是否被切割,要根据 ReactClippingViewGroupHelper 类的 calculateClippingRect()方法来计算。

简单来说,就是实现了这个接口的 ViewGroup 组件类,就可以移除不可见的子 View,回收内存(没考虑复用,因为复用的代价比较大,后面会说到)。

我们回到源码简单看一下构造函数:

public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {super(context);mFpsListener = fpsListener;// sTriedToGetScrollerField 默认是 false,所以一定会执行一次第一个if里面的代码。if (!sTriedToGetScrollerField) {  sTriedToGetScrollerField = true; // 设置为 true  try {// 通过反射获取 mScroller字段,mScroller其实是一个 OverScroller 对象sScrollerField = ScrollView.class.getDeclaredField("mScroller");sScrollerField.setAccessible(true);// 为了避免 IllegalAccessExceptions 异常  } catch (NoSuchFieldException e) {Log.w(  ReactConstants.TAG,  "Failed to get mScroller field for ScrollView! " +"This app will exhibit the bounce-back scrolling bug :(");  }}// 如果 OverScroller 对象不为空,则根据字段拿到 OverScroller 对象。if (sScrollerField != null) {  try {mScroller = (OverScroller) sScrollerField.get(this);  } catch (IllegalAccessException e) {throw new RuntimeException("Failed to get mScroller from ScrollView!", e);  }} else {  mScroller = null;}}

总的来说,构造函数里面就是为了获取一个 一个 OverScroller 对象。 OverScroller 是一个 Scoller 类,用于滚动的辅助计算,最早的实现是 Scroller 类,大概在 api 1的时候,后来 google 为了完善 Scroller 类,就给出了 OverScroller 这个类,用于处理超出边界的滚动及其他扩展情况。然后,我们还忘记了 mFpsListener = fpsListener;这一行代码,这个类和我们 React 的 onScroll 函数回调有关,后面有用到再解释。

ReactScrollView 对应这一个 ReactScrollViewManager 具体的位置位于 com.facebook.react.views.scroll.ReactScrollViewManager,这个类管理着 ReactScrollView 的一个示例,并且暴露了创建实例和设置属性的方法给 JS。

  @Override  public String getName() {return REACT_CLASS;  }

Manager 通过 放回一个 String 的变量供 JS 调用,然后通过 @ReactProp(或@ReactPropGroup)注解来导出属性的设置方法,提供给 JS 设置,也就大概反映了着他们之间的通信模式。如下,便是一个属性设置方法的示例代码:

  @ReactProp(name = "scrollEnabled", defaultBoolean = true)  public void setScrollEnabled(ReactScrollView view, boolean value) {view.setScrollEnabled(value);  }

上面的方法,暴露给 JS,JS可以通过这个方法设置 scrollEnabled 属性,来决定 ScrollView 的内容是否可以被滚动。

我们大概了解了一下 ReactScrollView 之后,就可以着手进行分析了,我们首先从 Native ScrollView 上出发,考虑的几个问题和前提:

  • ScrollView 继承子 FrameLayout,它是一个 ViewGroup.
  • ScrollView 只允许一个直接子View,然后这个子 View 在包裹这需要滚动的内容,这个子 View 一般是 LinearLayout 之类的布局类。
  • ScrollView 相当于把全被子View的内容一次显示出来,或者说,它不像 ListView 一样是动态添加的,但是 React ScrollView 却可以实现动态添加,是怎样一种实现机制?

就第三个问题来分析一下,我们知道 View的移动(内容滑动,并非View本身移动)就是调用了 View 的 scrollBy()和scrollTo()方法,而scrollBy()和scrollTo()方法最终会导致 View 的 onScrollChanged()方法的调用,那我每年进去 ReactScrollView 看下这个方法:

  @Override  protected void onScrollChanged(int x, int y, int oldX, int oldY) {super.onScrollChanged(x, y, oldX, oldY);// 继承了 onScrollChanged(),这里只需要知道这里完成了// 判断移动是否有效if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {  if (mRemoveClippedSubviews) { //默认是false,这里的 mRemoveClippedSubviews 设置值是在哪里设置的?updateClippingRect();  }  if (mFlinging) {mDoneFlinging = false;  }  ReactScrollViewHelper.emitScrollEvent(this);}  }

这个方法里面,继承了 ScrollView 的 onScrollChanged()方法,然后第一个 if 里面判断移动是否有效,然后第二个if里面,根绝 mRemoveClippedSubviews 的布尔值,如果是 true 就执行 updateClippingRect() 方法,这个 mRemoveClippedSubviews 是在哪里设置的呢? ReactScrollView 里面有个 setRemoveClippedSubviews()方法,这里对 mRemoveClippedSubviews 进行了赋值。但是又是再哪里调用这个方法的呢?

  @Override  public void setRemoveClippedSubviews(boolean removeClippedSubviews) {if (removeClippedSubviews && mClippingRect == null) {  mClippingRect = new Rect();}mRemoveClippedSubviews = removeClippedSubviews;updateClippingRect();  }

一路找呀找,我们找到了,ReactScrollViewManager 类发现了下面这个方法:

  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)  public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {view.setRemoveClippedSubviews(removeClippedSubviews);  }

原来,只要你在 React 的ScrollView 里面设置了 removeClippedSubviews 属性值就是,我们之前的 mRemoveClippedSubviews数值了。那么现在就来考虑,updateClippingRect()方法里面的实现把,下面是具体的实现代码:

  @Override  public void updateClippingRect() {if (!mRemoveClippedSubviews) {  return;// 如果 React 上的 ScrollView 的removeClippedSubviews 属性设置为 false}Assertions.assertNotNull(mClippingRect);// 关键代码就在这里了ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);// 获取直接子 View View contentView = getChildAt(0);//如果直接子 View 也是实现了 ReactClippingViewGroup 接口,则子 View 也会调用updateClippingRect()方法。if (contentView instanceof ReactClippingViewGroup) {  ((ReactClippingViewGroup) contentView).updateClippingRect();}  }

这个方法的详细很简单,基本上我都在上面的代码里面注释了,现在我们在重点放在 ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect) 这个静态方法里面,先上代码:

 public static void calculateClippingRect(View view, Rect outputRect) {ViewParent parent = view.getParent();if (parent == null) {  outputRect.setEmpty();  return;} else if (parent instanceof ReactClippingViewGroup) {  ReactClippingViewGroup clippingViewGroup = (ReactClippingViewGroup) parent;  if (clippingViewGroup.getRemoveClippedSubviews()) {clippingViewGroup.getClippingRect(sHelperRect);// Intersect the view with the parent's rectangle// This will result in the overlap with coordinates in the parent spaceif (!sHelperRect.intersect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())) {  outputRect.setEmpty();  return;}// Now we move the coordinates to the View's coordinate spacesHelperRect.offset(-view.getLeft(), -view.getTop());sHelperRect.offset(view.getScrollX(), view.getScrollY());outputRect.set(sHelperRect);return;  }}view.getDrawingRect(outputRect);  }

这里的代码也不是很长,但是单单看这里的代码还不能够说明根本问题,我之前提到过 React ScrollView 是一个 ViewGroup 类,实际上在 View 类型的子类也会实现 ReactClippingViewGroup 接口,就拿最简单的 View 来说,它在 Native 对应的类是 ReactViewGroup,先看看 updateClippingRect()方法是怎么实现的:

  @Override  public void updateClippingRect() {if (!mRemoveClippedSubviews) {  return;}Assertions.assertNotNull(mClippingRect);Assertions.assertNotNull(mAllChildren);ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);updateClippingToRect(mClippingRect);  }

看到这里,我们似乎清楚了:

  private void updateClippingToRect(Rect clippingRect) {Assertions.assertNotNull(mAllChildren);int clippedSoFar = 0;for (int i = 0; i < mAllChildrenCount; i++) {  updateSubviewClipStatus(clippingRect, i, clippedSoFar);  if (mAllChildren[i].getParent() == null) {clippedSoFar++;  }}  }