如何使用React Native绘制自定义形状?

huangapple go评论59阅读模式
英文:

How to draw a customized shape with react-native?

问题

以下是你提供的代码的翻译:

<Animated.View style={{ flex: 1 }}>
    <View style={{ height: '10%', width: '100%', backgroundColor: 'orange', flexDirection: 'row', justifyContent: 'center' }}>
        <Text style={{ fontSize: 26, fontWeight: '600', color: 'black', position: 'absolute', bottom: 10 }}>Dashboard</Text>
    </View>
    <View style={{ height: '100%', width: '100%', flexDirection: 'row', backgroundColor: 'white' }}>
        <View style={{
            height: '90%', width: '30%', backgroundColor: 'lightgrey', justifyContent: 'center',
        }}>
            <View>

滚动视图包含多个视图

            <ScrollView style={{ width: '100%', backgroundColor: 'grey' }}>
<TouchableOpacity style={{
width: 200, // 上部宽度
height: 0,
borderBottomWidth: 50, // 高度
borderBottomColor: "red",
borderLeftWidth: 0,
borderLeftColor: "transparent",
borderRightWidth: 50,
borderRightColor: "transparent",
borderStyle: "solid",
}}>
</TouchableOpacity>
</ScrollView>
</View>
</View>
<View style={{
height: '90%', width: '70%', backgroundColor: 'lightgrey', 
justifyContent: 'center'
}}>
<View>
<ScrollView style={{ width: '100%', backgroundColor: 'grey' }}>
<TouchableOpacity style={{ height: 100, width: '55%', backgroundColor: '#ffe4c4', justifyContent: 'center', alignItems: 'center', marginTop: 20, borderTopRightRadius: 30, borderBottomRightRadius: 30 }}>
<Text>dfjhxc</Text>
</TouchableOpacity>
</ScrollView>
</View>
</View>
</Animated.View>

但是我的 React Native 代码不起作用:
我想要下面的设计,该如何实现?
我们如何根据需求改变正方形的形状?

如何使用React Native绘制自定义形状?

如何使用React Native绘制自定义形状?

英文:

Below code is mine

 <Animated.View style={{ flex: 1 }}>
<View style={{ height: '10%', width: '100%', backgroundColor: 'orange', flexDirection: 'row', justifyContent: 'center' }}>
<Text style={{ fontSize: 26, fontWeight: '600', color: 'black', position: 'absolute', bottom: 10 }}>Dashboard</Text>
</View>
<View style={{ height: '100%', width: '100%', flexDirection: 'row', backgroundColor: 'white' }}>
<View style={{
height: '90%', width: '30%', backgroundColor: 'lightgrey', justifyContent: 'center',
}}>
<View>

Scroll view that contain multiple view

                            <TouchableOpacity style={{
width: 200, // top width
height: 0,
borderBottomWidth: 50, //hight
borderBottomColor: "red",
borderLeftWidth: 0,
borderLeftColor: "transparent",
borderRightWidth: 50,
borderRightColor: "transparent",
borderStyle: "solid",
}}>
</TouchableOpacity>
</ScrollView>
</View>
</View>
<View style={{
height: '90%', width: '70%', backgroundColor: 'lightgrey', 
justifyContent: 'center'
}}>
<View>
<ScrollView style={{ width: '100%', backgroundColor: 'grey' }}>
<TouchableOpacity style={{ height: 100, width: '55%', backgroundColor: '#ffe4c4', justifyContent: 'center', alignItems: 'center', marginTop: 20, borderTopRightRadius: 30, borderBottomRightRadius: 30 }}>
<Text>dfjhxc</Text>
</TouchableOpacity>
</ScrollView>
</View>
</View>
</View>
</Animated.View>
)
} 

如何使用React Native绘制自定义形状?

but my code on react-native doesn't work:
I want below design, how can I achieve that?
how we change shape of square as per our requirement?

如何使用React Native绘制自定义形状?

答案1

得分: 1

如果您使用react-native-svg的polygon,您可以获得所需的布局。您只需要知道您的条形图的位置和文本的位置,然后使用这些位置绘制多边形。我看到您正在使用一个ScrollView,所以我假设这些视图的定位会移动,这意味着您需要一些动画效果。react-native-reanimated和react-native-gesture-handler对此非常适合。

这里有一个演示链接:演示。我开始使用typescript,希望这没问题。

以下是一些React Native组件的代码示例,包括App、Sidebar、ContentItems、ListItem和BarConnectors。这些组件一起构建了一个交互式仪表板布局。

请注意,这些只是代码片段,没有完整的React Native应用程序。如果您需要更多详细信息或特定的帮助,请告诉我。

英文:

If you use react-native-svg's polygon you can get your desired layout. You just need to know the position of your bar and the position of your text, and then draw your polygon to with those positions. I see you are using a ScrollView, so I assume that the positioning of these views will be moving, meaning that you'll need some animation. react-native-reanimated and react-native-gesture-handler is good for this.

Here's a demo. I started to use typescript, so I hope that's okay

import React, { useState, useCallback } from 'react';
import {
  Button,
  LayoutRectangle,
  StyleSheet,
  Text,
  View,
  useWindowDimensions,
} from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import Constants from 'expo-constants';

import BarConnectors from './components/BarConnectors';
import ContentItems from './components/ContentItems';
import SideBar from './components/SideBar';

import useBarSharedValues from './hooks/useBarSharedValues';
import useLayout from './hooks/useLayout';

import Colors from './Colors';
import { ITEMS, totalItems } from './Constants';

export default function App() {
  const { width, height } = useWindowDimensions();
  // store sideBar's layout to tune pan gesture
  // and calculate itemLayout's x position
  const [sideBarLayout, onSideBarLayout] = useLayout();
  // use textLayout to help calculate itemLayout's y position
  const [textLayout, onTextLayout] = useLayout();
  const [itemLayouts, setItemLayouts] = useState(
    Array(totalItems)
      .fill(null)
      .map(() => ({ x: 0, y: 0, width: 0, height: 0 }))
  );
  const { barY, barRef, ...sharedValues } = useBarSharedValues({
    sideBarLayout,
  });
  // content absoluteY is underneath the Dashboard text
  const contentAbsoluteY =
    Constants.statusBarHeight + textLayout.y + textLayout.height;
  return (
    <GestureHandlerRootView style={styles.container}>
      <SafeAreaProvider style={styles.container}>
        <SafeAreaView style={styles.container}>
          <Text style={styles.title} onLayout={onTextLayout}>
            Dashboard
          </Text>
          <View style={styles.row}>
            <SideBar
              {...sharedValues}
              barY={barY}
              ref={barRef}
              sideBarLayout={sideBarLayout}
              onSideBarLayout={onSideBarLayout}
              yOffset={Constants.statusBarHeight}
            />
            <ContentItems
              items={ITEMS}
              itemLayouts={itemLayouts}
              setItemLayouts={setItemLayouts}
              yOffset={contentAbsoluteY}
              color={sharedValues.color}
            />
          </View>
          <BarConnectors
            width={width}
            height={height}
            barY={barY}
            {...sharedValues}
            yOffset={textLayout.height}
            itemLayouts={itemLayouts}
          />
          
        </SafeAreaView>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    opacity: 1,
  },
  row: {
    flexDirection: 'row',
    flex: 1,
  },
  title: {
    fontSize: 26,
    fontWeight: '600',
    color: 'black',
    backgroundColor: Colors.orange,
    textAlign: 'center',
    height: 40,
  },
  circle:{
    position:'absolute',
    width:10,
    height:10,
    borderRadius:5,
    backgroundColor:'pink'
  }
});

The sidebar component:

import React, { forwardRef, useCallback } from 'react';
import {
  LayoutChangeEvent,
  LayoutRectangle,
  StyleSheet,
  View,
  Text,
} from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  SharedValue,
  useAnimatedStyle,
  withTiming,
  useAnimatedProps,
  interpolate,
  Extrapolate,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import Colors from '../Colors';
import { barSize, barYPosition, sideBarWidth } from '../Constants';
import Svg, { Rect } from 'react-native-svg';
const hitSlop = 120;

type Props = {
  barY: SharedValue<number>;
  sideBarLayout: LayoutRectangle;
  onSideBarLayout: (e: LayoutChangeEvent) => void;
  yOffset: number;
};

// forward ref so that reanimated's measure function can be used
export default forwardRef<Animated.View, Props>(function SideBar1(
  { barY, sideBarLayout, onSideBarLayout, yOffset },
  aRef
) {
  const { height } = sideBarLayout;
  const safeAreaInsets = useSafeAreaInsets();
  // make bar draggable
  const rStyle = useAnimatedStyle(() => {
    return {
      top: barY.value,
    };
  });

  const pan = Gesture.Pan()
    .onChange((e) => {
      barY.value += e.changeY;
    })
    .onEnd(() => {
      // on release make bar to stay within view dimensions
      barY.value = withTiming(
        interpolate(
          barY.value,
          [-yOffset, height],
          [0, height - safeAreaInsets.bottom - barSize / 2],
          Extrapolate.CLAMP
        )
      );
    });

  return (
    <View style={styles.container} onLayout={onSideBarLayout}>
      <GestureDetector gesture={pan}>
        <View>
          <Animated.View
            style={[styles.bar, rStyle]}
            ref={aRef}
            hitSlop={{
              top: hitSlop,
              left: hitSlop,
              right: hitSlop,
              bottom: hitSlop,
            }}
          />
        </View>
      </GestureDetector>
    </View>
  );
});

const styles = StyleSheet.create({
  container: {
    flex: 1,
    width: sideBarWidth,
    backgroundColor: Colors.lightgray,
    borderRightWidth: 2,
    borderRightColor: Colors.paleOrange,
    zIndex: 40,
  },
  bar: {
    position: 'absolute',
    bottom: 0,
    right: 0,
    width: 3,
    height: barSize,
    backgroundColor: 'black',
    top: barYPosition,
    left: 2,
    zIndex: 41,
  },
});

render list items and set layout state within them

import React, { useCallback } from 'react';
import { StyleSheet, View, ScrollView, Text } from 'react-native';

import Colors from '../Colors';
import ListItem from './ListItem';
import { sideBarWidth, headerHeight } from '../Constants';

export default function ContentItems({
  items,
  color,
  itemLayouts,
  setItemLayouts,
  yOffset,
}) {
  return (
    <View style={styles.container}>
      <View style={styles.header} />
      <View style={styles.content}>
        {items.map((item) => (
          <ListItem
            {...item}
            color={color}
            key={item.id}
            setItemLayouts={setItemLayouts}
            xOffset={sideBarWidth}
            yOffset={yOffset+headerHeight}
          />
        ))}
      </View>
      <View style={styles.footer}></View>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    width: '70%',
    backgroundColor: Colors.darkgray,
    opacity: 1,
  },
  header: {
    backgroundColor: Colors.lightgray,
    height: headerHeight,
  },
  footer: {
    backgroundColor: Colors.lightgray,
    height: headerHeight,
  },
  content: {
    paddingTop: 5,
    flex: 1,
    backgroundColor: Colors.darkgray,
  },
  layout: {
    width: sideBarWidth / 0.3 / 3,
    padding: 10,
    paddingTop: 0,
    borderRightWidth: 1,
  },
});
import React, {
  useCallback,
  forwardRef,
  useEffect,
  useImperativeHandle,
} from 'react';
import { View, StyleSheet, Text } from 'react-native';
import Animated, {
  useAnimatedRef,
  useAnimatedStyle,
} from 'react-native-reanimated';
import Colors from '../Colors';
import { headerHeight, itemMarginVertical } from '../Constants';

export default function ListItem({
  title,
  color,
  setItemLayouts,
  index,
  xOffset,
  yOffset,
}) {
  const onLayout = useCallback(
    (e) => {
      const layout = e.nativeEvent.layout;
      if (!layout) return;
      setItemLayouts((prev) => {
        // add on the offsets to get the absolute position
        const newItems = [...prev];
        newItems[index] = {
          ...layout,
          x: layout.x + xOffset,
          y: layout.y + yOffset,
        };

        return newItems;
      });
    },
    [index, setItemLayouts, xOffset, yOffset]
  );
  const rStyle = useAnimatedStyle(() => {
    // if useAnimatedColor worked then the backgroundColor
    // would lighten and darken as the bar moved
    return {
      backgroundColor: color.value,
    };
  });
  return (
    <Animated.View style={[styles.container, rStyle]} onLayout={onLayout}>
      <Text>{title}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  container: {
    paddingVertical: 20,
    backgroundColor: Colors.paleOrange,
    marginVertical: itemMarginVertical,
    width: '50%',
    borderBottomEndRadius: 20,
    borderTopEndRadius: 20,
  },
});

