Skip to content

Instantly share code, notes, and snippets.

@jan4984
Created August 7, 2017 08:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jan4984/17502a990a822a6f7bcd6ba6b29097de to your computer and use it in GitHub Desktop.
Save jan4984/17502a990a822a6f7bcd6ba6b29097de to your computer and use it in GitHub Desktop.
react-native dragging animation with custom bounds control
import React from 'react';
import {
View,
ListView,
Image,
Animated,
PanResponder,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {ToolBar} from "../component/ToolBar";
import Util from '../utils/Util';
const IMGS=[
require('../res/a_gradual.png'),
require('../res/b_gradual.png'),
require('../res/c_gradual.png'),
require('../res/d_gradual.png'),
require('../res/e_gradual.png'),
require('../res/f_gradual.png'),
require('../res/g_gradual.png'),
];
const HS={
diskTM:71,
diskProgH:178,
twoLineH:81,
ListTM:61,
};
const winWidth=Util.getWindowWidth();
const diskSize=218;
const diskMargin=winWidth/2-diskSize/2-32;
const pad=winWidth/2-diskSize/2;
const firstDiskCenter = winWidth/2;
const diskSpace=diskSize+diskMargin;
Util.getHeight(HS);
class ExpandedDisks extends React.Component{
constructor(props){
super(props);
const state={};
this.curStartOffset=0;
state.x=new Animated.Value(0);
state.idx=0;//current first visible disk
state.offset=0;//current view left offset
this.state = state;
const {count} = props;
this.dragging=false;
this.lastCenterIdx=0;
this.lastValue=0;//current dragging offset
this.width = pad*2/*left & right pad*/
+ (count*diskSize + (count-1)*diskMargin)/*joint disks*/;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => !this.dragging,
onStartShouldSetPanResponderCapture: (evt, gestureState) => !this.dragging,
onMoveShouldSetPanResponder: (evt, gestureState) => !this.dragging,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => !this.dragging,
onPanResponderGrant: (e, g) => {
this.dragging=true;
},
onPanResponderMove: (e, g) => {
const offset =this.state.offset;
if(offset+g.dx>0){
this.state.x.setValue(-offset);
this.lastValue=0;
}else if(offset+g.dx<-(this.width-winWidth)){
this.state.x.setValue(-(this.width-winWidth)-offset);
this.lastValue=-(this.width-winWidth);
}else {
this.state.x.setValue(g.dx);
this.lastValue=offset+g.dx;
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (e, g) => {
const offset =this.state.offset;
const screenLeft = -this.lastValue;
const screenCenter = screenLeft + winWidth/2;
let n=0;
//TODO: use division other than accumulate
while(Math.abs(firstDiskCenter+diskSpace*n-screenCenter)>=diskSpace/2){
n++;
}
console.log('n:',n,'left:',screenLeft, 'center:',screenCenter,'diskSpace:',diskSpace);
const distance = firstDiskCenter+diskSpace*n-screenCenter;
console.log('from value',this.lastValue-offset);
Animated.timing(this.state.x,{
duration:300,
toValue:-(distance-(this.lastValue-offset)),
}).start(()=>{
const finalValue=-(distance-(this.lastValue));
this.dragging=false;
this.state.x.setValue(0);
console.log('final value:',finalValue);
this.setState({offset:finalValue,idx:n});
});
},
onPanResponderTerminate: (evt, gestureState) => {
},
onShouldBlockNativeResponder: (evt, gestureState) => true,
});
}
renderDisks(){
const {count} =this.props;
const n = this.state.idx;
const s={
width:diskSize,
height:diskSize,
position:'absolute',
top:0,
};
const curPad=-this.state.offset+pad;
//we render 5 disks, L2 L1 C R1 R2.
//because drag from most left to most right will see L2 L1, and drag from most right to left will see R2 R1
const keys = [iL2,iL1,iC,iR1,iR2] = [n-2,n-1,n,n+1,n+2];
const imgIdxs=[iL2,iL1,iC,iR1,iR2].map(v=>v%IMGS.length);
console.log('center disk idx:',n);
if(this.lastCenterIdx>n){
//we can only drag one disk at one time, so n==this.lastCenterIdx-1, i.e. previous one. L1 become C
//this key for L1 at previous render() should be this.lastCenterIdx-1, EQs key for C at current render()
//this prevent screen blink because we not replace in-screen-rendered element in render()
/**
* now previous R2 become L2. key for L2 = this.lastCenterIdx+2=n+3,if we want to reuse the Image
* components, we should design a recycle-use keys
*/
}
this.lastCenterIdx=n;
return [
iL2>=0?<Image key={keys[0]} style={[s,{left:curPad-diskSpace*2}]} resizeMode='stretch' source={IMGS[imgIdxs[0]]}/>:null,
iL1>=0?<Image key={keys[1]} style={[s,{left:curPad-diskSpace*1}]} resizeMode='stretch' source={IMGS[imgIdxs[1]]}/>:null,
<Image key={keys[2]} style={[s,{left:curPad}]} resizeMode='stretch' source={IMGS[imgIdxs[2]]}/>,
iR1<count ? <Image key={keys[3]} style={[s,{left:curPad+diskSpace*1}]} resizeMode='stretch' source={IMGS[imgIdxs[3]]}/>:null,
iR2<count ? <Image key={keys[4]} style={[s,{left:curPad+diskSpace*2}]} resizeMode='stretch' source={IMGS[imgIdxs[4]]}/>:null,
];
}
render(){
return <Animated.View {...this._panResponder.panHandlers} style={[{
width:this.width,
height:diskSize,
position:'absolute',
left:this.state.offset,
},{
transform:[{translateX:this.state.x}]
}]}>
<LinearGradient colors={['#f00', '#00f'] }
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={{flex:1,width:this.width}}>
</LinearGradient>
<View style={{position:'absolute',left:0,top:0,width:this.width,height:diskSize}}>
{this.renderDisks()}
</View>
</Animated.View>;
}
}
export class MusicPlayer extends React.Component{
constructor(props){
super(props);
}
render(){
return <View style={{flex:1}}>
<ToolBar title={this.props.title||"减压"}/>
<View style={{marginTop:HS.diskTM,width:winWidth}}>
<ExpandedDisks count={5}/>
</View>
</View>
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment