英文:
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 代码不起作用:
我想要下面的设计,该如何实现?
我们如何根据需求改变正方形的形状?
英文:
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>
)
}
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?
答案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);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论