Skip to content

Commit

Permalink
Implement ScrollView sticky headers on Android
Browse files Browse the repository at this point in the history
Summary:
This adds support for sticky headers on Android. The implementation if based primarily on the iOS one (https://github.com/facebook/react-native/blob/master/React/Views/RCTScrollView.m#L272) and adds some stuff that was missing to be able to handle z-index, view clipping, view hierarchy optimization and touch handling properly.

Some notable changes:
- Add `ChildDrawingOrderDelegate` interface to allow changing the `ViewGroup` drawing order using `ViewGroup#getChildDrawingOrder`. This is used to change the content view drawing order to make sure headers are drawn over the other cells. Right now I'm only reversing the drawing order as drawing only the header views last added a lot of complexity especially because of view clipping and I don't think it should cause issues.

- Add `collapsableChildren` prop that works like `collapsable` but applies to every child of the view. This is needed to be able to reference sticky headers by their indices otherwise some subviews can get optimized out and break indexes.
Closes #9456

Differential Revision: D3827366

fbshipit-source-id: cab044cfdbe2ccb98e1ecd3e02ed3ceaa253eb78
  • Loading branch information
janicduplessis authored and Facebook Github Bot 4 committed Sep 15, 2016
1 parent 272d3de commit 0e8b75b
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 24 deletions.
1 change: 1 addition & 0 deletions Examples/UIExplorer/js/UIExplorerExampleList.js
Expand Up @@ -185,6 +185,7 @@ const styles = StyleSheet.create({
padding: 5,
fontWeight: '500',
fontSize: 11,
backgroundColor: '#eeeeee',
},
row: {
backgroundColor: 'white',
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Components/ScrollView/ScrollView.js
Expand Up @@ -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),
Expand Down Expand Up @@ -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}
</View>;

Expand Down
9 changes: 9 additions & 0 deletions Libraries/Components/View/View.js
Expand Up @@ -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
Expand Down
Expand Up @@ -5,7 +5,7 @@
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;
Expand Down Expand Up @@ -56,6 +56,8 @@ public void setTransform(T view, ReadableArray matrix) {
} else {
setTransformProperty(view, matrix);
}

updateClipping(view);
}

@ReactProp(name = PROP_OPACITY, defaultFloat = 1.f)
Expand Down Expand Up @@ -114,30 +116,40 @@ public void setImportantForAccessibility(T view, String importantForAccessibilit
@ReactProp(name = PROP_ROTATION)
public void setRotation(T view, float rotation) {
view.setRotation(rotation);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_SCALE_X, defaultFloat = 1f)
public void setScaleX(T view, float scaleX) {
view.setScaleX(scaleX);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_SCALE_Y, defaultFloat = 1f)
public void setScaleY(T view, float scaleY) {
view.setScaleY(scaleY);

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_TRANSLATE_X, defaultFloat = 0f)
public void setTranslateX(T view, float translateX) {
view.setTranslationX(PixelUtil.toPixelFromDIP(translateX));

updateClipping(view);
}

@Deprecated
@ReactProp(name = PROP_TRANSLATE_Y, defaultFloat = 0f)
public void setTranslateY(T view, float translateY) {
view.setTranslationY(PixelUtil.toPixelFromDIP(translateY));

updateClipping(view);
}

@ReactProp(name = PROP_ACCESSIBILITY_LIVE_REGION)
Expand Down Expand Up @@ -176,4 +188,11 @@ private static void resetTransformProperty(View view) {
view.setScaleX(1);
view.setScaleY(1);
}

private static void updateClipping(View view) {
ViewParent parent = view.getParent();
if (parent instanceof ReactClippingViewGroup) {
((ReactClippingViewGroup) parent).updateClippingRect();
}
}
}
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.uimanager;

public interface DrawingOrderViewGroup {
/**
* Returns if the ViewGroup implements custom drawing order.
*/
boolean isDrawingOrderEnabled();

/**
* Returns which child to draw for the specified index.
*/
int getDrawingOrder(int i);
}
Expand Up @@ -92,8 +92,11 @@ public void handleCreateView(
return;
}

