Malaa-tech / expo-squircle-view

A native implementation for figma corner smoothing (Squircle Shape) for react native expo apps ⏹️
MIT License
52 stars 4 forks source link

It does not overflow the child #6

Open CavalcanteLeo opened 2 months ago

CavalcanteLeo commented 2 months ago
               <SquircleView
                  cornerSmoothing={100}
                  className="h-24 w-24 overflow-hidden bg-gray-850 rounded-lg"
                  preserveSmoothing={false}
                  borderRadius={20}
                  borderWidth={10}
                  borderColor={'red'}
                  backgroundColor={'red'}
                >
                  <Image
                    source={item.logo}
                    fallbackSource={fallbackImage}
                    className="flex-1"
                  />
                </SquircleView>
WadhahEssam commented 2 months ago

Thank you @CavalcanteLeo, clipping currently is partially supported, didn't try the Image component case. but overall the clipping method currently used is not the best and would love to revisit it later. will keep this open for others 🙏

tarkanlar commented 3 weeks ago
               <SquircleView
                  cornerSmoothing={100}
                  className="h-24 w-24 overflow-hidden bg-gray-850 rounded-lg"
                  preserveSmoothing={false}
                  borderRadius={20}
                  borderWidth={10}
                  borderColor={'red'}
                  backgroundColor={'red'}
                >
                  <Image
                    source={item.logo}
                    fallbackSource={fallbackImage}
                    className="flex-1"
                  />
                </SquircleView>

I solved it with maskedView

import MaskedView from '@react-native-masked-view/masked-view';
import { requireNativeViewManager } from 'expo-modules-core';
import * as React from 'react';
import type {
  AnimatableNumericValue,
  ColorValue,
  DimensionValue,
  TouchableOpacityProps,
  ViewProps,
} from 'react-native';
import { processColor, StyleSheet, TouchableOpacity } from 'react-native';

type SquircleProps = {
  cornerSmoothing?: number;
  borderRadius?: AnimatableNumericValue;
  borderWidth?: number;
  preserveSmoothing?: boolean;
  enabledIOSAnimation?: boolean;
  ignoreBorderWidthFromPadding?: boolean;
};

export type ExpoSquircleNativeViewProps = {
  squircleBackgroundColor?: ReturnType<typeof processColor>;
  squircleBorderColor?: ReturnType<typeof processColor>;
  squircleBorderWidth?: number;
} & ViewProps &
  SquircleProps;

export type SquircleViewProps = {
  backgroundColor?: ColorValue;
  borderColor?: ColorValue;
} & ViewProps &
  SquircleProps;

export type SquircleButtonProps = {
  backgroundColor?: ColorValue;
  borderColor?: ColorValue;
} & TouchableOpacityProps &
  SquircleProps;

const NativeView: React.ComponentType<ExpoSquircleNativeViewProps> =
  requireNativeViewManager('ExpoSquircleView');

const ExpoSquircleViewNativeWrapper = (
  props: React.PropsWithChildren<SquircleViewProps | SquircleButtonProps>,
) => {
  const {
    cornerSmoothing,
    backgroundColor,
    borderRadius,
    borderColor,
    borderWidth,
    preserveSmoothing,
    enabledIOSAnimation,
  } = props;

  return (
    <NativeView
      squircleBackgroundColor={processColor(backgroundColor)}
      squircleBorderColor={processColor(borderColor)}
      squircleBorderWidth={borderWidth}
      borderRadius={borderRadius}
      cornerSmoothing={cornerSmoothing}
      preserveSmoothing={preserveSmoothing}
      enabledIOSAnimation={enabledIOSAnimation}
      style={StyleSheet.absoluteFill}
    />
  );
};

export const SquircleButton = (
  props: React.PropsWithChildren<SquircleButtonProps>,
) => {
  const { children } = props;
  const { squircleProps, wrapperStyle } = useSquircleProps(props);

  return (
    <TouchableOpacity {...props} style={wrapperStyle}>
      <ExpoSquircleViewNativeWrapper {...squircleProps} />
      {children}
    </TouchableOpacity>
  );
};

export const SquircleView = (
  props: React.PropsWithChildren<SquircleViewProps>,
) => {
  const { children } = props;
  const { squircleProps, wrapperStyle } = useSquircleProps(props);

  return (
    <MaskedView
      style={wrapperStyle}
      maskElement={
        <NativeView
          squircleBackgroundColor={processColor('black')}
          squircleBorderColor={processColor('black')}
          borderRadius={squircleProps.borderRadius}
          cornerSmoothing={squircleProps.cornerSmoothing}
          preserveSmoothing={squircleProps.preserveSmoothing}
          enabledIOSAnimation={squircleProps.enabledIOSAnimation}
          style={StyleSheet.absoluteFill}
        />
      }
    >
      {children}
    </MaskedView>
  );
};

const useSquircleProps = (props: SquircleViewProps | SquircleButtonProps) => {
  const style = props.style ? StyleSheet.flatten(props.style) : undefined;

  const {
    cornerSmoothing,
    borderRadius,
    borderWidth,
    backgroundColor,
    borderColor,
    ignoreBorderWidthFromPadding,
  } = props;

  const {
    padding,
    paddingVertical,
    paddingHorizontal,
    paddingBottom,
    paddingEnd,
    paddingLeft,
    paddingRight,
    paddingStart,
    paddingTop,
  } = style || {};

  const calculatedPadding = React.useMemo(() => {
    const extraPadding = borderWidth || style?.borderWidth || 0;

    const calculatePadding = (_paddingValue: DimensionValue) => {
      if (typeof _paddingValue === 'number') {
        return _paddingValue + extraPadding;
      }
      return _paddingValue;
    };

    return {
      padding: padding ? calculatePadding(padding) : extraPadding,
      paddingVertical: paddingVertical
        ? calculatePadding(paddingVertical)
        : undefined,
      paddingHorizontal: paddingHorizontal
        ? calculatePadding(paddingHorizontal)
        : undefined,
      paddingBottom: paddingBottom
        ? calculatePadding(paddingBottom)
        : undefined,
      paddingEnd: paddingEnd ? calculatePadding(paddingEnd) : undefined,
      paddingLeft: paddingLeft ? calculatePadding(paddingLeft) : undefined,
      paddingRight: paddingRight ? calculatePadding(paddingRight) : undefined,
      paddingStart: paddingStart ? calculatePadding(paddingStart) : undefined,
      paddingTop: paddingTop ? calculatePadding(paddingTop) : undefined,
    };
  }, [
    borderWidth,
    style?.borderWidth,
    padding,
    paddingVertical,
    paddingHorizontal,
    paddingBottom,
    paddingEnd,
    paddingLeft,
    paddingRight,
    paddingStart,
    paddingTop,
  ]);

  return {
    squircleProps: {
      ...props,
      borderRadius: borderRadius || style?.borderRadius || 0,
      borderWidth: borderWidth || style?.borderWidth || 0,
      backgroundColor:
        backgroundColor || style?.backgroundColor || 'transparent',
      borderColor: borderColor || style?.borderColor || 'transparent',
      cornerSmoothing: cornerSmoothing !== undefined ? cornerSmoothing : 100,
      preserveSmoothing: props.preserveSmoothing || false,
      enabledIOSAnimation: props.enabledIOSAnimation || false,
    },
    wrapperStyle: [
      styles.container,
      style,
      {
        borderWidth: undefined,
        borderColor: undefined,
        backgroundColor: undefined,
        overflow: 'hidden' as const,
        ...(ignoreBorderWidthFromPadding === true
          ? undefined
          : calculatedPadding),
      },
    ],
  };
};

const styles = StyleSheet.create({
  container: { backgroundColor: 'transparent' },
});