拖动,拖放和交换项目动画?

时间:2016-10-30 07:14:36

标签: react-native

我似乎让拖放部分工作,但不知道如何进行交换。也不确定如何解决z-index问题(似乎用Animated.View做了一些可疑的事情)。

enter image description here

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;

编辑:我尝试了交换,但似乎只能工作一半左右。此外,zIndex仍然让我疯狂。我正在打印像{color} {zIndex}这样的状态,因此您可以看到它更新为100,但它似乎没有生效。将颜色改为蓝色似乎有效...我很困惑。

enter image description here

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只影响孩子的兄弟姐妹,所以我不得不把它放在父母(绿色框)而不是Animated.View上。

交换仅工作一半的原因是因为我在addDropzone中添加布局的方式,它们有时最终无法在inDropzone中使用。当我对布局进行排序时,inDropzone的工作方式与我的预期相符。

总的来说,整个事情仍然感觉像是一个 GIANT HACK ,所以如果真正了解他们正在做什么的人看到我的实施中存在缺陷并且可以改进它,那就是&#39; d真的很棒此外,进行预览会很不错,因此当您拖放放置区时,它会显示要更改的内容的临时交换,或者您可以想到的任何其他有用的可视指示器。拖放和交换是移动应用程序非常常见的功能,其中唯一的库只能在垂直列表中使用。我需要从头开始实现这个,因为我想让它成为一个照片网格。

enter image description here

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:因此上述功能在模拟器中效果很好,但在实际的iPhone上速度极慢。在开始加载(约3秒)之前加载一段时间(~3秒)并在交换项目(约1秒)时冻结需要很长时间。试图找出原因(可能是我的糟糕实现排序/循环数组太多次,但不知道如何做到这一点)。我无法相信它在实际手机上的速度要慢得多。

最新消息:我只是去研究/使用这些实现https://github.com/ollija/react-native-sortable-gridhttps://github.com/fangwei716/30-days-of-react-native#day-18来找出我做错了什么。他们很难找到(或者我不会从头开始做这个并发布这个问题),所以我希望这可以帮助那些试图为他们的应用做同样事情的人!

1 个答案:

答案 0 :(得分:0)

首先针对性能问题,我建议使用Direct Manipulation。当您想要转换图像时,需要使用setNativeProps:

this.refs['YOUR_IMAGE'].setNativeProps({style: {
  transform: [{ translateX }, { translateY }, { rotate }, { scale }],
}});

在react-native中我们有两个领域,JavaScript和Native端,我们之间有一个桥梁。

这是理解React Native性能的主要关键之一。每个领域本身都很快。当我们从一个领域转移到另一个领域时,通常会出现性能瓶颈。为了构建性能React Native应用程序,我们必须将桥上的传递保持在最低限度。

您可以使用示例here阅读更多内容。

其次,请参阅性能监视器(摇动您的设备或Command-D并选择Show Perf Monitor)。重要的部分是视图,上面的数字是你屏幕上的视图数量,底部数字通常更大,但通常表示你有一些可以改进/重构的内容。