1.React-Native手勢系統(tǒng)
參考連接:
“指尖上的魔法” -- 談談React-Native中的手勢
2.利用手勢系統(tǒng)PanResponder開發(fā)圖片拖拽刪除Demo
要實現(xiàn)的效果如下圖所示:
代碼詳解:
1.如何布局
關(guān)鍵點:
- 每個圖片view都是絕對定位,這樣才能脫離文檔流來進行拖拽操作咸这,并且圖片整體區(qū)域距離頁面頂部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 (
<View style={styles.imgContainer}>
{
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 (
<View
style={[styles.imageStyle, {left: l, top: t}]}
{...this._panResponder.panHandlers}
ref={ref => this.items[i] = ref}
activeOpacity={0.2}
key={i}
>
<TouchableOpacity onPress={() => this.pressImage(v, i)} style={[styles.imageStyle, {left: 0, top: 0}]}>
<Image source={{uri: v}} style={[styles.imageStyle, {left: 0, top: 0}]} />
</TouchableOpacity>
</View>
)
})
}
</View>
)
}
其中:{...this._panResponder.panHandlers}
是將手勢系統(tǒng)應用于當前View節(jié)點;ref={ref => this.items[i] = ref}
是拿到每個View的DOM暑椰,因為我們要對每個View進行拖拽操作霍转,通過ref可以設(shè)置它的style。
2.使用手勢系統(tǒng)
我們通常在componentWillMount生命周期中使用手勢系統(tǒng):
componentWillMount(){
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {// 手指觸碰屏幕那一刻觸發(fā)
},
onPanResponderMove: (evt, gestureState) => {// 手指移動過程中觸達
},
onPanResponderTerminationRequest: (evt, gestureState) => true,// 當有其他不同手勢出現(xiàn)一汽,響應是否中止當前的手勢
onPanResponderRelease: (evt, gestureState) => {// 手指離開屏幕觸發(fā)
},
onPanResponderTerminate: (evt, gestureState) => { // 當前手勢中止觸發(fā)
}
});
實際上避消,我們只需要操作三個方法即可:
- 手指觸發(fā)的時候,成為響應者召夹,記錄此時的位置信息(onPanResponderGrant)
- 手指移動過程中岩喷,根據(jù)計算移動信息,主要就是left戳鹅、top值均驶,來確定view的位置(onPanResponderMove)
- 手指釋放的時候,根據(jù)位置信息來決定是否刪除該view枫虏,還是恢復原來的位置(onPanResponderRelease)
分別看一下每個方法的應用:
手指觸碰觸發(fā)的方法
onPanResponderGrant: (evt, gestureState) => { // 手指觸碰屏幕那一刻觸發(fā)
const {pageX, pageY, locationY, locationX} = evt.nativeEvent; // pageY是相對于根節(jié)點的位置妇穴,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
});
// 刪除區(qū)域出來
// this.slideAniValue.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: 0,
duration: 300,
easing: Easing.linear,// 線性的漸變函數(shù)
}).start();
},
兩個參數(shù): evt, gestureState
evt: 使用evt.nativeEvent可以拿到類似于web的event對象腾它,常用的兩個屬性pageX, locationX死讹,其中pageX代表手指相對于根節(jié)點的位置瞒滴;locationX代表手指在當前元素的位置。
gestureState: 這個gestureState是一個對象赞警,包含手勢進行過程中更多的信息妓忍,其中比較常用的幾個是:
- dx/dy:手勢進行到現(xiàn)在的橫向/縱向相對位移
- vx/vy:此刻的橫向/縱向速度
- numberActiveTouches:responder上的觸摸的個數(shù)
1.this.index = this._getIdByPosition(pageX, pageY)
這是為了得到到底觸摸了哪個view,然后可以通過this.index
拿到他的dom對象愧旦;
2.this.preY
和this.preX
是保存當前點擊的view的位置世剖,為了之后的拖拽移動做準備;
3.item.setNativeProps
修改該dom屬性笤虫,使其突出顯示旁瘫;
手指移動觸發(fā)的方法
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){ // 圖片進入刪除區(qū)域
this.setState({
delText: '松開刪除',
});
}else{
this.setState({
delText: '拖拽此處可以刪除'
})
}
},
1.根據(jù)當前的手指位置祖凫,確定移動的view的位置;
2.移動到刪除區(qū)域酬凳,提示“松開刪除”惠况,否則是“拖拽此處可以刪除”;
手指離開屏幕時觸發(fā)的方法
onPanResponderRelease: (evt, gestureState) => { // 手指離開屏幕觸發(fā)
this.setState({
showDelModal: false
});
// 刪除區(qū)域隱藏
// this.state.slideOutBottom.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: -60,
duration: 300,
easing: Easing.linear,// 線性的漸變函數(shù)
}).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.如果移動到刪除區(qū)域稠屠,松開就刪除當前view;
2.如果沒有移動到刪除區(qū)域就松開台诗,那么恢復位置完箩,刪除區(qū)域撤銷赐俗;
刪除照片的方法
delImage(index) {
// 刪除照片
this.imgDelAni(index);
let cacheData = this.state.imgs;
cacheData.splice(index,1);
// 調(diào)整位置
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.刪除照片拉队,更新刪除后的照片數(shù)據(jù);
2.重新調(diào)整位置阻逮;
根據(jù)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表示圖片區(qū)域盒子距離也買你頂部的top值叔扼,并且必須是固定的事哭;
根據(jù)手指的當前位置確定是哪一個圖片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: (
<TouchableOpacity >
<Text style={styles.titleText} onPress={()=>navigation.goBack(null)}>返回</Text>
</TouchableOpacity>
)
}
}
componentDidMount() {
//注冊通知
DeviceEventEmitter.addListener('ChangeImgData',(data)=>{
//接收到詳情頁發(fā)送的通知,刷新首頁的數(shù)據(jù)瓜富,改變按鈕顏色和文字鳍咱,刷新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) => { // 手指觸碰屏幕那一刻觸發(fā)
const {pageX, pageY, locationY, locationX} = evt.nativeEvent; // pageY是相對于根節(jié)點的位置,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
});
// 刪除區(qū)域出來
// this.slideAniValue.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: 0,
duration: 300,
easing: Easing.linear,// 線性的漸變函數(shù)
}).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){ // 圖片進入刪除區(qū)域
this.setState({
delText: '松開刪除',
});
}else{
this.setState({
delText: '拖拽此處可以刪除'
})
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true, // 當有其他不同手勢出現(xiàn),響應是否中止當前的手勢
onPanResponderRelease: (evt, gestureState) => { // 手指離開屏幕觸發(fā)
this.setState({
showDelModal: false
});
// 刪除區(qū)域隱藏
// this.state.slideOutBottom.setValue(-60);
Animated.timing(this.slideAniValue, {
toValue: -60,
duration: 300,
easing: Easing.linear,// 線性的漸變函數(shù)
}).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) => { // 當前手勢中止觸發(fā)
}
});
}
imgDelAni(index) {
}
delImage(index) {
// 刪除照片
this.imgDelAni(index);
let cacheData = this.state.imgs;
cacheData.splice(index,1);
// 調(diào)整位置
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 (
<View style={styles.imgContainer}>
{
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 (
<View
style={[styles.imageStyle, {left: l, top: t}]}
{...this._panResponder.panHandlers}
ref={ref => this.items[i] = ref}
activeOpacity={0.2}
key={i}
>
<TouchableOpacity onPress={() => this.pressImage(v, i)} style={[styles.imageStyle, {left: 0, top: 0}]}>
<Image source={{uri: v}} style={[styles.imageStyle, {left: 0, top: 0}]} />
</TouchableOpacity>
</View>
)
})
}
<Animated.View style={[styles.delWraper, {bottom: this.slideAniValue}]}>
<Text style={{color: '#fff'}}>{this.state.delText}</Text>
</Animated.View>
{
this.state.showDelModal &&
<View style={styles.shadowModal}></View>
}
</View>
)
}
}
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,
}
});