Create now ➫ 🔗 kee.so
使用 Taro 开发 FUTAKE 小程序时,7 个与用户体验有关的优化。
参考 官方 Dark Mode 适配指南 添加 theme.json
,并在 app.config.js
中添加相关配置。
小程序自身 UI 的 Dark Mode,可使用 CSS 变量来控制,其它需要变化的色值,均源自 CSS 变量即可。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/dark-mode
▶ 点击查看代码
// less 主题文件
#theme() {
--dark: #000;
--darken: 0, 0, 0;
--light: #fff;
--lighten: 255, 255, 255;
--yellow: #ff9500;
--green: #34c759;
--blue: #007aff;
--indigo: #048;
--red: #ff3b30;
}
#dark-theme() {
--dark: #fff;
--darken: 255, 255, 255;
--light: #000;
--lighten: 0, 0, 0;
--yellow: #ff9500;
--green: #30d158;
--blue: #0a84ff;
--indigo: #bce;
--red: #ff453a;
}
page {
#theme();
}
@media (prefers-color-scheme: dark) {
page {
#dark-theme();
}
}
FUTAKE 实现了类似手机原生弹窗的效果 —— 按住弹窗体后,可上下拖动弹窗。
实现方式即监听 touch 相关事件,动态设置 CSS 偏移。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/draggable-modal
▶ 点击查看代码
// 核心代码(省略了工具函数)
const onTouchStart = useCallback(
(event) => {
if (stopClose) return;
const { pageX, pageY } = event.changedTouches[0];
startX.current = pageX;
startY.current = pageY;
startTime.current = Date.now();
},
[stopClose],
);
const onTouchMove = useCallback(
(event) => {
if (stopClose) return;
const diffY = getDiffY(event.changedTouches[0]);
if (!diffY) return;
setBottomRef.current(diffY);
},
[stopClose],
);
const onTouchEnd = useCallback(
(event) => {
if (stopClose) return;
const diffY = getDiffY(event.changedTouches[0]);
if (!diffY) {
setBottomRef.current(0);
return;
}
if (diffY > 200 || (diffY > 10 && Date.now() - startTime.current < 200)) {
onClose();
setTimeout(() => setBottomRef.current(0), 200);
return;
}
setBottomRef.current(0);
},
[onClose, stopClose],
);
使用自定义 tarBar,实现模糊半透明的毛玻璃效果,随着页面滚动 tabBar 一直动态变化。
使用 CSS 的 backdrop-filter
来实现。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/custom-tab-bar
▶ 点击查看代码
.tab-bar {
--lighten: 255, 255, 255;
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
backdrop-filter: blur(24px);
}
.tab {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(96px + constant(safe-area-inset-bottom));
height: calc(96px + env(safe-area-inset-bottom));
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.icon {
width: 44px;
height: 44px;
opacity: 0.2;
}
.tab.active .icon,
.tab.hover .icon {
opacity: 1;
}
@media (prefers-color-scheme: dark) {
.tab-bar {
--lighten: 0, 0, 0;
}
.icon {
filter: invert(1);
}
}
手机系统为左侧边缘向右滑返回,但如果屏幕过大,操作并不太顺手。
在一些 App 中,实现了直接在页面上右滑返回的效果,例如 Slack 和 Snapchat,体验非常顺滑。
在 Taro 小程序中,首先需要添加一个公共组件,页面均使用此公共组件包裹,然后在公共组件中监听 touch 相关事件。
这里的重点是需要计算滑动的角度,例如 →
这样的可以返回,但 ↘
和 ↓
这样的,应该忽略掉。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/touch-move-back
▶ 点击查看代码
// React Hooks(省略了工具函数)
const useMoveX = ({ toLeft, toRight, disable }) => {
const startX = useRef(0);
const startY = useRef(0);
const startTime = useRef(0);
const onTouchStart = useCallback(
(event) => {
if (disable) return;
const { pageX, pageY } = event.changedTouches[0];
startX.current = pageX;
startY.current = pageY;
startTime.current = Date.now();
},
[disable],
);
const getAbsAngle = useCallback((diffX, pageY) => {
const diffY = pageY - startY.current;
const angle = getAngle(diffX, diffY);
return Math.abs(angle);
}, []);
const onTouchEnd = useCallback(
(event) => {
if (disable) return;
const { pageX, pageY } = event.changedTouches[0];
const diffX = pageX - startX.current;
if (diffX > 0) {
if (!toRight || getAbsAngle(diffX, pageY) > 20) return;
if (diffX > 70 || (diffX > 10 && Date.now() - startTime.current < 200))
toRight();
} else {
if (!toLeft || getAbsAngle(diffX, pageY) < 160) return;
if (
diffX < -70 ||
(diffX < -10 && Date.now() - startTime.current < 200)
)
toLeft();
}
},
[disable, getAbsAngle, toLeft, toRight],
);
return { onTouchStart, onTouchEnd };
};
小程序原生的下拉加载也不错,但不够特别。FUTAKE 实现了毛玻璃下拉加载效果:
GIF 较模糊,强烈建议体验小程序的实际效果。
同样是监听 touch 事件,但实现更复杂一些,需要根据偏移,处理毛玻璃的模糊度,以及触发 loading 动画等。
在 React 中使用时,要注意将 loading 元素隔离开来,因为 loading 元素是不断 re-render 的。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/blur-loading
▶ 点击查看代码
// 部分核心代码(省略了相关组件)
const START = 40;
const END = 100;
const useBlurLoading = ({ hasTabBar }) => {
const [blur, setBlur] = useState(0);
const [dots, setDots] = useState(false);
const blurStyle = useMemo(() => {
if (blur < START) return undefined;
return { '--blur': `blur(${Math.floor((blur - START) / 3)}px)` };
}, [blur]);
const startLoading = useCallback((reqFn) => {
setBlur(60);
setDots(true);
const onEnd = () => {
setTimeout(() => {
setBlur(0);
setDots(false);
}, 300);
};
reqFn().then(shakePhone).finally(onEnd);
}, []);
const diffY = useRef(0);
const maxDiffY = useRef(0);
const hasLoading = useRef(false);
const hasReq = useRef(false);
const reqRef = useRef(null);
// change
const onTouchMoveChange = useCallback((absDiffY, onReq) => {
if (absDiffY < START || absDiffY > END + 20) return;
// 记录 diffY
diffY.current = absDiffY;
if (absDiffY > maxDiffY.current) maxDiffY.current = absDiffY;
// 记录请求函数
if (!reqRef.current) reqRef.current = onReq;
// 显示 blur
requestAnimationFrame(() => setBlur(absDiffY));
// 显示 dots 动画
if (!hasLoading.current && absDiffY > END) {
hasLoading.current = true;
setDots(true);
shakePhone();
return;
}
// 若未触发请求,停止 dots 动画
if (hasLoading.current && absDiffY < END && !hasReq.current) {
hasLoading.current = false;
setDots(false);
}
}, []);
// end
const onTouchEnd = useCallback(() => {
if (!reqRef.current) return;
// 应触发请求
const shouldReq =
hasLoading.current && Math.abs(diffY.current - maxDiffY.current) < 5;
if (shouldReq) {
// 发送请求
hasReq.current = true;
reqRef.current().finally(() => {
setTimeout(() => {
hasReq.current = false;
hasLoading.current = false;
setDots(false);
setBlur(0);
diffY.current = 0;
maxDiffY.current = 0;
reqRef.current = null;
}, 300);
});
return;
}
// 不触发请求
setTimeout(() => {
setBlur(0);
diffY.current = 0;
maxDiffY.current = 0;
}, 200);
}, [setBlur, setDots]);
const loadingEl = blur > 0 && (
<BlurLoading dots={dots} blurStyle={blurStyle} hasTabBar={hasTabBar} />
);
return { loadingEl, startLoading, onTouchMoveChange, onTouchEnd };
};
FUTAKE 使用 Swiper 组件,实现了类似抖音的上下滑动浏览。
但随着列表元素不断增加,小程序将变得卡顿,因为需要实现列表数据的动态化。
展示正在浏览的条目以及前后预载入条目,其它条目展示空元素占位即可。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/use-dynamic-list
▶ 点击查看代码
// React Hooks(省略了工具函数)
export const useDynamicList = (list, index, count = 5) => {
return useMemo(() => {
const len = list.length;
if (len <= count) return list;
const [before, after] = splitCount(count);
let start = index - before;
let end = index + after;
if (start < 0) start = 0;
if (end > len - 1) end = len - 1;
const res = [...Array(len)];
for (let i = start; i <= end; i++) {
res[i] = list[i];
}
return res;
}, [index, count, list]);
};
FUTAKE 实现了类似 Instagram 的对图片双击即可点赞的效果。
同时增加了「喜欢」展示红色 ❤️,「取消喜欢」展示白色 🤍 的逻辑。
完整代码 → github.com/nanxiaobei/taro-ux-kits/tree/main/double-click-like
▶ 点击查看代码
const LikeWrapper = ({ isLiked, likeRequest }) => {
const prevTime = useRef(0);
const [iconVisible, setIconVisible] = useState(false);
const [isRed, setIsRed] = useState(!isLiked);
// 双击喜欢
const onClick = useCallback(
async (event) => {
const startTime = event.timeStamp;
if (startTime - prevTime.current < 300) {
if (iconVisible) return;
setIsRed(!isLiked);
setIconVisible(true);
likeRequest({ isLiked }).finally(() => {
const timeLeft = 550 - (Date.now() - startTime);
const hideIcon = () => setIconVisible(false);
timeLeft > 0 ? setTimeout(hideIcon, timeLeft) : hideIcon();
});
}
prevTime.current = startTime;
},
[iconVisible, isLiked, likeRequest],
);
return (
<View className="like-wrapper" onClick={onClick}>
{iconVisible && <LikeIcon isRed={isRed} />}
</View>
);
};