I need to build a custom circle menu in react-native. I am currently building this menu with expo. It needs to have a circle in the middle of the screen with a logo/text and when you press and hold on the circle, smaller circles buttons come out from the middle and grow into place around the main circle, evenly spaced. Then, while still pressing, as you move your finger towards these smaller circles, they get bigger to reveal an icon and or text. When you release your finger on one of these outer circle buttons, it then takes you to that part of the app.
The closest module I found for my need is this one:
https://github.com/Ramotion/react-native-circle-menu
It did not work out of the box. I had to modify it to get it working with the most recent react-native/expo and the progress portion also doesn't work, so I commented that out as well.
As far as my need and what I have, I was able to get the circle in the middle with text, you press and hold and the smaller circle buttons come out and are evenly spaces around it, but when you drag to them and let go, im not sure which events on which components would register how I need.
Here is the code that renders the main menu button:
return <Animated.View style={{
transform: [
{
scaleX: this.state.animation.interpolate({
inputRange: [0, .2, .75, 1],
outputRange: [1.5, 1.2, .5, 1]
})
},
{
scaleY: this.state.animation.interpolate({
inputRange: [0, .2, .75, 1],
outputRange: [1.5, 1.2, .5, 1]
})
}
]
}}>
<TouchableOpacity
onPressIn={this.openMenu}
onPressOut={this.closeMenu}
style={styles.closedButton}
activeOpacity="1">
<Text style={styles.closedButtonText}>XYZ</Text>
</TouchableOpacity>
</Animated.View>
This renders the smaller circle buttons that are around the outside of the main menu button above:
renderActions() {
if (!this.state.active) {
return null
}
// Defines the amount of angle to increase for each item so they do not overlap
const increase = Math.PI * 2 / this.props.items.length;
// Define initial angle
let angle = -Math.PI / 2;
// Loop through each action (item) to add them to the display
return this.props.items.map((item, index) => {
// Angle first starts at the initial angle
const btnAngle = angle;
// Then increases the angle by 'increase' for each button
angle += increase;
// Displays the individual action (item)
return <ActionIcon
key={index}
animation={this.state.animation}
angle={btnAngle}
bgColor={this.props.bgColor}
buttonColor={item.color}
icon={item.name}
radius={this.props.radius - this.props.itemSize}
onPress={() => {
this.closeMenu();
this.props.onPress(index);
}}
size={this.props.itemSize}
style={[styles.actionContainer, { position: 'absolute' }]}
/>
})
}
and finally, here is how the circle menu is being called from the main App.js
items = [
{
name: 'md-home',
color: '#298CFF',
},
{
name: 'md-search',
color: '#30A400',
},
{
name: 'md-time',
color: '#FF4B32',
},
{
name: 'md-settings',
color: '#8A39FF',
},
{
name: 'md-navigate',
color: '#FF6A00',
},
];
onPress = index => console.log(`${this.items[index].name} icon pressed!`);
render() {
return <View style={styles.container}>
<CircleMenu
bgColor="#0E1329"
items={this.items}
onPress={this.onPress}
/>
</View>
}
If there is any other information I can provide, please let me know. Thanks in advance.
Related
I am using React Leaflet to create a custom map.
The card (Image Overlay) is currently being rendered, but it does not fill the container in which the card is placed.
The size of the container is set strictly and I would like the map to fill the container as much as possible. All attempts to resize with CSS failed.
Live example: https://codesandbox.io/s/dark-worker-8qbzfr
My code:
import "./styles.css";
import { MapContainer, ImageOverlay } from "react-leaflet";
import "leaflet/dist/leaflet.css";
const M = ({ width, height, zoom, center }) => {
const wh = [width, 500];
const origin = [0, 0];
const bounds = [origin, wh];
return (
<div style={{ width: "1100px", height: "600px" }}>
<MapContainer
style={{ height: "100%", minHeight: "100%" }}
bounds={zoom ? undefined : bounds}
boundsOptions={{
padding: [0, 0]
}}
maxBounds={bounds}
zoom={center ? zoom : undefined}
center={zoom ? center : undefined}
>
<ImageOverlay
url="../ptichnikDraw.png"
bounds={bounds}
className="map_main"
/>
</MapContainer>
</div>
);
};
const App = () => {
return (
<div
style={{
display: "flex",
justifyContent: "center"
}}
>
<M width={1500} height={500} center={[0, 0]} />
</div>
);
};
export default App;
You want your "card" (the <ImageOverlay>) to fill the initial map container/viewport.
You will not be able to do so using (only) CSS, but with Leaflet map options and methods, which are exposed by React Leaflet wrapper:
Use the Leaflet map whenReady option (exposed as a prop of <MapContainer>) to attach a callback that is executed once the map is initialized
Runs the given function fn when the map gets initialized
While not explicitly documented, that callback receives a load event argument, which target property is the newly instantiated Leaflet map
Fired when the map is initialized
Call the fitBounds() map method on that instance, with the same bounds as your Image Overlay, to have the map view be adjusted accordingly
Sets a map view that contains the given geographical bounds with the maximum zoom level possible.
Important: make sure to set the map zoomSnap option to value 0, otherwise the fitBounds will be modified so that the final zoom is a multiple of zoomSnap (default to 1, i.e. integer zoom levels)
Forces the map's zoom level to always be a multiple of this, particularly right after a fitBounds() or a pinch-zoom. By default, the zoom level snaps to the nearest integer; lower values (e.g. 0.5 or 0.1) allow for greater granularity. A value of 0 means the zoom level will not be snapped after fitBounds or a pinch-zoom.
const M = ({ width, height, zoom, center }) => {
const wh = [width, 500];
const origin = [0, 0];
const bounds = [origin, wh];
return (
<div style={{ width: "1100px", height: "600px" }}>
<MapContainer
style={{ height: "100%", minHeight: "100%" }}
bounds={zoom ? undefined : bounds}
boundsOptions={{
padding: [0, 0]
}}
maxBounds={bounds}
zoom={center ? zoom : undefined}
center={zoom ? center : undefined}
zoomSnap={0} // Important to disable snap after fitBounds
whenReady={(e) => e.target.fitBounds(bounds)} // Have the map adjust its view to the same bounds as the Image Overlay
>
<ImageOverlay
url="../ptichnikDraw.png"
bounds={bounds}
className="map_main"
/>
</MapContainer>
</div>
);
};
Updated CodeSandbox: https://codesandbox.io/s/pedantic-tree-lz1o9q
Using the default Leaflet JavaScript library you can use map.invalidateSize(). Don't know how that would work in React but maybe you can figure that one out using the documentation.
I'm trying to create a component on my screen, where the user is able to use their finger to rotate the componnent around its center. I have been able to get the angle and consistently update it (verified through console.log), but the value is not being passed rotate under Animated.view.
Here is my code:
class Spinner extends React.Component {
constructor(props){
super(props);
this.angleCalculation = this.angleCalculation.bind(this);
this.state = {
angleString: '0deg'
}
}
angleCalculation(x,y){
xValue = x - 50
yValue = (y * -1) + 150
angle = Math.atan(yValue/xValue) * (180/Math.PI)
this.state.angleString = angle + 'deg'
console.log(this.state.angleString)
}
render() {
return(
<View style={styles.container}>
<Animated.View
style={styles.box, {
backgroundColor: 'blue',
width:100,
height:300,
transform:[{ rotate: this.state.angleString
}]
}}
onStartShouldSetResponder={(evt) => true}
onResponderGrant={(evt) => {this.angleCalculation(evt.nativeEvent.locationX,evt.nativeEvent.locationY)}}
onResponderMove={(evt) => {this.angleCalculation(evt.nativeEvent.locationX,evt.nativeEvent.locationY)}}
onResponderRelease={(evt) => {this.angleCalculation(evt.nativeEvent.locationX,evt.nativeEvent.locationY)}}
/>
</View>
)
}
}
The console.log consistently updates as I moved my finger in a circular fashion. And it does so in the angleString form that I want. In my this.state, I set an initial value of '0 deg' but I am not sure why it is updating under 'rotate'.
What can I do to make it update in real-time? So the rotate accepts the updated values and follows along with the user's finger?
Thanks for any help that you can provide!
ReactVis API here outlines what is available which might be helpful. I have successfully added a "drag to select" function to my custom code. In the picture, we would be selecting 13 to 17 and turn the fill value to red for example.
More pseudo code question but code would be awesome.
A container div in a Hook would look like this:
<div className="unselectable"
style={{ position: 'relative' }}
onMouseDown={(e) => {
if (!x && !y) {
setX(e.clientX); setY(e.clientY)
}
}}
onMouseMove={(e) => {
if (x && y) {
const newX = e.clientX; const newY = e.clientY;
setX1(newX); setY1(newY);
setRect(
<div style={{
zIndex: 10000,
position: 'fixed',
left: x > newX ? newX : x, top: y > newY ? newY : y,
width: Math.abs(newX - x), height: Math.abs(newY - y),
backgroundColor: 'gray', opacity: 0.2
}} />
)
}
}}
onMouseUp={(e) => {
setX(null); setY(null); setX1(null); setY1(null);
setRect(null);
}}
>
<XYPlot>
//...
</XYPlot>
{rect}
</div>
Thank you very much.
In order to achieve "selected" vs "notSelected" state of the series, we need to use "datawithcolor" objects as our data object: {x: foo, y: bar, color:0}. Passing such an array to a react-series would be sufficient to render different coloured series (bars for example). For example: [{x: foo, y: bar, color:0}] where colorDomain would be [0, 1] and the colorRange would be some blue and red.
In order to have a listed of selected "values", we can hold on to a list of values in a react state (hooks or class), either during a dragging of a rectangle or after the rectangle has been drawn, in my case only during dragging. Every time the rectangle changes, the list must be updated.
On re-render generate a new "datawithcolor" array for React-vis bar-series with colour ranges and domains defined using indexes returned from onNearestX or similar React-vis callback.
How do we update the selected list?
<ReactSeries
// see React-vis source of abstract-series.js for details
/*
onNearestX(value, {
innerX: xScaleFn(value),
index: valueIndex,
event: event.nativeEvent
});
*/
onNearestX={(datapoint, { index, innerX }) => {
// rect is mentioned in the question
if (rect && isWithinRect({ x, x1, value: innerX })) {
if (!selected.includes(index)) { // prevent rerender
setSelected([...selected, index]);
}
} else {
if (!rect) {
setSelected([index]); // single hover
}
}
}}
colorDomain={[0, 1]}
colorRange={['red', 'blue']}
data={dataWithColor} />
For full example see this.
Things to remember, managing the rectangle and selected list are two different things. React-vis colorRange should be clear by now, the docs were not so helpful to me.
I'm trying to create geometric shapes (circles) that change color depending on certain values assumed by a variable.
In theory I set the values of these geometric shapes so that they turn out to be circles, instead they turn out to be squares. Can you tell me why?
this.state = {
pressColorDx1: "",
//...
}
//....
if (this.pressDx_p1 >= 10) { this.setState({ pressColorDx1: "#00438c" }) }
else { this.setState({ pressColorDx1: '#afeeee' }) }
render() {
return (
//..
<View style={{width: 15,height: 15,borderRadius: 15/2,backgroundColor: this.state.pressColorDx1,right: 30, top: 40}}></View>
)}
this should be a circle, but it is displayed like a square.
It's not a problem of top and right.. I have tried also borderRadius: 50% and it doesn't work.
You can see there https://snack.expo.io/#keyz23/92058c
I have the following code which when you click the gray semi-circle there are two animations that happen, I am very close to my intended outcome, however the line path animation animates about a point but that point seems to move at the end, I would like it to rotate about it's endpoint so that i doesn't move out of the circle that endpoint is in.
It is much clearer in the jsfiddle to see what I am talking about and the issue at hand.
** You have to click the gray circle to activate the animation **
console.clear();
var loop = "M31,201.1C16.6,118.8,69.1,40.5,150.7,26c32.5-5.8,64.5-1.6,93.7,14.5c59.1,32.6,88.4,94,77.1,159.1";
var pin = "M180.4,193.3c-1.8,0-3.5-0.9-4.4-2.6c-1.3-2.4-0.5-5.5,2-6.8L309.8,111c2.4-1.3,5.5-0.5,6.8, 2c1.3,2.4,0.5,5.5-2,6.8l-131.8,72.9C182.1,193.1,181.2,193.3,180.4,193.3z";
var circle_svg = "M-224.1,227.9c-5.1,5.1-5.1,13.4,0,18.5s13.4,5.1,18.5,0s5.1-13.4,0-18.5C-210.8,222.8-219,222.8-224.1,227.9z";
var loopLength = Snap.path.getTotalLength(loop);
console.log(loopLength);
var s = Snap();
circle = s.path({
path: loop,
fill: "gray",
stroke: "#c00",
strokeWidth: 0,
strokeLinecap: "round"
});
var g = s.gradient("l(0, 0, 1, 1)#80DFFE-#0CA5EE-#07A3EE");
circleOutline = s.path({
path: Snap.path.getSubpath(loop, 0, 0),
stroke: g,
fillOpacity: 0,
strokeWidth: 0,
strokeLinecap: "round"
});
circle_middle = s.path({
path: circle_svg,
stroke: "#000000",
fill: "#ffffff",
strokeWidth: 5,
});
pin_line = s.path({
path: pin,
fill: "#000000"
});
// +"t"+[translateX,translateY]
circle_middle.attr({
transform: "t" + [400, -65]
});
pin_line.attr({
transform: 'r-170,180,180'
});
circle.click(function(e) {
Snap.animate(0, loopLength,
function(step) { //step function
circleOutline.attr({
path: Snap.path.getSubpath(loop, 0, step),
strokeWidth: 25
});
}, // end of step function
800 //duration
); //Snap.animate
pin_line.animate({
transform: 'r30,180,180',
translate: '20, 20, 85, 85'
}, 1000);
}); //click the circle
svg {
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
So there was indeed an alignment issue, and I tried to fix that in Illustrator, then came back to your example, and realized how some of those transforms in the JS actually work, and was able to adjust them to finally look correct, but when I tried just adjusting the transforms using your paths, I couldn't get it right, even when applying t(ranslations) to the pin_line in addition to rotations. So it's either a combination of the paths needing to be replaced with some fine-tuning of that rotation points, or it's that I just don't fully understand how to work the snapsvg code. :) In any case, I got a fiddle working that incorporates some of my new path coordinates made in Illustrator and some changes to the JS: https://jsfiddle.net/14e107mt/4/ In addition to the paths changing, it comments out the translation of circle_middle's translation (since it's now aligned there by default), and changes the pin_line rotation coordinates to
pin_line.attr({ transform: 'r-170 176 172' });
and
pin_line.animate({ transform: 'r30 176 172' }, 1000 );
instead of 180 180.