1.React-Native手势系统
参考连接:
“指尖上的魔法” -- 谈谈React-Native中的手势
React-Native ListView拖拽交换Item
2.利用手势系统PanResponder开发图片拖拽删除Demo
要实现的效果如下图所示:
代码详解:
1.如何布局
关键点:
- 每个图片view都是绝对定位,这样才能脱离文档流来进行拖拽操作,并且图片整体区域距离页面顶部200距离;
- 每四个一行显示,利用计算得出每张图片的left值和top值。
我们把所有图片资源存放在state中:
this.state = {
imgs: [
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2908558741,1476032262&fm=27&gp=0.jpg',
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2660096790,1445343165&fm=27&gp=0.jpg',
'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=586492344,3997176522&fm=27&gp=0.jpg',
'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1138659146,799893005&fm=27&gp=0.jpg',
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2634329296,2422503635&fm=27&gp=0.jpg',
'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2793821546,4232499990&fm=27&gp=0.jpg',
],
showDelModal: false,
delText: '拖拽此处可以删除',
};
render方法:
render() {
return (
{
this.state.imgs.map((v, i) => {
let l = 0; // left
let t = 0; // top
if(i >= 0 && i <= 3){
l = (10 + IMG_WIDTH)*i + 10;
}else if(i > 3 && i <= 7){
l = (10 + IMG_WIDTH)*(i - 4) + 10;
}else if(i > 7){
l = 10;
};
t = Math.floor(i/4)*(10+IMG_HEIGHT)+10 + 200;
return (
this.items[i] = ref}
activeOpacity={0.2}
key={i}
>
this.pressImage(v, i)} style={[styles.imageStyle, {left: 0, top: 0}]}>
)
})
}
)
}
其中:{...this._panResponder.panHandlers}
是将手势系统应用于当前View节点;ref={ref => this.items[i] = ref}
是拿到每个View的DOM,因为我们要对每个View进行拖拽操作,通过ref可以设置它的style。
2.使用手势系统
我们通常在componentWillMount生命周期中使用手势系统:
componentWillMount(){
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {// 手指触碰屏幕那一刻触发
},
onPanResponderMove: (evt, gestureState) => {// 手指移动过程中触达
},
onPanResponderTerminationRequest: (evt, gestureState) => true,// 当有其他不同手势出现,响应是否中止当前的手势
onPanResponderRelease: (evt, gestureState) => {// 手指离开屏幕触发
},
onPanResponderTerminate: (evt, gestureState) => { // 当前手势中止触发
}
});
实际上,我们只需要操作三个方法即可:
- 手指触发的时候,成为响应者,记录此时的位置信息(onPanResponderGrant)
- 手指移动过程中,根据计算移动信息,主要就是left、top值,来确定view的位置(onPanResponderMove)
- 手指释放的时候,根据位置信息来决定是否删除该view,还是恢复原来的位置(onPanResponderRelease)
分别看一下每个方法的应用:
手指触碰触发的方法
onPanResponderGrant: (evt, gestureState) => { // 手指触碰屏幕那一刻触发
const {pageX, pageY, locationY, locationX} = evt.nativeEvent; // pageY是相对于根节点的位置,locationY是相对于元素自己
this.index = this._getIdByPosition(pageX, pageY);
this.preY = pageY - locationY; // 保存当前正确点击item的位置,为了后面移动item
this.preX = pageX - locationX; // 保存当前正确点击item的位置,为了后面移动item
let item = this.items[this.index];
item.setNativeProps({
style: {
shadowColor: "#000",
shadowOpacity: 0.7,
shadowRadius: 5,
shadowOffset: {height: 4, width: 2},
elevation: 15,
zIndex: 999
}
});
this.setState({
showDelModal: true
});
// 删除区域出来
// this.slideAniValue.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: 0,
duration: 300,
easing: Easing.linear,// 线性的渐变函数
}).start();
},
两个参数: evt, gestureState
evt: 使用evt.nativeEvent可以拿到类似于web的event对象,常用的两个属性pageX, locationX,其中pageX代表手指相对于根节点的位置;locationX代表手指在当前元素的位置。
gestureState: 这个gestureState是一个对象,包含手势进行过程中更多的信息,其中比较常用的几个是:
- dx/dy:手势进行到现在的横向/纵向相对位移
- vx/vy:此刻的横向/纵向速度
- numberActiveTouches:responder上的触摸的个数
1.this.index = this._getIdByPosition(pageX, pageY)
这是为了得到到底触摸了哪个view,然后可以通过this.index
拿到他的dom对象;
2.this.preY
和this.preX
是保存当前点击的view的位置,为了之后的拖拽移动做准备;
3.item.setNativeProps
修改该dom属性,使其突出显示;
手指移动触发的方法
onPanResponderMove: (evt, gestureState) => {
let top = this.preY + gestureState.dy;
let left = this.preX + gestureState.dx;
let item = this.items[this.index];
item.setNativeProps({
style: {top: top, left: left},
});
if(top >= HEIGHT- IMG_HEIGHT - 60){ // 图片进入删除区域
this.setState({
delText: '松开删除',
});
}else{
this.setState({
delText: '拖拽此处可以删除'
})
}
},
1.根据当前的手指位置,确定移动的view的位置;
2.移动到删除区域,提示“松开删除”,否则是“拖拽此处可以删除”;
手指离开屏幕时触发的方法
onPanResponderRelease: (evt, gestureState) => { // 手指离开屏幕触发
this.setState({
showDelModal: false
});
// 删除区域隐藏
// this.state.slideOutBottom.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: -60,
duration: 300,
easing: Easing.linear,// 线性的渐变函数
}).start();
if(this.state.delText == '松开删除'){
// 删除图片
this.delImage(this.index);
}else{
const shadowStyle = {
shadowColor: "#000",
shadowOpacity: 0,
shadowRadius: 0,
shadowOffset: {height: 0, width: 0,},
elevation: 0,
zIndex: 1
};
let item = this.items[this.index];
// 回到原来的位置
item.setNativeProps({
style: {...shadowStyle, top: this._getTopValueYById(this.index).top, left: this._getTopValueYById(this.index).left}
});
}
},
抬起手指的时候,需要做两件事:
1.如果移动到删除区域,松开就删除当前view;
2.如果没有移动到删除区域就松开,那么恢复位置,删除区域撤销;
删除照片的方法
delImage(index) {
// 删除照片
this.imgDelAni(index);
let cacheData = this.state.imgs;
cacheData.splice(index,1);
// 调整位置
this.setState({
imgs: cacheData
});
let l = 0; // left
let t = 0; // top
if(index>=0 && index<=3){
l = (10+IMG_WIDTH)*index + 10;
}else if(index>3 && index<=7){
l = (10+IMG_WIDTH)*(index-4) + 10;
}else if(index>7){
l = 10;
};
t = Math.floor(index/4)*(10+IMG_HEIGHT)+10 + 200;
this.items[index].setNativeProps({
style: {
left: l,
top: t,
zIndex: 1
}
})
}
1.删除照片,更新删除后的照片数据;
2.重新调整位置;
根据view的index,来确定它的位置信息
_getTopValueYById(id) {
let top = 0;
let left = 0;
if(id >= 0 && id <= 3){
left = (10 + IMG_WIDTH)*id + 10;
}else if(id > 3 && id <= 7){
left = (10 + IMG_WIDTH)*(id - 4) + 10;
}else if(id > 7){
left = 10;
};
top = Math.floor(id/4)*(10+IMG_HEIGHT)+10 + 200;
return {
top,
left
}
}
1.10表示图片view之间的间距;
2.200表示图片区域盒子距离也买你顶部的top值,并且必须是固定的;
根据手指的当前位置确定是哪一个图片view
_getIdByPosition(pageX, pageY) {
let w = IMG_WIDTH;
let h = IMG_HEIGHT;
let id = -1;
if(pageY >= 210 && pageY <= 210 + h){
// 在第一排点击
if(pageX >= 10 && pageX <= 10 + w){
id = 0;
}
if(pageX >= 20 + w && pageX <= 20 + w + w){
id = 1;
}
if(pageX >= 30 + 2*w){
id = 2;
}
if(pageX >= 40 + 3*w){
id = 3;
}
}
if(pageY >= 210 + 20 + h && pageY <= 210 + 20 + h + h){
// 在第二排点击
if(pageX >= 10 && pageX <= 10 + w){
id = 4;
}
if(pageX >= 20 + w && pageX <= 20 + w + w){
id = 5;
}
if(pageX >= 30 + 2*w){
id = 6;
}
if(pageX >= 40 + 3*w){
id = 7;
}
}
if(pageY >= 210 + 20 + h + h + 10 && pageY <= 210 + 20 + h + h + 10 + h){
// 在第三排点击的
if(pageX >= 10 && pageX <= 10 + w){
id = 8;
}
}
return id;
}
完整代码:
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View,
TouchableOpacity,
BackHandler,
Image,
ViewPagerAndroid,
DeviceEventEmitter,
PanResponder,
TouchableWithoutFeedback,
Animated,
Easing
} from 'react-native';
import {WIDTH, HEIGHT} from '../utils/config';
import Button from '../components/Button';
const IMG_WIDTH = (WIDTH-52) / 4;
const IMG_HEIGHT = (WIDTH-40) / 4;
export default class ImageView extends Component<{}> {
constructor(props){
super(props);
this.state = {
imgs: [
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2908558741,1476032262&fm=27&gp=0.jpg',
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2660096790,1445343165&fm=27&gp=0.jpg',
'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=586492344,3997176522&fm=27&gp=0.jpg',
'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1138659146,799893005&fm=27&gp=0.jpg',
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2634329296,2422503635&fm=27&gp=0.jpg',
'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2793821546,4232499990&fm=27&gp=0.jpg',
],
showDelModal: false,
delText: '拖拽此处可以删除',
};
this.slideAniValue = new Animated.Value(-60);
this.items = [];
}
static navigationOptions = ({navigation}) => {
return {
title: "照片拖拽删除",
headerLeft: (
navigation.goBack(null)}>返回
)
}
}
componentDidMount() {
//注册通知
DeviceEventEmitter.addListener('ChangeImgData',(data)=>{
//接收到详情页发送的通知,刷新首页的数据,改变按钮颜色和文字,刷新UI
this.setState({
imgs: data.imgData
})
});
}
pressImage(v, i) {
const {navigation} = this.props;
navigation.navigate('ImageShowScreen', {uri: v, index: i, images: this.state.imgs});
}
_getIdByPosition(pageX, pageY) {
let w = IMG_WIDTH;
let h = IMG_HEIGHT;
let id = -1;
if(pageY >= 210 && pageY <= 210 + h){
// 在第一排点击
if(pageX >= 10 && pageX <= 10 + w){
id = 0;
}
if(pageX >= 20 + w && pageX <= 20 + w + w){
id = 1;
}
if(pageX >= 30 + 2*w){
id = 2;
}
if(pageX >= 40 + 3*w){
id = 3;
}
}
if(pageY >= 210 + 20 + h && pageY <= 210 + 20 + h + h){
// 在第二排点击
if(pageX >= 10 && pageX <= 10 + w){
id = 4;
}
if(pageX >= 20 + w && pageX <= 20 + w + w){
id = 5;
}
if(pageX >= 30 + 2*w){
id = 6;
}
if(pageX >= 40 + 3*w){
id = 7;
}
}
if(pageY >= 210 + 20 + h + h + 10 && pageY <= 210 + 20 + h + h + 10 + h){
// 在第三排点击的
if(pageX >= 10 && pageX <= 10 + w){
id = 8;
}
}
return id;
}
_getTopValueYById(id) {
let top = 0;
let left = 0;
if(id >= 0 && id <= 3){
left = (10 + IMG_WIDTH)*id + 10;
}else if(id > 3 && id <= 7){
left = (10 + IMG_WIDTH)*(id - 4) + 10;
}else if(id > 7){
left = 10;
};
top = Math.floor(id/4)*(10+IMG_HEIGHT)+10 + 200;
return {
top,
left
}
}
componentWillMount(){
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => { // 手指触碰屏幕那一刻触发
const {pageX, pageY, locationY, locationX} = evt.nativeEvent; // pageY是相对于根节点的位置,locationY是相对于元素自己
this.index = this._getIdByPosition(pageX, pageY);
this.preY = pageY - locationY; // 保存当前正确点击item的位置,为了后面移动item
this.preX = pageX - locationX; // 保存当前正确点击item的位置,为了后面移动item
let item = this.items[this.index];
item.setNativeProps({
style: {
shadowColor: "#000",
shadowOpacity: 0.7,
shadowRadius: 5,
shadowOffset: {height: 4, width: 2},
elevation: 15,
zIndex: 999
}
});
this.setState({
showDelModal: true
});
// 删除区域出来
// this.slideAniValue.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: 0,
duration: 300,
easing: Easing.linear,// 线性的渐变函数
}).start();
},
onPanResponderMove: (evt, gestureState) => {
let top = this.preY + gestureState.dy;
let left = this.preX + gestureState.dx;
let item = this.items[this.index];
item.setNativeProps({
style: {top: top, left: left},
});
if(top >= HEIGHT- IMG_HEIGHT - 60){ // 图片进入删除区域
this.setState({
delText: '松开删除',
});
}else{
this.setState({
delText: '拖拽此处可以删除'
})
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true, // 当有其他不同手势出现,响应是否中止当前的手势
onPanResponderRelease: (evt, gestureState) => { // 手指离开屏幕触发
this.setState({
showDelModal: false
});
// 删除区域隐藏
// this.state.slideOutBottom.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: -60,
duration: 300,
easing: Easing.linear,// 线性的渐变函数
}).start();
if(this.state.delText == '松开删除'){
// 删除图片
this.delImage(this.index);
}else{
const shadowStyle = {
shadowColor: "#000",
shadowOpacity: 0,
shadowRadius: 0,
shadowOffset: {height: 0, width: 0,},
elevation: 0,
zIndex: 1
};
let item = this.items[this.index];
// 回到原来的位置
item.setNativeProps({
style: {...shadowStyle, top: this._getTopValueYById(this.index).top, left: this._getTopValueYById(this.index).left}
});
}
},
onPanResponderTerminate: (evt, gestureState) => { // 当前手势中止触发
}
});
}
imgDelAni(index) {
}
delImage(index) {
// 删除照片
this.imgDelAni(index);
let cacheData = this.state.imgs;
cacheData.splice(index,1);
// 调整位置
this.setState({
imgs: cacheData
});
let l = 0; // left
let t = 0; // top
if(index>=0 && index<=3){
l = (10+IMG_WIDTH)*index + 10;
}else if(index>3 && index<=7){
l = (10+IMG_WIDTH)*(index-4) + 10;
}else if(index>7){
l = 10;
};
t = Math.floor(index/4)*(10+IMG_HEIGHT)+10 + 200;
this.items[index].setNativeProps({
style: {
left: l,
top: t,
zIndex: 1
}
})
}
render() {
return (
{
this.state.imgs.map((v, i) => {
let l = 0; // left
let t = 0; // top
if(i >= 0 && i <= 3){
l = (10 + IMG_WIDTH)*i + 10;
}else if(i > 3 && i <= 7){
l = (10 + IMG_WIDTH)*(i - 4) + 10;
}else if(i > 7){
l = 10;
};
t = Math.floor(i/4)*(10+IMG_HEIGHT)+10 + 200;
return (
this.items[i] = ref}
activeOpacity={0.2}
key={i}
>
this.pressImage(v, i)} style={[styles.imageStyle, {left: 0, top: 0}]}>
)
})
}
{this.state.delText}
{
this.state.showDelModal &&
}
)
}
}
const styles = StyleSheet.create({
imgContainer: {
flex: 1
},
imageStyle: {
width: IMG_WIDTH,
height: IMG_HEIGHT,
position: 'absolute',
borderRadius: 3,
},
delWraper:{
width: WIDTH,
height: 60,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'red',
position: 'absolute',
// bottom: 0,
left: 0,
zIndex: 998
},
shadowModal:{
width: WIDTH,
height: HEIGHT,
position: 'absolute',
backgroundColor: '#000',
opacity: 0.4,
zIndex: 888,
bottom: 0,
left: 0,
}
});