Skip to content

Conversation

skevy
Copy link

@skevy skevy commented Jul 7, 2017

Description

It's very important in complex UIs to be able to apply alpha channel-based masks to arbitrary content. Common use cases include adding gradient masks at the top or bottom of scroll views, creating masked text effects, feathering images, and generally just masking views while still allowing transparency of those views.

The original motivation for creating this component stemmed from work on react-navigation. As I tried to mimic behavior in the native iOS header, I needed to be able to achieve the effect pictured here (this is a screenshot from a native iOS application):

iOS native navbar animation

In this image, there are two masks:

  • A mask on the back button chevron
  • A gradient mask on the right button

In addition, the underlying view in the navigation bar is intended to be a UIBlurView. Thus, alpha masking is the only way to achieve this effect.

Behind the scenes, the maskView property on UIView is used. This is a shortcut to setting the mask on the CALayer directly.

This gives us the ability to mask any view with any other view. While building this component (and testing in the context of an Expo app), I was able to use a GLView (a view that renders an OpenGL context) to mask a Video component!

I chose to implement this only on iOS right now, as the Android implementation is a) significantly more complicated and b) will most likely not be as performant (especially when trying to mask more complex views).

Test Plan (required)

Review the <MaskedViewIOS> section in the RNTester app, observe that views are masked appropriately.

example

Copy link
Member

@ide ide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good to me. One thing I'd check is making sure that dealloc gets called on the mask view when the masked view is unmounted. I don't see any retain cycles but think it'd be good to check quickly.