Finally, draw the polygons that connect the bars to the list items. This will be by absolutely positioning the svg canvas on top of the screen:

import React from 'react';
import { LayoutRectange } from 'react-native';

import Animated, {
  SharedValue,
  useAnimatedProps,
} from 'react-native-reanimated';
import Svg, { Polygon as SvgPolygon } from 'react-native-svg';
import Constants from 'expo-constants';
import { StyleSheet } from 'react-native';
import { sideBarWidth } from '../Constants';
import { MeasuredDimensions } from '../helpers/reanimated';
import Polygon from './Polygon';
const AnimatedPolygon = Animated.createAnimatedComponent(SvgPolygon);

interface Props {
  width: number;
  height: number;
  color: SharedValue<string>;
  barY: SharedValue<number>;
  layouts: SharedValue<MeasuredDimensions>[];
  barLayout: SharedValue<MeasuredDimensions>;
  itemLayouts: LayoutRectange[];
  yOffset: number;
}

export default function BarConnectors({
  width,
  height,
  color,
  layouts,
  barLayout,
  barY,
  yOffset = 0,
  itemLayouts,
}: Props) {
 
  return (
    <Svg
      width={width}
      height={height - yOffset}
      style={[styles.svgContainer, { top: yOffset }]}
      pointerEvents="none">
      {itemLayouts.map((layout, i) => {
        return (
          <Polygon
            itemLayout={layout}
            layout={layout}
            barLayout={barLayout}
            index={i}
            key={`polygons-${i}`}
            color={color}
            barY={barY}
            yOffset={yOffset}
          />
        );
      })}
    </Svg>
  );
}

const styles = StyleSheet.create({
  svgContainer: {
    position: 'absolute',
    // backgroundColor: 'red',
    // opacity:0.25,
    top: 0,
    left: 0,
    bottom: 0,
    zIndex: 100,
  },
});
import React from 'react';
import Animated, {
  SharedValue,
  useAnimatedProps,
  useDerivedValue,
  runOnUI,
} from 'react-native-reanimated';
import { LayoutRectangle } from 'react-native';
import { Circle, Polygon as SvgPolygon } from 'react-native-svg';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { segmentSize, barYPosition, itemMarginVertical } from '../Constants';

import { initialBarLayout } from '../hooks/useBarSharedValues';
import { MeasuredDimensions, measureView } from '../helpers/reanimated';
import Colors from '../Colors';

const AnimatedPolygon = Animated.createAnimatedComponent(SvgPolygon);
// eslint-disable-next-line
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

interface Props {
  itemLayout: LayoutRectangle;
  layout: SharedValue<MeasuredDimensions>;
  barLayout: SharedValue<MeasuredDimensions>;
  index: number;
  color: SharedValue<string>;
  barY: SharedValue<number>;
  yOffset: number;
}

export default function Polygon({
  index,
  color,
  barLayout,
  barY,
  itemLayout,
}: Props) {
  const insets = useSafeAreaInsets();
  // animate point changes when barLayout changes
  const points = useDerivedValue(() => {
    console.log('recalculating polygon points')
    let p = '';
    const bLayout = barLayout.value || initialBarLayout;
    // if you use barLayout.value.pageY you get the absolute y position
    // but its value doesnt update

    // if you use barY.value its isnt the absolute y position
    const barPoint = barY.value + segmentSize * index + insets.top;
    const itemPageX = itemLayout.x;
    const itemPageY = itemLayout.y;
    p =
      `${itemPageX},${itemPageY + itemLayout.height} ` +
      `${itemPageX},${itemPageY} ` +
      `${bLayout.pageX},${barPoint} ` +
      `${bLayout.pageX}, ${barPoint + segmentSize / 2} `;

    return p;
  }, [itemLayout,barY.value,barLayout.value]);
  const animatedProps = useAnimatedProps(() => {
    return {
      // fill: color.value,
      points: points.value,
    };
  });
  return (
    <AnimatedPolygon fill={Colors.paleOrange} animatedProps={animatedProps} />
  );
}

// export default Animated.createAnimatedComponent(Polygon);

huangapple
  • 本文由 发表于 2023年5月22日 20:49:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76306377.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定