Skip to content

Instantly share code, notes, and snippets.

@dive2Pro
Created December 1, 2021 01:16
Show Gist options
  • Save dive2Pro/1122df0d25e68e7af64a0f0c6d436084 to your computer and use it in GitHub Desktop.
Save dive2Pro/1122df0d25e68e7af64a0f0c6d436084 to your computer and use it in GitHub Desktop.
给 flatlist 添加左滑&drag&drop 功能
import React, { createContext, Fragment, useContext, useEffect, useReducer, useRef, useState } from 'react';
import {
findNodeHandle,
TouchableHighlight,
Animated,
Button,
UIManager,
StyleSheet,
Text,
View,
TextInput,
PanResponder,
FlatList,
Easing,
TouchableOpacity
} from 'react-native';
import _ from 'lodash';
import NativeUtil from '../lib/native';
import { Page, RadioButton, Row } from '../baseComponents';
const PubSub = {
subs: {},
sub(name, cb) {
if (!this.subs[name]) {
this.subs[name] = [];
}
const index = this.subs[name].push(cb);
return () => {
if (this.subs[name]) this.subs[name].splice(index, 1);
};
},
emit(name, value) {
if (this.subs[name]) {
console.log('emit', name);
this.subs[name].forEach(cb => cb(value));
}
if (this.subs['*']) {
this.subs['*'].forEach(cb => {
cb(value);
});
}
}
};
class Todo {
status = 'OPEN';
title = '';
desc = '';
due = null;
images = [];
sync = -1; // -1 未保存; 0, 保存中; 1 已保存
index = 0;
/**
*
* @param {TodoManager} store
*/
constructor(manager) {
this.manager = manager;
}
isOpen() {
return this.status === 'OPEN';
}
isFinish() {
return this.status === 'FINISH';
}
_update() {
this.manager.update(this);
}
updateTitle(title) {
this.title = title;
this._update();
}
updateDesc(desc) {
this.desc = desc;
this._update();
}
toggle() {
this.status = this.status === 'FINISH' ? 'OPEN' : 'FINISH';
this._update();
}
setDue(dueDate) {
this.due = dueDate;
this._update();
}
updateImages(images) {
this.images = images;
this._update();
}
remove() {
this.manager.delete(this);
}
toString() {
return {
status: this.status,
desc: this.desc,
title: this.title,
images: this.images,
sync: this.sync,
index: this.index
};
}
}
const EventName = {
// 其实可以换成 list 和 item 两个就能够满足了。
// 在 hooks 中 listContainer 只关心 list 什么时候更新,更新了什么会在 render 中反映出来
refreshing: 'refreshing',
delete: 'delete',
add: 'add',
loadMore: 'loadMore',
item: id => 'item' + id
};
function useForceUpdate() {
return useReducer(i => i + 1, 0)[1];
}
function useTodoItem(propTodo) {
const forceUpdate = useForceUpdate();
useEffect(
() => {
const unSub = PubSub.sub(EventName.item(propTodo.id), todo => {
forceUpdate();
});
return unSub;
},
[propTodo.id]
);
// console.log(propTodo.toString(), ' --- ');
return { todo: propTodo };
}
function useCbState(initOrFn) {
const ref = useRef(initOrFn);
const [, _setState] = useState(initOrFn);
const setState = initOrFn => {
_setState(prev => {
let result = initOrFn;
if (typeof initOrFn === 'function') {
result = initOrFn(prev);
}
ref.current = result;
return result;
});
};
return [() => ref.current, setState];
}
function Swiper(props) {
const [rightActionLayout, setRightActionLayout] = useCbState({});
const pan = useRef(new Animated.ValueXY()).current;
const [rightOpen, setRightOpen] = useCbState(false);
const [isSwip, setIsSwip] = useState(false);
const xyValueRef = useRef({
x: 0,
y: 0
});
useEffect(
() => {
props.onSwipper && props.onSwipper(rightOpen() || isSwip);
},
[isSwip, rightOpen()]
);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
return Math.abs(gestureState.dx) > 5;
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
return Math.abs(gestureState.dx) > 5;
},
onMoveShouldSetPanResponder: (evt, gesture) => {
// 只有在向左滑动的时候
console.log(evt, ' move should ,');
return false;
},
onPanResponderGrant: () => {
pan.setOffset({
x: xyValueRef.current.x
});
pan.setValue({
x: 0,
y: 0
});
},
onPanResponderMove: (evt, gesture) => {
if (!rightOpen() && gesture.dx > 0) {
return;
}
setIsSwip(true);
pan.setValue({
x: gesture.dx
});
},
onPanResponderRelease: (evt, gestureState) => {
setIsSwip(false);
// 当释放的时候,要对
console.log('release: !', pan.x._value, rightOpen());
// 向左滑
if (gestureState.dx < 0) {
// 关闭状态
if (!rightOpen()) {
if (xyValueRef.current.x < rightActionLayout().width * -0.3) {
// 打开 rightActions
Animated.spring(pan, {
toValue: { x: rightActionLayout().width * -1, y: 0 }
}).start(({ finished }) => {
finished && pan.flattenOffset();
});
setRightOpen(true);
return;
}
}
if (xyValueRef.current.x <= rightActionLayout().width * -1) {
close();
return;
}
}
pan.flattenOffset();
Animated.spring(pan, {
toValue: { x: 0, y: 0 }
}).start();
setRightOpen(false);
},
onPanResponderReject: evt => {
// console.log(evt, ' = evnt ');
}
})
).current;
useEffect(
() => {
pan.addListener(xy => {
xyValueRef.current = xy;
});
},
[pan]
);
const close = () => {
Animated.spring(pan, {
toValue: { x: 0, y: 0 }
}).start(({ finished }) => {
finished && pan.flattenOffset();
});
};
const passenger = {
hide: () => {
close();
setRightOpen(false);
}
};
return (
<View>
<TouchableHighlight
style={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
zIndex: -1
}}
onPress={() => {
console.log('touch Press');
}}
onLayout={({ nativeEvent: { layout } }) => {
setRightActionLayout(layout);
}}
>
{props.rightActions && props.rightActions(passenger)}
</TouchableHighlight>
<Animated.View
style={{
transform: [{ translateX: pan.x }],
zIndex: 1,
backgroundColor: 'white'
}}
{...panResponder.panHandlers}
>
{props.children}
</Animated.View>
</View>
);
}
function TodoItem(props) {
const { todo } = useTodoItem(props.todo);
return (
<Swiper
onSwipper={swipe => {
props.onSwipe(swipe);
}}
rightActions={action => (
<Row>
<Text>删除</Text>
<Text
onPress={() => {
console.log(' click hello world');
action.hide();
}}
>
Hello World!
</Text>
</Row>
)}
>
<DropObj>
{trigger => {
return (
<TouchableOpacity
minDuration={800}
onLongPress={() => {
props.drag();
}}
style={{
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomWidth: 0.5,
borderBottomColor: 'red'
}}
>
<RadioButton
status={!todo.isOpen() ? 'checked' : ''}
onPress={() => {
todo.toggle();
console.log(' toggle ');
trigger();
}}
/>
<View style={{}}>
<TextInput
onChangeText={text => {
console.log(text, ' tesxt ');
todo.updateTitle(text);
}}
style={{ fontSize: 18 }}
value={todo.title}
placeholder="例如: "
/>
<Text>{todo.desc}</Text>
{todo.images.length ? <Row /> : <Text />}
<Text>{todo.due ? todo.due : '设置时间'}</Text>
<Text>{todo.sync}</Text>
</View>
</TouchableOpacity>
);
}}
</DropObj>
</Swiper>
);
}
const DB = {
ids: new Set(),
async load() {
const keys = await Storage.loadMasterData('DB_KEYS');
if (keys) {
this.ids = new Set(JSON.parse(keys));
} else {
this.ids = new Set();
}
const result = [];
for await (const id of this.ids) {
result.push(await Storage.loadMasterData(id));
}
return result;
},
save(obj) {
console.log(obj, 'save', obj.toString());
// localStorage.setItem(obj.id, obj);
this.ids.add(obj.id);
Storage.saveMasterData('DB_KEYS', this.ids);
Storage.saveMasterData(obj.id, obj.toString());
},
remove(obj) {
console.log(obj, 'remove', this.ids);
this.ids.delete(obj.id);
//
// localStorage.removeItem(obj.id);
Storage.saveMasterData('DB_KEYS', this.ids);
Storage.clearMasterData(obj.id, obj);
},
clean() {
// localStorage.clear();
// Storage.remove
Storage.clearMasterData('DB_KEYS');
this.ids.forEach(id => {
Storage.clearMasterData(id);
});
}
};
function TodoList(props) {
const ref = React.useRef();
if (!ref.current) {
ref.current = new TodoManager(new TodoStore(DB));
}
const manager = ref.current;
const forceUpdate = React.useReducer(i => i + 1, 0)[1];
useEffect(() => {
[EventName.delete, EventName.add, EventName.refreshing, EventName.loadMore].forEach(name => {
PubSub.sub(name, () => {
forceUpdate();
});
});
return () => {
PubSub.subs = [];
};
}, []);
const [isSwipe, setSwipe] = useState(new Set());
// console.log('isSwipe = ', isSwipe);
return (
<View style={{ flex: 1 }}>
<DragAndDropFlatList
data={manager.todos}
renderItem={({ item, drag, dragEnd }) => {
return (
<TodoItem
onSwipe={swipping =>
setSwipe(prev => {
if (swipping) {
prev.add(item.id);
} else {
prev.delete(item.id);
}
return new Set(prev);
})
}
drag={isSwipe.size ? null : drag}
dragEnd={dragEnd}
key={item.id}
todo={item}
/>
);
}}
refreshing={manager.refreshing}
onRefresh={() => manager.init()}
keyExtractor={(item, index) => {
return item.id;
}}
enableMJLoadMore={true}
mjLoadingMore={manager.loadingMore}
onMJLoadMore={() => manager.loadMore()}
/>
<Button
onPress={() => {
manager.add();
}}
title="Example button"
/>
</View>
);
}
function Spacer(props) {
useEffect(
() => {
if (props.open) {
Animated.timing(h, {
duration: 150,
toValue: props.h
}).start();
} else {
Animated.timing(h, {
toValue: 0,
duration: 150
}).start();
// h.setValue(0);
}
return () => {
h.stopAnimation();
};
},
[props.open]
);
// useInterval(() => {
// console.log('interval ---')
// } , 1000)
//
// useEffect(() => {
// console.log('Mounted');
// }, []);
const h = useRef(new Animated.Value(props.open ? props.h : 0)).current;
return (
<Animated.View style={{ height: h }}>
{props.children}
<Text>{props.index} = index</Text>
</Animated.View>
);
}
function DragAndDropFlatList(props) {
const flRef = useRef();
const [, setII] = useState(1);
const [hoverComponent, setHoverComponent] = useCbState();
const [dragging, setDragging] = useCbState(false);
const [data, setData] = useCbState(props.data);
useEffect(
() => {
setData(props.data);
},
[props.data]
);
const drag = (item, index) => {
return event => {
const comp = props.renderItem({
item,
drag: () => {}
});
heightRef.current = _positions[index].height;
setHoverComponent({
comp,
item,
index
});
};
};
// TODO 计算 spacer 出现的位置
const _positions = useRef([]).current;
const _spacerOpens = useRef([]).current;
const initPositions = () => {
if (!props.data.every((item, i) => _positions[i])) {
return;
}
if (!flRef.current.layout) {
return;
}
_positions[0].y = 0;
for (let i = 1; i < _positions.length; i++) {
_positions[i].y = _positions[i - 1].y + _positions[i - 1].height;
}
};
const renderPlaceHolder = hc => {
return (
<View
style={{
backgroundColor: 'lightgreen',
width: _positions[hc.index].width,
height: _positions[hc.index].height
}}
/>
);
};
const heightRef = useRef(0);
const renderItem = ({ item, index }) => {
const space1 = (
<Spacer
index={-1}
key={`${dragging() ? 'true' : 'false'} -1`}
open={dragging() && _spacerOpens[-1]}
h={index === 0 && _spacerOpens[-1] ? heightRef.current : 0}
>
{dragging() && renderPlaceHolder(hoverComponent())}
</Spacer>
);
console.log(_spacerOpens[index], ' = open ');
const space2 = (
<Spacer
index={index}
// key 设置为 dragging(),滑动时它会改变,导致 Spacer 重启,而不是更新,这样就不会有动画。
// 关闭时, 也是重启,此时会变成立即消失,配合消失的 dragging 给人一种回弹的感觉
key={`${dragging() ? 'true' : 'false'} ${index}`}
open={_spacerOpens[index]}
h={heightRef.current}
>
{dragging() && renderPlaceHolder(hoverComponent())}
</Spacer>
);
return (
<Animated.View
onLayout={({ nativeEvent: { layout } }) => {
if (dragging()) {
return;
}
_positions[index] = layout;
_spacerOpens[index] = false;
initPositions();
}}
>
{space1}
<View
style={
dragging() && hoverComponent()?.index === index
? {
position: 'absolute'
}
: hoverComponent()?.index === index
? {}
: {}
}
>
{props.renderItem({ item, drag: drag(item, index), dragEnd })}
</View>
{space2}
</Animated.View>
);
};
const hoverAnim = useRef(new Animated.Value(0)).current;
const dragAndDropResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (evt, gesture) => {
if (hoverComponent()) {
return true;
}
return false;
},
onPanResponderGrant: (evt, gesture) => {
dragStart(hoverComponent().index);
setDragging(true);
},
onPanResponderMove: (evt, gesture) => {
// 同时处理 FlatList 的滑动
hoverAnim.setValue(gesture.dy);
prevDyRef.current = gesture.dy;
anime(gesture.dy, draggingScrollOffsetDiffRef.current);
},
onPanResponderReject: evt => false,
onPanResponderRelease: evt => {
dragEnd();
}
})
).current;
const forceUpdate = useForceUpdate();
const prevDyRef = useRef(0);
// 当 drag&moving 同步滚动 scrollView
const scrollViewAnim = (dy, scrollOffset) => {
if (!dragging()) {
return;
}
const index = hoverComponent().index;
const { y } = _positions[index];
const currentPosition = dy + y + scrollOffset;
const scrollSpeed = 100;
const scrollViewHeight = flRef.current.layout.height;
const scrollOffsetY = contentOffsetRef.current.y;
const startRect = [scrollOffsetY, scrollOffsetY + scrollViewHeight * 0.15];
const endRect = [scrollOffsetY + scrollViewHeight * 0.8, scrollOffsetY + scrollViewHeight];
const isInDuration = (y, range) => {
return y >= range[0] && y <= range[1];
};
const shouldScrollUp = isInDuration(currentPosition, startRect);
const shouldScrollDown = isInDuration(currentPosition, endRect);
// 根据 currentPosition 在 rect 中的位置,计算出百分比,百分比 * speed 则为滑动的速度
let result;
if (shouldScrollUp) {
const percent = (startRect[1] - currentPosition) / startRect[1];
result = scrollSpeed * -1 * percent;
console.log(percent, result);
} else if (shouldScrollDown) {
const percent = (currentPosition - endRect[0]) / (endRect[1] - endRect[0]);
result = scrollSpeed * percent;
console.log(percent, result);
}
if (result) {
flRef.current.scrollToOffset({
animated: false,
offset: contentOffsetRef.current.y + result
});
}
};
const anime = (dy, scrollOffset = 0) => {
const openTo = num => {
// console.log(' to num ', num);
setII(prev => {
if (prev !== num) {
resetSpacerOpens();
_spacerOpens[num] = true;
}
return num;
});
};
// console.log('anime = dy = ', dy);
scrollViewAnim(dy, scrollOffset);
// dy 移动时以 index 如果 dy
const index = hoverComponent().index;
const { y, height } = _positions[index];
// 聚合成一条线
// 将超过 index 的 positions 都减去一个 height
const positions = _.cloneDeep(_positions);
for (let i = index + 1; i < positions.length; i++) {
positions[i].y -= height;
}
// 先找到 y + dy 最大的目标 items
// 还要加上 dragging 时滑动滚动的距离
let targetIndex = -1;
const diff = y + dy + scrollOffset;
for (let i = positions.length - 1; i >= 0; i--) {
if (diff >= positions[i].y) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) {
return openTo(-1);
}
// console.log('target = ', targetIndex, positions[targetIndex], ' y + dy = ', y + dy);
if (diff > Math.ceil(positions[targetIndex].y + positions[targetIndex].height / 2)) {
openTo(targetIndex);
} else if (diff > positions[targetIndex].y) {
openTo(targetIndex - 1);
}
};
const dragStart = index => {
// offset 要等于 y - flatList 滑动的距离
setII(index);
// dragging 起时:
_spacerOpens[index] = true;
hoverAnim.setOffset(_positions[index].y - contentOffsetRef.current.y);
hoverAnim.setValue(0);
forceUpdate();
};
const resetSpacerOpens = () => {
for (let k in _spacerOpens) {
_spacerOpens[k] = false;
}
};
const drop = () => {
//
const { index } = hoverComponent();
const opened = _spacerOpens.findIndex(opened => opened);
if (index !== opened) {
// 移动位置
const oldData = data();
const oldItem = oldData[index];
oldData.splice(index, 1);
if (opened <= 1) {
oldData.splice(opened + 1, 0, oldItem);
} else {
if (opened < index) {
oldData.splice(opened + 1, 0, oldItem);
} else {
oldData.splice(opened, 0, oldItem);
}
}
// console.log(opened, index, ' ======');
// console.log(oldData, ' ==== ', data());
setData([...oldData]);
props.onChange && props.onChange([...oldData]);
}
};
const dragEnd = () => {
drop();
resetSpacerOpens();
setDragging(false);
hoverAnim.flattenOffset();
setHoverComponent(null);
draggingScrollOffsetStartRef.current = 0;
draggingScrollOffsetDiffRef.current = 0;
prevDyRef.current = 0;
setII(-1);
};
const renderHoverComponent = obj => {
const position = _positions[obj.index];
console.log(position, '= posiiton');
return (
<Animated.View
style={[
{
position: 'absolute',
width: position.width,
height: position.height,
shadowColor: '#999',
shadowOffset: { width: 1, height: -2 }, // 设置阴影偏移,该值会设置整个阴影的偏移,width可以看做x,height可以看做y,x向右为正,y向下为正
shadowOpacity: 1,
shadowRadius: 1.5
},
{
transform: [
{
translateY: hoverAnim
}
]
}
]}
>
{obj.comp}
</Animated.View>
);
};
useEffect(
() => {
if (flRef.current) {
setTimeout(() => {
UIManager.measure(findNodeHandle(flRef.current), (x, y, width, height, pageX, pageY, ...rest) => {
console.log(rest, ' = rest ', height, pageY, y);
flRef.current.layout = {
pageY,
x,
y,
width,
height,
pageX
};
});
});
}
},
[props.data]
);
const contentOffsetRef = useRef({ y: 0 });
const draggingScrollOffsetStartRef = useRef(0);
const draggingScrollOffsetDiffRef = useRef(0);
return (
<Animated.View style={{ backgroundColor: 'red' }} {...dragAndDropResponder.panHandlers}>
{/* TODO 处理添加了这些元素后 hoverComponent 的位置 */}
{/* <Text onPress={() => setII(ii + 1)}>+++</Text> */}
{/* <Text onPress={() => setII(ii - 1)}>---</Text> */}
<FlatList
ref={flRef}
onLayout={({ nativeEvent }) => {
console.log(JSON.stringify(nativeEvent), ' ---- ', nativeEvent);
}}
scrollEnabled={!hoverComponent()}
onScroll={({ nativeEvent: { contentOffset } }) => {
if (dragging() && !draggingScrollOffsetStartRef.current) {
draggingScrollOffsetStartRef.current = contentOffset.y;
} else if (dragging()) {
draggingScrollOffsetDiffRef.current = contentOffset.y - draggingScrollOffsetStartRef.current;
anime(prevDyRef.current, draggingScrollOffsetDiffRef.current);
}
contentOffsetRef.current = contentOffset;
console.log('onscroll -> ', contentOffset);
}}
{...props}
data={data()}
renderItem={renderItem}
refreshing={dragging() ? false : props.refreshing}
enableMJLoadMore={dragging() ? false : props.enableMJLoadMore}
/>
{dragging() && renderHoverComponent(hoverComponent())}
</Animated.View>
);
}
class TodoManager {
refreshing = false; // refreshing
loadingMore = false;
constructor(store) {
this.store = store;
this.store.manager = this;
this.init();
}
async init() {
try {
// 🟢
// 1. 更新(上拉)之前,要将本地所有未处理的先上传到服务端
// 2. 再将本地的内容清空。本地数据的唯一用处就是用来保证实时性
// 🔴
// 0. 只有在本地无数据的时候,才使用服务端数据初始化
// 1. 本地已经是最新的了,即使刷新,也是以本地的为准
PubSub.emit('refreshing', true);
this.refreshing = true;
await this.store.refresh();
} catch (e) {
console.error('初始化错误', e);
} finally {
this.refreshing = false;
PubSub.emit('refreshing', false);
}
}
/**
*
* 加载更多
*
*/
async loadMore() {
try {
PubSub.emit(EventName.loadMore, true);
this.loadingMore = true;
await this.store.loadMore();
} catch (e) {
console.error('加载更多错误:🔴', e);
} finally {
this.loadingMore = false;
PubSub.emit(EventName.loadMore, false);
}
}
get todos() {
return this.store.getTodos();
}
add() {
const todo = new Todo(this);
todo.id = id++;
todo.title = id + ' title ';
this.store.add(todo);
PubSub.emit(EventName.add, todo);
return todo;
}
/**
*
* @param {Todo} todo
*/
update(todo) {
this._save(todo);
}
/**
*
* @param {Todo} todo
*/
delete(todo) {
this._delete(todo);
}
deleteAll() {
this._deleteAll();
}
/**
*
* @param {Todo} todo
*/
async _save(todo) {
try {
todo.sync = 0;
PubSub.emit(EventName.item(todo.id), todo);
await this.store.update(todo);
todo.sync = 1;
} catch (e) {
todo.sync = -1;
}
PubSub.emit(EventName.item(todo.id), todo);
}
async _delete(todo) {
await this.store.delete(todo);
PubSub.emit(EventName.delete, todo);
}
async _deleteAll() {
await this.store.clean();
PubSub.emit(EventName.delete);
}
}
function fakeUpdateTodo(todo) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
}
let id = 1;
let index = 1;
class TodoStore {
todos = [];
manager = null; // TodoManager
constructor(db) {
this.db = db;
this.update = _.debounce(this.update, 500);
}
async refresh() {
// clean todos
// clean database
await this._clean();
console.log('refresh');
await this.loadMore();
}
async loadMore() {
// 写入 db
// 写入 todos
function fakeLoadMore() {
return new Promise(resolve =>
setTimeout(() => {
resolve(
new Array(8).fill(0).map(() => ({
id: id++,
title: id + 'title',
index: index++
}))
);
}, 200)
);
}
const data = await fakeLoadMore();
console.log('loadmore, ', data);
this.todos = this.todos.concat(
data.map(item => {
const todo = new Todo(this.manager);
todo.id = item.id;
todo.title = item.title;
todo.index = item.index;
return todo;
})
);
}
getTodos() {
console.log('get todos', this);
return this.todos;
}
/**
*
* @param {Todo} todo
*/
async add(todo) {
try {
todo.sync = 0;
// 异步执行,这里要考虑请求接口的方案了
// 写入 db
this.todos.push(todo);
this._updateToDB(todo);
await fakeUpdateTodo(todo);
todo.sync = 1;
this._removeFromDB(todo);
} catch (e) {
console.error('更新 todo 时发生错误:', e);
todo.sync = -1;
}
}
/**
*
* @param {Todo} todo
*/
async update(todo) {
// 异步执行,这里要考虑请求接口的方案了
// 写入 db
this._updateToDB(todo);
await fakeUpdateTodo(todo);
this._removeFromDB(todo);
}
async delete(todo) {
try {
// 异步执行,这里要考虑请求接口的方案了
// 写入 db
todo.sync = 0;
await fakeUpdateTodo(todo);
todo.sync = 1;
this._removeFromDB(todo);
} catch (e) {
console.error('删除 todo 时发生错误:', e);
todo.sync = -1;
}
}
async _clean() {
await this._sync();
await this._cleanDB();
this.todos = [];
}
_updateToDB(todo) {
this.db.save(todo);
}
_removeFromDB(todo) {
this.db.remove(todo);
}
async _sync() {
// 同步数据到服务端
const allUnSync = await this.db.load();
if (allUnSync.length > 0) {
//
}
for await (const todo of allUnSync) {
// 请求接口。更新单条数据
await this.update(todo);
}
}
_cleanDB() {
this.db.clean();
}
}
function App() {
useEffect(() => {
NativeUtil.hideSplash();
}, []);
return (
<Page>
<DropToProvider>
<View style={styles.header}>
<DropToTarget>
<Text style={styles.title}>React Native for Web</Text>
</DropToTarget>
</View>
<TodoList />
</DropToProvider>
</Page>
);
}
const dropToCtx = createContext();
function DropToProvider(props) {
const targetPosition = useRef({
x: 0,
y: 0
});
const obj = useRef({
comp: null,
coordinate: {
width: 0,
height: 0
}
});
const anim = useRef(new Animated.Value(0)).current;
// round anim
const opacityInterpolate = anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [0, 1, 0]
});
const roundInterpolate = anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [0, 50, 0],
easing: Easing.bounce
});
const widthInterpolate = anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [obj.current.coordinate.width, 80, 0]
});
const heightInterpolate = anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [obj.current.coordinate.height, 80, 0]
});
const setTarget = (pageX, pageY) => {
targetPosition.current = {
pageX,
pageY
};
};
const forceUpdate = useForceUpdate();
const roadToAnim = useRef(new Animated.ValueXY(targetPosition.current.pageX, targetPosition.current.pageY)).current;
const start = (comp, coordinate) => {
obj.current = { comp, coordinate };
anim.setValue(0);
roadToAnim.setValue({
x: coordinate.pageX,
y: coordinate.pageY
});
shakeAnim.setValue(0);
Animated.sequence([
Animated.parallel(
// 初始化位置, 变形
[
Animated.timing(roadToAnim, {
toValue: {
x: 200,
y: coordinate.pageY - 50
},
duration: 250
}),
Animated.timing(anim, {
toValue: 1,
duration: 250
})
],
{}
),
// 触发移动动画
Animated.parallel(
[
Animated.timing(anim, { toValue: 2, duration: 250 }),
Animated.timing(roadToAnim, {
toValue: {
x: targetPosition.current.pageX + 50,
y: targetPosition.current.pageY
},
duration: 250
})
],
{
stopTogether: true
}
),
Animated.spring(shakeAnim, { toValue: 2, duration: 150 })
]).start();
forceUpdate();
};
const shakeAnim = useRef(new Animated.Value(0)).current;
const rotate = useRef(
shakeAnim.interpolate({
inputRange: [0, 1, 2],
outputRange: ['0deg', '45deg', '0deg']
})
).current;
return (
<dropToCtx.Provider
value={{
setTarget,
start,
targetStyle: {
transform: [
{
rotate
}
]
}
}}
>
{props.children}
<Animated.View
style={{
overflow: 'hidden',
borderWidth: 1,
borderColor: 'red',
borderRadius: roundInterpolate,
transform: [],
opacity: opacityInterpolate,
width: widthInterpolate,
height: heightInterpolate,
position: 'absolute',
top: roadToAnim.y,
left: roadToAnim.x
}}
>
{obj.current.comp}
</Animated.View>
</dropToCtx.Provider>
);
}
function useMounted(cb) {
useEffect(cb, []);
}
export function useUnmounted(cb) {
useEffect(() => cb, []);
}
function DropObj(props) {
const ctx = useContext(dropToCtx);
const ref = useRef();
const trigger = () => {
const comp = props.children();
UIManager.measure(findNodeHandle(ref.current), (x, y, width, height, pageX, pageY) => {
console.log(pageY, pageX, x, y, ' = x x x x x x');
ctx.start(comp, { pageX, pageY, x, y, width, height });
});
};
return <Fragment>{React.cloneElement(props.children(trigger), { ref })}</Fragment>;
}
function DropToTarget(props) {
const ctx = useContext(dropToCtx);
const ref = useRef();
useMounted(() => {
// 找到组件的位置
UIManager.measure(findNodeHandle(ref.current), (x, y, width, height, pageX, pageY, ...rest) => {
console.log(pageY, pageX);
ctx.setTarget(pageX, pageY);
});
});
console.log(ctx.targetStyle, ' = ');
return (
<Fragment>
<Animated.View style={ctx.targetStyle}>{props.children}</Animated.View>
<View ref={ref} />
</Fragment>
);
}
const styles = StyleSheet.create({
app: {
marginHorizontal: 'auto',
maxWidth: 500
},
logo: {
height: 80
},
header: {
padding: 20
},
title: {},
text: {},
link: {
color: '#1B95E0'
},
code: {
fontFamily: 'monospace, monospace'
}
});
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment