Wydaje mi się, że część "przeciągnij i upuść" działa, ale nie wiem, jak wykonywać zamienniki. Również nie wiesz, jak rozwiązać problem z-index (wydaje się, że robi coś podejrzanego z Animated.View).Przeciągnij, upuść i zamień animację elementów?
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image,
PanResponder,
Animated,
Alert,
} from 'react-native';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture:() => true,
onMoveShouldSetPanResponderCapture:() => true,
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
},
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, gesture) => {
this.state.pan.flattenOffset();
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 3 }
).start();
let dropzone = this.inDropZone(gesture);
if (dropzone) {
console.log(dropzone.y-this.layout.y, this.state.pan.y._value, dropzone.y);
Animated.spring(
this.state.pan,
{toValue:{
x: 0,
y: dropzone.y-this.layout.y,
}}
).start();
} else {
Animated.spring(
this.state.pan,
{toValue:{x:0,y:0}}
).start();
}
},
});
}
inDropZone(gesture) {
var isDropZone = false;
for (dropzone of this.props.dropZoneValues) {
if (gesture.moveY > dropzone.y && gesture.moveY < dropzone.y + dropzone.height && gesture.moveX > dropzone.x && gesture.moveX < dropzone.x + dropzone.width) {
isDropZone = dropzone;
}
}
return isDropZone;
}
setDropZoneValues(event) {
this.props.setDropZoneValues(event.nativeEvent.layout);
this.layout = event.nativeEvent.layout;
}
render() {
let { pan, scale } = this.state;
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
let imageStyle = {transform: [{translateX}, {translateY}, {rotate}, {scale}]};
return (
<View
style={styles.dropzone}
onLayout={this.setDropZoneValues.bind(this)}
>
<Animated.View
style={[imageStyle, styles.draggable]}
{...this._panResponder.panHandlers}>
<Image style={styles.image} resizeMode="contain" source={{ uri: this.props.uri }} />
</Animated.View>
</View>
);
}
}
class Playground extends Component {
constructor(props) {
super(props);
this.state = {
dropZoneValues: [],
};
}
setDropZoneValues(layout) {
this.setState({
dropZoneValues: this.state.dropZoneValues.concat(layout),
});
}
render() {
return (
<View style={styles.container}>
<Draggable
dropZoneValues={this.state.dropZoneValues}
setDropZoneValues={this.setDropZoneValues.bind(this)}
uri="https://pbs.twimg.com/profile_images/378800000822867536/3f5a00acf72df93528b6bb7cd0a4fd0c.jpeg"
/>
<Draggable
dropZoneValues={this.state.dropZoneValues}
setDropZoneValues={this.setDropZoneValues.bind(this)}
uri="https://pbs.twimg.com/profile_images/446566229210181632/2IeTff-V.jpeg"
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'orange',
justifyContent: 'center',
alignItems: 'center',
},
dropzone: {
zIndex: 0,
margin: 5,
width: 106,
height: 106,
borderColor: 'green',
borderWidth: 3
},
draggable: {
zIndex: 0,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
width: 100,
height: 100,
borderWidth: 1,
borderColor: 'black'
},
image: {
width: 75,
height: 75
}
});
export default Playground;
EDIT: zrobiłem próbę na zamiany, ale wydaje się tylko do pracy o pół godziny. Ponadto zIndex wciąż doprowadza mnie do szaleństwa. Drukuję stan taki jak {color} {zIndex}
, więc możesz go zaktualizować do 100, ale wydaje się, że nie działa. Zmiana koloru na niebieski wydaje się jednak działać ... jestem zdezorientowany.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image,
PanResponder,
Animated,
Alert,
} from 'react-native';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
zIndex: 0,
color: 'white',
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture:() => true,
onMoveShouldSetPanResponderCapture:() => true,
onPanResponderGrant: (e, gestureState) => {
console.log('moving', this.props.index);
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
this.setState({ color: 'blue', zIndex: 100 });
},
onPanResponderMove: Animated.event([null,
{ dx: this.state.pan.x, dy: this.state.pan.y },
]),
onPanResponderRelease: (e, gesture) => {
this.state.pan.flattenOffset();
// de-scale
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 3 }
).start();
this.setState({ color: 'white', zIndex: 0 });
let dropzone = this.inDropZone(gesture);
if (dropzone) { // plop into dropzone
// console.log(dropzone.y-this.layout.y, this.state.pan.y._value, dropzone.y);
console.log('grabbed', this.props.index, ' => dropped', dropzone.index);
Animated.spring(
this.state.pan,
{toValue:{
x: 0,
y: dropzone.y-this.layout.y,
}}
).start();
if (this.props.index !== dropzone.index) {
this.props.swapItems(this.props.index, dropzone.index, dropzone.y-this.layout.y);
}
} else {
// spring back to start
Animated.spring(
this.state.pan,
{toValue:{x:0,y:0}}
).start();
}
},
});
}
inDropZone(gesture) {
var isDropZone = false;
for (var dropzone of this.props.dropZoneValues) {
if (gesture.moveY > dropzone.y && gesture.moveY < dropzone.y + dropzone.height) {
isDropZone = dropzone;
}
}
return isDropZone;
}
setDropZoneValues(event) {
this.props.setDropZoneValues(event.nativeEvent.layout, this.props.index, this);
this.layout = event.nativeEvent.layout;
this.layout.index = this.props.index;
}
render() {
let { pan, scale, zIndex, color } = this.state;
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
let imageStyle = {
transform: [{translateX}, {translateY}, {rotate}, {scale}]
};
return (
<View
style={[styles.dropzone]}
onLayout={this.setDropZoneValues.bind(this)}
>
<Animated.View
{...this._panResponder.panHandlers}
style={[imageStyle, styles.draggable, { backgroundColor: color, zIndex }]}
>
<Text>{this.props.index}</Text>
<Text>{this.props.char}</Text>
<Text>{this.state.color} {this.state.zIndex}</Text>
</Animated.View>
</View>
);
}
}
Array.prototype.swap = function (x,y) {
var b = this[x];
this[x] = this[y];
this[y] = b;
return this;
}
Array.prototype.clone = function() {
return this.slice(0);
};
const items = [
'shiba inu',
'labrador',
];
class Playground extends Component {
constructor(props) {
super(props);
this.state = {
items,
dropZoneValues: [],
dropzones: [],
};
}
setDropZoneValues(layout, index, dropzone) {
layout.index = index;
this.setState({
dropZoneValues: this.state.dropZoneValues.concat(layout),
});
this.setState({
dropzones: this.state.dropzones.concat(dropzone),
});
}
swapItems(i1, i2, y) {
console.log('swapping', i1, i2);
var height = y < 0 ? this.state.dropzones[i1].layout.height : -this.state.dropzones[i1].layout.height;
Animated.spring(
this.state.dropzones[i2].state.pan,
{toValue:{
x: 0,
y: -y-height
}}
).start();
var clone = this.state.items.clone();
console.log(clone);
clone.swap(i1, i2);
console.log(clone);
this.setState({
items: clone
});
}
render() {
console.log('state', this.state);
return (
<View style={styles.container}>
{this.state.items.map((i, index) =>
<Draggable key={index}
dropZoneValues={this.state.dropZoneValues}
setDropZoneValues={this.setDropZoneValues.bind(this)}
char={i}
index={index}
swapItems={this.swapItems.bind(this)}
/>
)}
<View style={{ zIndex: 100, backgroundColor: 'red' }}><Text>foo</Text></View>
<View style={{ zIndex: -100, top: -10, backgroundColor: 'blue' }}><Text>bar</Text></View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'orange',
justifyContent: 'center',
alignItems: 'center',
},
dropzone: {
// margin: 5,
zIndex: -100,
width: 106,
height: 106,
borderColor: 'green',
borderWidth: 3,
backgroundColor: 'lightgreen',
},
draggable: {
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
width: 100,
height: 100,
borderWidth: 1,
borderColor: 'black'
},
image: {
width: 75,
height: 75
}
});
export default Playground;
EDIT2: zIndex
wpływa tylko rodzeństwo dzieci, więc musiałem umieścić go na rodzica (zielona ramka) zamiast Animated.View
.
Powód, dla którego zamiana działała tylko przez połowę czasu, polegał na tym, że ze względu na sposób, w jaki dodawałem układy w addDropzone
, czasami kończyły się one nieprzydatnością do użycia w inDropzone
. Kiedy sortuję układy, inDropzone
działa tak, jak bym się spodziewał.
Ogólnie rzecz biorąc, cała ta kwestia nadal wygląda jak GIANT HACK, więc jeśli ktoś, kto rzeczywiście wie, co robi, widzi błędy w mojej implementacji i może ją poprawić, to byłoby naprawdę świetnie. Przydałby się również podgląd, więc po przeciągnięciu nad strefą zrzutu pokazuje tymczasową wymianę tego, co ma się zmienić, lub inne użyteczne wskaźniki wizualne, które można wymyślić. Przeciąganie, upuszczanie i zamiana to bardzo powszechna funkcja aplikacji mobilnej, a jedyna dostępna tam biblioteka działa tylko na liście pionowej. Musiałem to zaimplementować od zera, ponieważ chciałem zrobić z tego sieć zdjęć.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image,
PanResponder,
Animated,
Alert,
} from 'react-native';
import _ from 'lodash';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
zIndex: 0,
backgroundColor: 'white',
};
}
handleOnLayout(event) {
const { addDropzone } = this.props;
const { layout } = event.nativeEvent;
this.layout = layout;
addDropzone(this, layout);
}
componentWillMount() {
const { inDropzone, swapItems, index } = this.props;
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture:() => true,
onMoveShouldSetPanResponderCapture:() => true,
onPanResponderGrant: (e, gestureState) => {
console.log('moving', index);
this.state.pan.setOffset({ x: this.state.pan.x._value, y: this.state.pan.y._value });
this.state.pan.setValue({ x: 0, y: 0 });
Animated.spring(this.state.scale, { toValue: 0.75, friction: 3 }).start();
this.setState({ backgroundColor: 'deepskyblue', zIndex: 1 });
},
onPanResponderMove: Animated.event([null, { dx: this.state.pan.x, dy: this.state.pan.y }]),
onPanResponderRelease: (e, gesture) => {
this.state.pan.flattenOffset();
Animated.spring(this.state.scale, { toValue: 1 }).start();
this.setState({ backgroundColor: 'white', zIndex: 0 });
let dropzone = inDropzone(gesture);
if (dropzone) {
console.log('in dropzone', dropzone.index);
// adjust into place
Animated.spring(this.state.pan, { toValue: {
x: dropzone.x - this.layout.x,
y: dropzone.y - this.layout.y,
} }).start();
if (index !== dropzone.index) {
swapItems(index, dropzone.index);
}
}
Animated.spring(this.state.pan, { toValue: { x: 0, y: 0 } }).start();
}
});
}
render() {
const { pan, scale, zIndex, backgroundColor } = this.state;
const [translateX, translateY] = [pan.x, pan.y];
const rotate = '0deg';
const imageStyle = {
transform: [{ translateX }, { translateY }, { rotate }, { scale }],
};
return (
<View
style={[styles.dropzone, { zIndex }]}
onLayout={event => this.handleOnLayout(event)}
>
<Animated.View
{...this._panResponder.panHandlers}
style={[imageStyle, styles.draggable, { backgroundColor }]}
>
<Image style={styles.image} source={{ uri: this.props.item }} />
</Animated.View>
</View>
);
}
}
const swap = (array, fromIndex, toIndex) => {
const newArray = array.slice(0);
newArray[fromIndex] = array[toIndex];
newArray[toIndex] = array[fromIndex];
return newArray;
}
class Playground extends Component {
constructor(props) {
super(props);
this.state = {
items: [
'https://files.graphiq.com/465/media/images/t2/Shiba_Inu_5187048.jpg',
'https://i.ytimg.com/vi/To8oesttqc4/hqdefault.jpg',
'https://vitaminsforpitbulls.com/wp-content/uploads/2013/06/english-bulldog-puppy-for-sale-909x1024.jpg',
'https://s-media-cache-ak0.pinimg.com/236x/20/16/e6/2016e61e8642c8aab60c71f6e3bcd004.jpg',
'https://pbs.twimg.com/profile_images/446566229210181632/2IeTff-V.jpeg',
'https://s-media-cache-ak0.pinimg.com/236x/fa/7b/18/fa7b185924d9d4d14a0623bc567f4e87.jpg',
],
dropzones: [],
dropzoneLayouts: [],
};
}
addDropzone(dropzone, dropzoneLayout) {
const { items, dropzones, dropzoneLayouts } = this.state;
// HACK: to make sure setting state does not re-add dropzones
if (items.length !== dropzones.length) {
this.setState({
dropzones: [...dropzones, dropzone],
dropzoneLayouts: [...dropzoneLayouts, dropzoneLayout],
});
}
}
inDropzone(gesture) {
const { dropzoneLayouts } = this.state;
// HACK: with the way they are added, sometimes the layouts end up out of order, so we need to sort by y,x (x,y doesn't work)
const sortedDropzoneLayouts = _.sortBy(dropzoneLayouts, ['y', 'x']);
let inDropzone = false;
sortedDropzoneLayouts.forEach((dropzone, index) => {
const inX = gesture.moveX > dropzone.x && gesture.moveX < dropzone.x + dropzone.width;
const inY = gesture.moveY > dropzone.y && gesture.moveY < dropzone.y + dropzone.height;
if (inX && inY) {
inDropzone = dropzone;
inDropzone.index = index;
}
});
return inDropzone;
}
swapItems(fromIndex, toIndex) {
console.log('swapping', fromIndex, '<->', toIndex);
const { items, dropzones } = this.state;
this.setState({
items: swap(items, fromIndex, toIndex),
dropzones: swap(dropzones, fromIndex, toIndex),
});
}
render() {
console.log(this.state);
return (
<View style={styles.container}>
{this.state.items.map((item, index) =>
<Draggable key={index}
item={item}
index={index}
addDropzone={this.addDropzone.bind(this)}
inDropzone={this.inDropzone.bind(this)}
swapItems={this.swapItems.bind(this)}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 60,
backgroundColor: 'orange',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
},
dropzone: {
// margin: 5,
zIndex: -1,
width: 106,
height: 106,
borderColor: 'green',
borderWidth: 3,
backgroundColor: 'lightgreen',
},
draggable: {
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
width: 100,
height: 100,
borderWidth: 1,
borderColor: 'black'
},
image: {
width: 75,
height: 75
}
});
export default Playground;
Edit3: Więc powyższe działa świetnie w symulatorze, ale jest bardzo powolny na rzeczywiste iPhone. Ładowanie zajmuje zbyt wiele czasu, zanim możesz przeciągnąć coś (~ 3 sekundy) i zamarza podczas zamiany przedmiotów (~ 1 sekunda). Próbuję dowiedzieć się dlaczego (prawdopodobnie moja straszna implementacja sortowania/pętli nad tablicami zbyt wiele razy, ale nie wiem jak inaczej to zrobić). Nie mogłem uwierzyć, jak znacznie wolniej jest na prawdziwym telefonie.
NAJNOWSZE: Idę na studia/wykorzystać te implementacje https://github.com/ollija/react-native-sortable-grid, https://github.com/fangwei716/30-days-of-react-native#day-18 aby dowiedzieć się, co zrobiłem źle. Bardzo trudno było je znaleźć (inaczej nie zrobiłbym tego od zera i nie napisałem tego pytania), więc mam nadzieję, że pomoże to komuś, kto próbuje zrobić to samo dla swojej aplikacji!
Wow https://github.com/ollija/react-native-sortable-grid! – kayla
Naprawdę chciałbym wiedzieć o tym wcześniej, zamiast po prostu grzebać w ciemności: https://github.com/fangwei716/30-days-of-react-native#day-18 W każdym razie, zapomnij o moim rozwiązaniu, ale ten link i powyższy link powinny pomóc osobom, które próbują to zrobić za pomocą swoich aplikacji. Dowiesz się, co zrobiłem źle, analizując ich implementacje. – kayla