// the mask is set correctly.
if (self.maskView != nil) {
if (self.maskView.superview != nil) {
UIView *_maskView = self.maskView;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can all the code in this block just be [removeFromSuperview]? I don't understand what setting maskView several times does here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maskView is set when the prop is set, but at that point, the view we're setting as a mask is still a part of the view hierarchy, so setting it doesn't do what we want it to do -- we need to reset it in order for the CALayer mask to be applied correctly.

Honestly, as I type this I realize there's a clearer way to write this while maintaining the same behavior, so gonna do that.


const target = React.Children.only(this.props.children);

if (typeof this.props.renderMask !== 'function') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to remove this and still get the warning from PropTypes right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could -- this method gives a better warning though, and is a convention I've seen followed elsewhere in the codebase.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I hate propTypes and they're kind of being phased out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay 🤷‍♂️

const maskElement = this.props.renderMask();
let maskElementWithRef = null;
if (maskElement) {
maskElementWithRef = React.cloneElement(maskElement, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use cloneReferencedElement so that if the owner of the mask set a ref, we don't clobber it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure that isn't necessary anymore. From the React docs:

However, it also preserves refs. This means that if you get a child with a ref on it, you won't accidentally steal it from your ancestor. You will get the same ref attached to your new element.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's great, i'm so glad they changed that

<RCTMaskedView
style={this.props.style}
maskRef={this.state.maskViewNodeHandle}>
<View style={[StyleSheet.absoluteFill, { opacity: 0 }]}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about adding pointerEvents: 'none' here? technically not needed but if someone were to copy this implementation for Android then we'd maybe want it. i don't feel strongly either way

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmk can do

@skevy
Copy link
Author

skevy commented Jul 7, 2017

Re: dealloc -- I am indeed sure it's being called correctly, and this is due to the fact that React still owns the mask view -- and thus is managing it's lifecycle and deallocation. The fact that we removed it from the subview hierarchy (with removeFromSuperview) makes no difference.

@skevy skevy force-pushed the @skevy/MaskedView branch from 51df298 to d37e079 Compare July 7, 2017 22:26
if (!node) {
return;
}
if (!this.state.maskViewNodeRef || this.state.maskViewNodeRef !== node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove !this.state.maskViewNodeRef ||, that's already captured in the !== node check

also ideally to make the setState update be transactional this check would happen in the callback..

setState(state => {
  if (state.maskViewNodeRef !== node) {
    return { maskViewNodeRef: node };
  }
  return {}; // ideally there'd be a way to tell React not to reconcile but i don't think returning null here would do that..
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if putting the check in the callback doesn't work then i think we should get rid of the callback and just use setState(object) so that we're either fully transactional or not at all

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K fixed.

@skevy skevy force-pushed the @skevy/MaskedView branch from d37e079 to 3ea91b8 Compare July 7, 2017 22:34
@skevy skevy force-pushed the @skevy/MaskedView branch from 3ea91b8 to 77c8237 Compare July 7, 2017 22:36
@skevy skevy closed this Jul 7, 2017
gabrieldonadel pushed a commit that referenced this pull request Jul 31, 2025
…tion for existing view (facebook#51294)

Summary:
Pull Request resolved: facebook#51294

changelog: [internal]

Fix a crash where a node that is supposed to be culled doesn't get visited because culling context is not updated.
The differentiator would generate a create instruction for a view that already exists.

Stack trace for the crash:
```
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
  * frame #0: 0x0000000111740874 libsystem_kernel.dylib`__pthread_kill + 8
    frame #1: 0x00000001117aa2ec libsystem_pthread.dylib`pthread_kill + 264
    frame #2: 0x0000000180171ea8 libsystem_c.dylib`abort + 100
    frame #3: 0x00000001802b0144 libc++abi.dylib`abort_message + 128
    frame #4: 0x000000018029fe4c libc++abi.dylib`demangling_terminate_handler() + 296
    frame #5: 0x000000018006f220 libobjc.A.dylib`_objc_terminate() + 124
    frame #6: 0x00000001375d1964 INFRAFramework`meta_terminate() + 5468
    frame #7: 0x00000001802af570 libc++abi.dylib`std::__terminate(void (*)()) + 12
    frame #8: 0x00000001802b2498 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 32
    frame #9: 0x00000001802b2478 libc++abi.dylib`__cxa_throw + 88
    frame #10: 0x0000000180093904 libobjc.A.dylib`objc_exception_throw + 384
    frame #11: 0x0000000180e6999c Foundation`-[NSAssertionHandler handleFailureInFunction:file:lineNumber:description:] + 268
    frame #12: 0x000000031a3bcfc8 XPLAT_6_Framework`-[RCTComponentViewRegistry dequeueComponentViewWithComponentHandle:tag:] + 528
    frame #13: 0x000000031a3ccdec XPLAT_6_Framework`RCTPerformMountInstructions(std::__1::vector<facebook::react::ShadowViewMutation, std::__1::allocator<facebook::react::ShadowViewMutation>> const&, RCTComponentViewRegistry*, RCTMountingTransactionObserverCoordinator&, int) + 356
    frame #14: 0x000000031a3ccc7c XPLAT_6_Framework`-[RCTMountingManager performTransaction:]::$_1::operator()(facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) const + 80
    frame #15: 0x000000031a3ccc20 XPLAT_6_Framework`decltype(std::declval<-[RCTMountingManager performTransaction:]::$_1&>()(std::declval<facebook::react::MountingTransaction const&>(), std::declval<facebook::react::SurfaceTelemetry const&>())) std::__1::__invoke[abi:ne190102]<-[RCTMountingManager performTransaction:]::$_1&, facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&>(-[RCTMountingManager performTransaction:]::$_1&, facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) + 40
    frame #16: 0x000000031a3ccbc8 XPLAT_6_Framework`void std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne190102]<-[RCTMountingManager performTransaction:]::$_1&, facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&>(-[RCTMountingManager performTransaction:]::$_1&, facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) + 40
    frame #17: 0x000000031a3ccb94 XPLAT_6_Framework`std::__1::__function::__alloc_func<-[RCTMountingManager performTransaction:]::$_1, std::__1::allocator<-[RCTMountingManager performTransaction:]::$_1>, void (facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&)>::operator()[abi:ne190102](facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) + 44
    frame #18: 0x000000031a3cba1c XPLAT_6_Framework`std::__1::__function::__func<-[RCTMountingManager performTransaction:]::$_1, std::__1::allocator<-[RCTMountingManager performTransaction:]::$_1>, void (facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&)>::operator()(facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) + 44
    frame #20: 0x000000032f219804 XPLAT_1_Framework`std::__1::function<void (facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&)>::operator()(this=0x000000016d4f0c78, __arg=0x000000016d4f0a10, __arg=0x000000016d4f0978) const at function.h:989:10
    frame #21: 0x000000032f219668 XPLAT_1_Framework`facebook::react::TelemetryController::pullTransaction(this=0x00000003f4680f00, willMount=0x000000016d4f0c98, doMount=0x000000016d4f0c78, didMount=0x000000016d4f0c58) const at TelemetryController.cpp:39:3
    frame #22: 0x000000031a3c5b28 XPLAT_6_Framework`-[RCTMountingManager performTransaction:] + 544
    frame #23: 0x000000031a3c5864 XPLAT_6_Framework`-[RCTMountingManager initiateTransaction:] + 456
    frame #24: 0x000000031a3c5240 XPLAT_6_Framework`__42-[RCTMountingManager scheduleTransaction:]_block_invoke + 308
    frame #25: 0x0000000131f81b84 BOTTOMFramework`__RCTExecuteOnMainQueue_block_invoke + 40
    frame #26: 0x000000018017c788 libdispatch.dylib`_dispatch_call_block_and_release + 24
    frame #27: 0x0000000180197278 libdispatch.dylib`_dispatch_client_callout + 12
    frame #28: 0x00000001801b2fcc libdispatch.dylib`_dispatch_main_queue_drain.cold.7 + 24
    frame #29: 0x000000018018c1c4 libdispatch.dylib`_dispatch_main_queue_drain + 1184
    frame #30: 0x000000018018bd14 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 40
    frame #31: 0x0000000180427fec CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
    frame #32: 0x00000001804229f8 CoreFoundation`__CFRunLoopRun + 1920
    frame #33: 0x0000000180421e3c CoreFoundation`CFRunLoopRunSpecific + 536
    frame #34: 0x0000000190f62d00 GraphicsServices`GSEventRunModal + 164
    frame #35: 0x0000000185bcec98 UIKitCore`-[UIApplication _run] + 796
    frame #36: 0x0000000185bd3064 UIKitCore`UIApplicationMain + 124
    frame #37: 0x0000000115fbf0bc PRODUCTFramework`main + 200
    frame #38: 0x00000001114293d8 dyld_sim`start_sim + 20
    frame #39: 0x0000000111506b4c dyld`start + 6000
```

Reviewed By: rubennorte

Differential Revision: D74654157

fbshipit-source-id: 9181bcd28524c71d0ca4620bd630dc0baa172386
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants