diff --git a/Examples/UIExplorer/js/UIExplorerExampleList.js b/Examples/UIExplorer/js/UIExplorerExampleList.js index 633ce7177b6..81ba03a4726 100644 --- a/Examples/UIExplorer/js/UIExplorerExampleList.js +++ b/Examples/UIExplorer/js/UIExplorerExampleList.js @@ -185,6 +185,7 @@ const styles = StyleSheet.create({ padding: 5, fontWeight: '500', fontSize: 11, + backgroundColor: '#eeeeee', }, row: { backgroundColor: 'white', diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index f96b2d93aea..f754d801ec0 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -263,7 +263,6 @@ const ScrollView = React.createClass({ * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the * top of the scroll view. This property is not supported in conjunction * with `horizontal={true}`. - * @platform ios */ stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), style: StyleSheetPropType(ViewStylePropTypes), @@ -453,7 +452,8 @@ const ScrollView = React.createClass({ ref={this._setInnerViewRef} style={contentContainerStyle} removeClippedSubviews={this.props.removeClippedSubviews} - collapsable={false}> + collapsable={false} + collapseChildren={!this.props.stickyHeaderIndices}> {this.props.children} ; diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 4d8c4738321..8596da22fb5 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -473,6 +473,15 @@ const View = React.createClass({ */ collapsable: PropTypes.bool, + /** + * Same as `collapsable` but also applies to all of this view's children. + * Setting this to `false` ensures that the all children exists in the native + * view hierarchy. + * + * @platform android + */ + collapseChildren: PropTypes.bool, + /** * Whether this `View` needs to rendered offscreen and composited with an alpha * in order to preserve 100% correct colors and blending behavior. The default diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 815bad0fadd..1282c57fea4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -5,7 +5,7 @@ package com.facebook.react.uimanager; import android.graphics.Color; import android.os.Build; import android.view.View; -import android.view.ViewGroup; +import android.view.ViewParent; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.uimanager.annotations.ReactProp; @@ -56,6 +56,8 @@ public abstract class BaseViewManager= 0; i--) { - View child = viewGroup.getChildAt(i); + int childIndex = useCustomOrder ? + ((DrawingOrderViewGroup) viewGroup).getDrawingOrder(i) : i; + View child = viewGroup.getChildAt(childIndex); PointF childPoint = mTempPoint; if (isTransformedTouchPointInView(eventCoords[0], eventCoords[1], viewGroup, child, childPoint)) { // If it is contained within the child View, the childPoint value will contain the view diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index 34765858349..2f78219e060 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -823,4 +823,8 @@ public class UIImplementation { return rootTag; } + + public ViewManager getViewManager(String name) { + return mViewManagers.get(name); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 469e0c708c8..3430688b7c7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -28,6 +28,7 @@ public class ViewProps { public static final String ALIGN_SELF = "alignSelf"; public static final String BOTTOM = "bottom"; public static final String COLLAPSABLE = "collapsable"; + public static final String COLLAPSE_CHILDREN = "collapseChildren"; public static final String FLEX = "flex"; public static final String FLEX_GROW = "flexGrow"; public static final String FLEX_SHRINK = "flexShrink"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 09c93809124..f8d6acc74b4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -9,10 +9,6 @@ package com.facebook.react.views.scroll; -import javax.annotation.Nullable; - -import java.lang.reflect.Field; - import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; @@ -24,13 +20,21 @@ import android.view.View; import android.widget.OverScroller; import android.widget.ScrollView; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; -import com.facebook.infer.annotation.Assertions; +import com.facebook.react.views.view.ReactViewGroup; +import com.facebook.react.views.view.ReactViewManager; + +import java.lang.reflect.Field; + +import javax.annotation.Nullable; /** * A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has @@ -58,6 +62,16 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou private @Nullable String mScrollPerfTag; private @Nullable Drawable mEndBackground; private int mEndFillColor = Color.TRANSPARENT; + private @Nullable int[] mStickyHeaderIndices; + private ReactViewManager mViewManager; + + private final ReactViewGroup.ChildDrawingOrderDelegate mContentDrawingOrderDelegate = + new ReactViewGroup.ChildDrawingOrderDelegate() { + @Override + public int getChildDrawingOrder(ReactViewGroup viewGroup, int drawingIndex) { + return viewGroup.getChildCount() - drawingIndex - 1; + } + }; public ReactScrollView(ReactContext context) { this(context, null); @@ -65,6 +79,10 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) { super(context); + + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + mViewManager = (ReactViewManager) uiManager.getUIImplementation().getViewManager(ReactViewManager.REACT_CLASS); + mFpsListener = fpsListener; if (!sTriedToGetScrollerField) { @@ -139,6 +157,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou super.onScrollChanged(x, y, oldX, oldY); if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { + dockClosestSectionHeader(); if (mRemoveClippedSubviews) { updateClippingRect(); } @@ -309,6 +328,84 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou } } + public void setStickyHeaderIndices(@Nullable ReadableArray indices) { + if (indices == null) { + mStickyHeaderIndices = null; + } else { + int[] indicesArray = new int[indices.size()]; + for (int i = 0; i < indices.size(); i++) { + indicesArray[i] = indices.getInt(i); + } + + mStickyHeaderIndices = indicesArray; + } + dockClosestSectionHeader(); + } + + private void dockClosestSectionHeader() { + if (mStickyHeaderIndices == null) { + return; + } + + View previousHeader = null; + View currentHeader = null; + View nextHeader = null; + ReactViewGroup contentView = (ReactViewGroup) getChildAt(0); + if (contentView == null) { + return; + } + contentView.setChildDrawingOrderDelegate(mContentDrawingOrderDelegate); + + int scrollY = getScrollY(); + for (int idx : mStickyHeaderIndices) { + // If the subviews are out of sync with the sticky header indices don't + // do anything. + if (idx >= mViewManager.getChildCount(contentView)) { + break; + } + + View header = mViewManager.getChildAt(contentView, idx); + + // If nextHeader not yet found, search for docked headers. + if (nextHeader == null) { + int top = header.getTop(); + if (top > scrollY) { + nextHeader = header; + } else { + previousHeader = currentHeader; + currentHeader = header; + } + } + + header.setTranslationY(0); + } + + if (currentHeader == null) { + return; + } + + int currentHeaderTop = currentHeader.getTop(); + int currentHeaderHeight = currentHeader.getHeight(); + int yOffset = scrollY - currentHeaderTop; + + if (nextHeader != null) { + // The next header nudges the current header out of the way when it reaches + // the top of the screen. + int nextHeaderTop = nextHeader.getTop(); + int overlap = currentHeaderHeight - (nextHeaderTop - scrollY); + yOffset -= Math.max(0, overlap); + } + + currentHeader.setTranslationY(yOffset); + + if (previousHeader != null) { + // The previous header sits right above the currentHeader's initial position + // so it scrolls away nicely once the currentHeader has locked into place. + yOffset = currentHeaderTop - previousHeader.getTop() - previousHeader.getHeight(); + previousHeader.setTranslationY(yOffset); + } + } + @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { if (mScroller != null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index 4fe6676f2b4..b6d5ede311c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -9,18 +9,18 @@ package com.facebook.react.views.scroll; -import javax.annotation.Nullable; - -import java.util.Map; - import android.graphics.Color; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.MapBuilder; -import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; +import com.facebook.react.uimanager.annotations.ReactProp; + +import java.util.Map; + +import javax.annotation.Nullable; /** * View manager for {@link ReactScrollView} components. @@ -104,6 +104,11 @@ public class ReactScrollViewManager view.setEndFillColor(color); } + @ReactProp(name = "stickyHeaderIndices") + public void setStickyHeaderIndices(ReactScrollView view, @Nullable ReadableArray indices) { + view.setStickyHeaderIndices(indices); + } + @Override public @Nullable Map getCommandsMap() { return ReactScrollViewCommandHelper.getCommandsMap(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 9575034477f..6cd11931712 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -13,7 +13,9 @@ import javax.annotation.Nullable; import android.content.Context; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.view.animation.Animation; @@ -34,13 +36,17 @@ import com.facebook.react.uimanager.ReactClippingViewGroupHelper; * initializes most of the storage needed for them. */ public class ReactViewGroup extends ViewGroup implements - ReactInterceptingViewGroup, com.facebook.react.uimanager.ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView { + ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView, DrawingOrderViewGroup { private static final int ARRAY_CAPACITY_INCREMENT = 12; private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0); /* should only be used in {@link #updateClippingToRect} */ - private static final Rect sHelperRect = new Rect(); + private static final RectF sHelperRect = new RectF(); + + public interface ChildDrawingOrderDelegate { + int getChildDrawingOrder(ReactViewGroup view, int i); + } /** * This listener will be set for child views when removeClippedSubview property is enabled. When @@ -93,6 +99,7 @@ public class ReactViewGroup extends ViewGroup implements private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; private boolean mNeedsOffscreenAlphaCompositing = false; + private @Nullable ChildDrawingOrderDelegate mChildDrawingOrderDelegate; public ReactViewGroup(Context context) { super(context); @@ -290,8 +297,15 @@ public class ReactViewGroup extends ViewGroup implements private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { View child = Assertions.assertNotNull(mAllChildren)[idx]; sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); - boolean intersects = clippingRect - .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + Matrix matrix = child.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(sHelperRect); + } + boolean intersects = clippingRect.intersects( + (int) sHelperRect.left, + (int) sHelperRect.top, + (int) Math.ceil(sHelperRect.right), + (int) Math.ceil(sHelperRect.bottom)); boolean needUpdateClippingRecursive = false; // We never want to clip children that are being animated, as this can easily break layout : // when layout animation changes size and/or position of views contained inside a listview that @@ -337,8 +351,15 @@ public class ReactViewGroup extends ViewGroup implements // do fast check whether intersect state changed sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom()); - boolean intersects = mClippingRect - .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + Matrix matrix = subview.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(sHelperRect); + } + boolean intersects = mClippingRect.intersects( + (int) sHelperRect.left, + (int) sHelperRect.top, + (int) Math.ceil(sHelperRect.right), + (int) Math.ceil(sHelperRect.bottom)); // If it was intersecting before, should be attached to the parent boolean oldIntersects = (subview.getParent() != null); @@ -531,4 +552,27 @@ public class ReactViewGroup extends ViewGroup implements mHitSlopRect = rect; } + @Override + public int getDrawingOrder(int i) { + return getChildDrawingOrder(getChildCount(), i); + } + + @Override + public boolean isDrawingOrderEnabled() { + return isChildrenDrawingOrderEnabled(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderDelegate == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderDelegate.getChildDrawingOrder(this, i); + } + } + + public void setChildDrawingOrderDelegate(@Nullable ChildDrawingOrderDelegate delegate) { + setChildrenDrawingOrderEnabled(delegate != null); + mChildDrawingOrderDelegate = delegate; + } }