7 user experience related optimizations for developing FUTAKE applets using Taro.
👉 Click to experience FUTAKE 🌁
1. Dark Mode
See the official Dark Mode Adaptation guide to add theme. Json and add the relevant configuration in app.config.js.
The Dark Mode of the UI of the small program itself can be controlled by USING CSS variables, and other color values that need to be changed can be derived from CSS variables.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
// less theme file
#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(a); }@media (prefers-color-scheme: dark) {
page {
#dark-theme();
}
}
Copy the code
2. Draggable Modal
FUTAKE implements a similar effect to the mobile phone’s native popup window — after holding down the popup window, you can drag the window up and down.
The implementation method is to listen to touch related events, dynamically set CSS offset, to further improve performance, using native small program WXS to write.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
// WXS core code (omit utility functions)
module.exports = {
onTouchStart: function (event, ownerInstance) {
var obj = ownerInstance.getState();
if(! obj.setOffset) {var moveWrapper = ownerInstance.selectComponent("#move-wrapper");
var setWrapperStyle = moveWrapper.setStyle;
obj.raf = moveWrapper.requestAnimationFrame;
obj.setTimeout = getSetTimeout(obj.raf);
obj.setOffset = function (offset) {
setWrapperStyle(
offset === 0? {}, {"margin-bottom": "-" + Math.ceil(offset) + "px"}); }; }var pos = event.changedTouches[0];
obj.startX = pos.pageX;
obj.startY = pos.pageY;
obj.startTime = Date.now();
obj.prevOffset = null;
obj.reset = false;
},
onTouchMove: function (event, ownerInstance) {
var obj = ownerInstance.getState();
var offset = getOffset(event, obj, "touchMove");
if(! offset)return;
obj.raf(function () {
obj.setOffset(offset);
});
},
onTouchEnd: function (event, ownerInstance) {
var obj = ownerInstance.getState();
var offset = getOffset(event, obj);
if(! offset) {if (obj.reset) obj.setOffset(0);
return;
}
obj.raf(function () {
if (offset > 150 || (offset > 10 && Date.now() - obj.startTime < 200)) {
ownerInstance.callMethod("onClose");
obj.setTimeout(function () {
obj.setOffset(0);
}, 200);
return;
}
obj.setOffset(0); }); }};Copy the code
3. Frosted glass tabBar
Use a custom tarBar to achieve a blurred translucent frosted glass effect that changes dynamically as the page scrolls.
This method is implemented using the CONTEXT context Bath-filter of the CSS.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
.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); }}Copy the code
4. Swipe right to return to the page
The phone system slides back to the right from the left edge, but if the screen is too large, it doesn’t work very well.
Some apps, such as Slack and Snapchat, allow you to swipe right back to a page, and the experience is very smooth.
In the Taro applet, you first need to add a public component that wraps all pages, and then listen for touch related events in the public component.
The focus here is to calculate the sliding Angle, such as → can be returned, but such as ↘ and ↓, should be ignored.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
// React Hooks (omitted utility functions)
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 };
};
Copy the code
5. Ground-glass pull-down loading effect
The native pull-down loading of applets is good, but not special. FUTAKE implements the ground-glass pull-down loading effect:
GIF is fuzzy, it is highly recommended to experience the actual effect of the small program.
It also listens to touch events, but the implementation is more complicated. It needs to deal with the blur of frosted-glass according to offset, and trigger loading animation, etc.
When using React, it is important to isolate the loading element because it is constantly re-render.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
// Part of the core code (omit related components)
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(The ${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;
/ / record diffY
diffY.current = absDiffY;
if (absDiffY > maxDiffY.current) maxDiffY.current = absDiffY;
// record the request function
if(! reqRef.current) reqRef.current = onReq;/ / display the blur
requestAnimationFrame(() = > setBlur(absDiffY));
// Display dots animation
if(! hasLoading.current && absDiffY > END) { hasLoading.current =true;
setDots(true);
shakePhone();
return;
}
// Stop dots animation if the request is not triggered
if(hasLoading.current && absDiffY < END && ! hasReq.current) { hasLoading.current =false;
setDots(false); }} []);// end
const onTouchEnd = useCallback(() = > {
if(! reqRef.current)return;
// The request should be triggered
const shouldReq =
hasLoading.current && Math.abs(diffY.current - maxDiffY.current) < 5;
if (shouldReq) {
// Send the request
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;
}
// Do not trigger the request
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 };
};
Copy the code
Swiper Dynamic list data
FUTAKE uses the Swiper component for tiktok-like scrolling.
But as the list elements grow, the applets become sluggish because the list data needs to be dynamic.
Show the items being browsed and the pre-loaded items before and after, and the other items show empty element placeholders.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
// React Hooks (omitted utility functions)
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]);
};
Copy the code
7. Double-click to like the animation
FUTAKE implements the instagram-like effect of double-clicking a like on an image.
The logic of “like” showing red ❤️ and “unlike” showing white 🤍 has also been added.
Complete code → github.com/nanxiaobei/…
▶ Click to view the code
const LikeWrapper = ({ isLiked, likeRequest }) = > {
const prevTime = useRef(0);
const [iconVisible, setIconVisible] = useState(false);
const[isRed, setIsRed] = useState(! isLiked);// Double-click like
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>
);
};
Copy the code
👉 Welcome to FUTAKE 🗺
👉 Visit FUTAKE at 🗺