- 卷积神经网络学习笔记——ZFNet(Tensorflow实现)
- 24年3月使用VS22编译TelegramDesktop
- STM32中RFID模块(MFRC522)简单应用
- java反序列化-CC1
最近在研究Figma的一些功能设计, 对其中的数值输入框可以直接鼠标拖拽调整的这个设计印象非常深刻. 这里用了其他网友的一张动态截图演示一下效果. 。
实际这个拖拽的功能不止看到的这么简单, 在深度研究使用之后, 发现这个拖拽可以无限的拖动, 当鼠标超出网页后会自动回到另一端然后继续拖动, 而且按住shift键, 可以调整单次数值变化的间隔值为10, 细节非常的丰富. 。
这篇文章, 我们就来尝试实现一下这个支持拖拽调整数值的输入框组件. 实现基于: typescript + react + tailwindcss + shadcn-ui 实现的功能有
下面拆解一下组件的实现逻辑 。
通常的拖动更新数值, 实现是现在元素上监听 mousedown, 然后在document上监听 mousemove 和 mouseup. 在 mousemove 中处理位置的计算更新逻辑, 参考draggable-input-v1. 首先, 构建state记录的输入框值和鼠标的位置.
const [snapshot, setSnapshot] = useState(value);
const [mousePos, setMousePos] = useState<number[] | null>(null);
当鼠标在左侧标签按下的时候, 记录鼠标的初始位置 。
const onDragStart = useCallback(
(position: number[]) => {
setMousePos(position);
},
[]
);
给label的jsx绑定mousedown事件 。
return (
<div
onMouseDown={(e) => {
onDragStart([e.clientX, e.clientY]);
}}
className="cursor-ew-resize absolute top-0 left-0 h-full flex items-center"
>
{label}
</div>
);
然后在document上监听, mousemove和mouseup事件, 用于拖拽更新数值 。
useEffect(() => {
// Only change the value if the drag was actually started.
const onUpdate = (event: MouseEvent) => {
const { clientX } = event;
if (mousePos) {
const newSnapshot = snapshot + clientX - mousePos[0];
onChange(newSnapshot);
}
};
// Stop the drag operation now.
const onEnd = (event: MouseEvent) => {
const { clientX } = event;
if (mousePos) {
const newSnapshot = snapshot + clientX - mousePos[0];
setSnapshot(newSnapshot);
setMousePos(null);
onChange(newSnapshot);
}
};
document.addEventListener('mousemove', onUpdate);
document.addEventListener('mouseup', onEnd);
return () => {
document.removeEventListener('mousemove', onUpdate);
document.removeEventListener('mouseup', onEnd);
};
}, [mousePos, onChange, snapshot]);
这个时候, 我们已经可以通过鼠标拖拽来调整数值了. 。
但是, 和Figma的不太一样
基于这些问题, 可以猜测, Figma的这种无限拖拽, 其实是隐藏了鼠标后, 用虚拟的光标模拟鼠标操作的. 我们仔细留意一下Figma的输入框拖拽按下的瞬间, 发现鼠标样式是有变化的, 进一步证明我们的猜想. 。
那么下面就是考虑, 如果用JS隐藏鼠标, 并且保证鼠标不会移出页面可见区域窗口. 这让我想到了, 在浏览一下Web3D效果的页面时, 鼠标点击后进入场景交互, 此时鼠标用于控制第一人称视角相机, 不论怎么拖动都不会跑到别的屏幕上. 。
于是问了一下Claude, 确实有这样的一个API, Element.requestPointerLock() 可以让我们锁定鼠标在某个元素内, 默认使用esc可以推出锁定, 也可以用 document.exitPointerLock() 手动退出锁定. 那么我们要做的就是在鼠标按下(mousedown)的时候, 进入锁定, 鼠标抬起(mouseup)的时候退出锁定. 参考案例里面, v2版本中draggable-label.tsx文件中的的部分代码 。
const onDragStart = useCallback(
(position: number[]) => {
document.body.requestPointerLock();
setMousePos(position);
},
[]
);
const onEnd = () => {
setMousePos(null);
setCursorPosition(null);
document.exitPointerLock();
};
解决了锁定鼠标的问题, 下一步就是虚拟光标模拟鼠标移动的实现. 这一步不算复杂, 我们找一个水平resize的光标对应的svg, 在需要的时候, 控制他显示然后调整位置即可. 这里我直接封装了一个hooks, 参考 use-ew-resize-cursor.tsx 的实现 。
import { useEffect, useState } from 'react';
const EWResizeCursorID = 'ZMeta_ew_resize_cursor';
export const useEWResizeCursor = () => {
const [position, setPosition] = useState<number[] | null>(null);
useEffect(() => {
let ewCursorEle = document.querySelector(
`#${EWResizeCursorID}`
) as HTMLDivElement;
if (position == null) {
ewCursorEle && document.body.removeChild(ewCursorEle);
return;
}
if (ewCursorEle == null) {
ewCursorEle = document.createElement('div');
ewCursorEle.id = EWResizeCursorID;
ewCursorEle.style.cssText =
'position: fixed; top: 0; left: 0;transform: translate3d(-50%, -50%, 0);';
ewCursorEle.innerHTML = `<svg t="1721283130691" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4450" width="32" height="32"><path d="M955.976575 533.675016l-166.175122 166.644747a28.148621 28.148621 0 0 1-39.845904 0c-10.945883-11.018133-10.945883-28.900021 0-39.954279l119.465463-119.826713H160.575743l119.465462 119.826713c11.018133 11.018133 11.018133 28.900021 0 39.954279a28.148621 28.148621 0 0 1-39.845904 0l-166.102872-166.644747c-5.888379-5.852254-8.381006-13.691385-8.019756-21.422141-0.36125-7.658506 2.131377-15.461511 8.019756-21.34989l166.102872-166.608622a28.148621 28.148621 0 0 1 39.845904 0c11.018133 11.018133 11.018133 28.900021 0 39.954279L160.575743 484.075355h708.845269l-119.465463-119.826713c-10.945883-11.018133-10.945883-28.900021 0-39.954279a28.148621 28.148621 0 0 1 39.845904 0l166.175122 166.608622c5.888379 5.888379 8.381006 13.691385 7.911381 21.34989 0.4335 7.730756-2.059127 15.569886-7.911381 21.422141z" fill="#bfbfbf" p-id="4451"></path></svg>`;
document.body.appendChild(ewCursorEle);
}
const [x, y] = position;
ewCursorEle.style.top = y + 'px';
ewCursorEle.style.left = x + 'px';
}, [position]);
return { setPosition };
};
实现的内容很简单, 当传入位置时, 手动构建一个svg的光标在body下, 然后动态设置top和left的值. 那么在mousemove 的时候, 获取鼠标的位移,然后更新给 useEWResizeCursor 即可. 参考我的实现是, mousedown的时候记录初始位置, 直接展示虚拟光标并更新位置. 。
const { setPosition: setCursorPosition } = useEWResizeCursor();
const [mousePos, setMousePos] = useState<number[] | null>(null);
// Start the drag to change operation when the mouse button is down.
const onDragStart = useCallback(
(position: number[]) => {
document.body.requestPointerLock();
setMousePos(position);
setSnapshot(value);
setCursorPosition(position);
},
[setCursorPosition, value]
);
同时记录 mousePos 即鼠标的实际位置, 在mousemove的时候继续使用. 这么做是因为, 当我们使用 requestPointerLock()之后, 鼠标的位置已经被锁定了, 我们在拖动鼠标时, mouseEvent.clientX等属性不会更新, 但是 mouseEvent.movementX 和 mosueEvent.movementY还是可以正常使用的. 因此我们需要自己记录并计算鼠标拖动的大概位置 。
const onUpdate = (event: MouseEvent) => {
const { movementX, movementY } = event;
if (mousePos) {
const newSnapshot = snapshot + movementX;
const [x, y] = mousePos;
const newMousePos = [x + movementX, y + movementY];
setMousePos(newMousePos);
setCursorPosition(newMousePos);
setSnapshot(newSnapshot);
onChange(newSnapshot);
}
};
这样, 鼠标拖动, 虚拟的鼠标位置会更新, 然后输入框的值也会更新. 但是还不能做到无限拖动, Figma的效果是, 当鼠标水平超出网页边界后, 会从另一端出来, 这样就可以周而复始的拖动. 。
那我们要做的就是限制虚拟的光标位置在一个范围内, 这一步其实也很简单, 我们只需要保证 mousePosX 在 (0, bodyWidth)之间即可. 我们可以封装一个小的方法, 来实现这个限制的功能 。
function calcAbsoluteRemainder(value: number, max: number) {
value = value % max;
return value < 0 ? value + max : value;
}
这样, 当鼠标的x位置超出屏幕右侧, 则会自动跳到左侧, 反之亦然. 那么我们的代码就可以简单的改动一下 。
const onUpdate = (event: MouseEvent) => {
const { movementX, movementY } = event;
if (mousePos) {
const newSnapshot = snapshot + movementX;
const [x, y] = mousePos;
const bodyWidth = document.documentElement.clientWidth;
const bodyHeight = document.documentElement.clientHeight;
const newX = calcAbsoluteRemainder(x + movementX, bodyWidth);
const newY = calcAbsoluteRemainder(y + movementY, bodyHeight);
const newMousePos = [newX, newY];
setMousePos(newMousePos);
setCursorPosition(newMousePos);
setSnapshot(newSnapshot);
onChange(newSnapshot);
}
};
这样就可以做到无限拖拽修改数值了. 现在的逻辑是, 每移动一个像素, 数值就加减1, 如果觉得这个更新的频率太快, 希望做到 每10个像素加减1, 我们可以再 补充一个 scale的参数, 在计算snapshot的时候, 用movementX * scale, 然后onchange的时候取整即可. 。
const newSnapshot = snapshot + movementX * scale;
onChange(Math.round(snapshot));
同样, 我想每次调整的间隔是10而不是1 (Figma安装shift拖动) 那么可以再定义一个step参数, 在onchange的时候, 对其做整数倍计算 。
const newValue = Math.round(newSnapshot);
onChange(step === 1 ? newValue : Math.floor(newValue / step) * step);
这里step为1的时候, 就直接返回取整后的值即可. 。
至此, 我们仿照Figma的拖拽功能已经全部实现, 看下最终效果, 简直一模一样. 。
这里我们注意到一个小细节, 即拖拽松开的时候, 鼠标是会回到起始按下的位置, 这点和figma一样. 而且, 第一次按下的时候, 因为锁定鼠标, 浏览器默认会提示 按住esc显示鼠标. 看了一下Figma的web端, 也是一样的. 。
最后此篇关于Figma数值输入框支持拖拽调整功能实现的文章就讲到这里了,如果你想了解更多关于Figma数值输入框支持拖拽调整功能实现的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在构建一个 RCP 应用程序,其中每个季度都会更新功能/插件。因此,如果用户选择自动更新功能/插件,则会下载更新插件的新 jar,但旧插件仍在使用我不再使用的磁盘空间。 我厌倦了删除包含旧 jar
我如何从外部 Controller 功能中调用 Controller 内部的功能,例如电话间隙回调功能 这是 Controller 外部定义的功能 function onDeviceReady()
如果某个功能(例如 MediaSource)可用,我如何使用 Google Dart 检查。 new MediaSource() 抛出一个错误。如何以编程方式检查此类或功能是否存在?有任何想法吗?是否
我正在尝试运行 Azure Orchestrations,突然我开始从 statusQueryGetUri 收到错误: 协调器函数“UploadDocumentOrchestrator”失败:函数“U
我见过 iPhone 上的应用程序,如果在 3.0 上运行,将使用 3.0 功能/API,例如应用内电子邮件编辑器,如果在 2.x 上运行,则不使用这些功能,并退出应用程序以启动邮件相反。 这是怎么做
这是 DB 规范化理论中的一个概念: Third normal form is violated when a non-key field is a fact about another non-ke
如果我定义 #if SOMETHING #endif 而且我还没有在任何地方定义 SOMETHING。 #if 中的代码会编译吗? 最佳答案 当#if的参数表达式中使用的名称未定义为宏时(在所有其他宏
我刚刚澄清了 A* 路径查找应该如何在两条路径具有相等值的 [情况] 下运行,无论是在计算期间还是在结束时,如果有两条相等的短路径。 例如,我在我的起始节点,我可以扩展到两个可能的节点,但它们都具有相
Java有没有类似下面的东西 宏 一种遍历所有私有(private)字段的方法 类似于 smalltalk symbols 的东西——即用于快速比较静态字符串的东西? 请注意,我正在尝试为 black
这个程序应该将华氏度转换为摄氏度: #include int main() { float fahrenheit, celsius; int max, min, step;
当打开PC缓存功能后, 软件将采用先进先出的原则排队对示波器采集的每一帧数据, 进行帧缓存。 当发现屏幕中有感兴趣的波形掠过时, 鼠标点击软件的(暂停)按钮, 可以选择回看某一帧的波形
我有一个特殊的(虚拟)函数,我想在沙盒环境中使用它: disable.system.call eval(parse(text = 'model.frame("1 ~ 1")'), envir = e
使用新的 Service 实现,我是否必须为我的所有服务提供一个 Options 方法? 使用我的所有服务当前使用的旧 ServiceBase 方法,OPTIONS 返回 OK,但没有 Access-
我正在阅读 Fogus 的关于 Clojure 的喜悦的书,在并行编程章节中,我看到了一个函数定义,它肯定想说明一些重要的事情,但我不知道是什么。此外,我看不到这个函数有什么用 - 当我执行时,它什么
我有大量的 C 代码,大部分代码被注释掉和/或 #if 0。当我使用 % 键匹配 if-else 的左括号和右括号时,它也匹配注释掉的代码。 有没有办法或vim插件在匹配括号时不考虑注释掉或#if 0
我有这个功能: map(map(fn x =>[x])) [[],[1],[2,3,4]]; 产生: val it = [[],[[1]],[[2],[3],[4]]] 我不明白这个功能是如何工作的。
我使用 Visual Studio 代码创建了一个函数应用程序,然后发布了它。功能应用程序运行良好。我现在在功能门户中使用代码部署功能(KUDU)并跳过构建。下面是日志 9:55:46 AM
我有一个数据框df: userID Score Task_Alpha Task_Beta Task_Charlie Task_Delta 3108 -8.00 Easy Easy
我真的无法解决这个问题: 我有一个返回数据框的函数。但是,数据框仅打印在我的控制台中,尽管我希望将其存储在工作空间中。我怎样才能做到这一点? 样本数据: n <- 32640 t <- seq(3*p
有没有办法找出所有可能的激活器命令行选项? activator -help仅提供最低限度的可用选项/功能列表,但所有好的东西都隐藏起来,即使在 typesafe 网站在线文档中也不可用。 到目前为止,
我是一名优秀的程序员,十分优秀!