node.setShouldCollapseChildren(shouldCollapseChildren(initialProps));

boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) &&
isLayoutOnlyAndCollapsable(initialProps);
isLayoutOnlyAndCollapsable(initialProps) &&
(node.getParent() == null || node.getParent().shouldCollapseChildren());
node.setIsLayoutOnly(isLayoutOnly);

if (!isLayoutOnly) {
Expand Down Expand Up @@ -435,7 +438,19 @@ private void transitionLayoutOnlyViewToNativeView(
mTagsWithLayoutVisited.clear();
}

private static boolean isLayoutOnlyAndCollapsable(@Nullable ReactStylesDiffMap props) {
private static boolean shouldCollapseChildren(@Nullable ReactStylesDiffMap props) {
if (props == null) {
return true;
}

if (props.hasKey(ViewProps.COLLAPSE_CHILDREN) && !props.getBoolean(ViewProps.COLLAPSE_CHILDREN, true)) {
return false;
}

return true;
}

private static boolean isCollapsable(@Nullable ReactStylesDiffMap props) {
if (props == null) {
return true;
}
Expand All @@ -444,6 +459,18 @@ private static boolean isLayoutOnlyAndCollapsable(@Nullable ReactStylesDiffMap p
return false;
}

return true;
}

private static boolean isLayoutOnlyAndCollapsable(@Nullable ReactStylesDiffMap props) {
if (props == null) {
return true;
}

if (!isCollapsable(props) || !shouldCollapseChildren(props)) {
return false;
}

ReadableMapKeySetIterator keyIterator = props.mBackingMap.keySetIterator();
while (keyIterator.hasNextKey()) {
if (!ViewProps.isLayoutOnly(keyIterator.nextKey())) {
Expand Down
Expand Up @@ -10,14 +10,14 @@
package com.facebook.react.uimanager;

/**
* This interface should be implemented be native ViewGroup subclasses that can represent more
* This interface should be implemented by native ViewGroup subclasses that can represent more
* than a single react node. In that case, virtual and non-virtual (mapping to a View) elements
* can overlap, and TouchTargetHelper may incorrectly dispatch touch event to a wrong element
* because it priorities children over parents.
*/
public interface ReactCompoundViewGroup extends ReactCompoundView {
/**
* Returns true if react node responsible for the touch even is flattened into this ViewGroup.
* Returns true if react node responsible for the touch event is flattened into this ViewGroup.
* Use reactTagForTouch() to get its tag.
*/
boolean interceptsTouchEvent(float touchX, float touchY);
Expand Down
Expand Up @@ -53,6 +53,7 @@ public class ReactShadowNode extends CSSNode {
private boolean mNodeUpdated = true;

// layout-only nodes
private boolean mShouldCollapseChildren;
private boolean mIsLayoutOnly;
private int mTotalNativeChildren = 0;
private @Nullable ReactShadowNode mNativeParent;
Expand Down Expand Up @@ -367,6 +368,14 @@ public boolean isLayoutOnly() {
return mIsLayoutOnly;
}

public void setShouldCollapseChildren(boolean collapsable) {
mShouldCollapseChildren = collapsable;
}

public boolean shouldCollapseChildren() {
return mShouldCollapseChildren;
}

public int getTotalNativeChildren() {
return mTotalNativeChildren;
}
Expand Down
Expand Up @@ -124,8 +124,12 @@ private static View findClosestReactAncestor(View view) {
*/
private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup) {
int childrenCount = viewGroup.getChildCount();
final boolean useCustomOrder = (viewGroup instanceof DrawingOrderViewGroup) &&
((DrawingOrderViewGroup) viewGroup).isDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 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
Expand Down
Expand Up @@ -823,4 +823,8 @@ public int resolveRootTagFromReactTag(int reactTag) {

return rootTag;
}

public ViewManager getViewManager(String name) {
return mViewManagers.get(name);
}
}
Expand Up @@ -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";
Expand Down

0 comments on commit 0e8b75b

Please sign in to comment.