分组动画改「抽拉卷尺」式刚性平移 + 悬浮组名折起一边滚动一边收起 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正
① 分组折起/展开动画重做为「抽拉卷尺」式刚性平移(推翻上一轮的“按行缩放”):
- 用户要求:折起时所有信号一起向上平移、逐条消失在组名栏里;展开时逐条从组名栏
向下冒出;卷尺上的元素相对位置不变。
- 旧实现按行高缩放 ⇒ 元素被压扁(相对位置改变),且每帧行高都在变、每帧都要重建
行集合。
- 新实现:整组作为一个刚性整体平移,只维护一个逐行平移量 m_rowShift;**行高永不改变**,
平移只作用于“组名行之后”的行(组名是卷尺外壳,始终不动)。
收起 = shift 从 fromShift 推到 blockHeight(全部藏进组名栏);展开 = 推到 0。
ease-out cubic;时长 = clamp(115 + 6×行数, 150, 320) ms。
- 动画期间**不重建行集合**(AdvanceGroupAnimations 不再设 m_rowsDirty)——这是流畅的关键。
- 中途反向点击(折叠没播完就点展开)从**当前位移续接**(GroupAnim::fromShift),不再跳变。
- m_contentHeight = 自然高度 − Σ位移;RowRect() 减去本行位移;
HitRow() 改为逐行比较实际绘制位置(动画中行位置与内容坐标不再一一对应)。
② “藏进组名栏”的裁剪(新增 AnimationHideTopForRow / ClampRowClipTop):
- 信号行越过组名**绘制位置**的下边界即被裁掉;已经完全越过的行整行跳过。
- 除 GDI 裁剪(PaintRowText / FillRowRect 用的 m_rowClipRect)外,还必须用
g.SetClip(clip, CombineModeIntersect) 收紧 GDI+ 裁剪 —— 行内的色块 / 复选框 /
网格徽标走 GDI+、只受 SetClip 约束,不收紧就会画到组名栏上面去。
③ 悬浮组名折起改为“一边向上滚动一边收起”(推翻上一轮的“先滚动到位再折起”):
- 用户要求:悬浮组名既然在最上方,就一边向上滚动一边收起,保证它始终在最上方;
动画完成时滚动偏移恰好落在“组名正好在最上方”且信号已经收起。
- 实现:GroupAnim::scrollSynced —— 滚动偏移与平移量按同一进度推进
(m_scrollOffset = scrollFrom + (scrollTo − scrollFrom) × eased),二者合成让组名的
屏幕位置恒定;目标 scrollTo = min(组名的内容偏移, 折起后可达的最大滚动量)
(后者用于“折起后内容比列表还短”的情形,避免结束时被强行夹取 ⇒ 无跳变)。
- 旧的 m_pendingGroupToggle 两段式机制与 StartScrollAnimationTo 的挂起参数已删除;
用户滚轮 / 拖滚动条 ⇒ CancelScrollAnimation() 只解除同步,折叠/展开继续播放。
④ 筛选状态下折叠三角状态修正:
- 根因:PaintFileRow 直接读 m_tab->IsCollapsed()(普通模式的折叠状态),而筛选模式用的是
独立的 m_filterCollapsed ⇒ 筛选里折叠/展开时三角不变化。
- 修法:改读统一的 IsGroupCollapsed()(按当前模式分派)。
⑤ 样式区「目标」行的绘制与布局修正:
- 绘制:PaintTargetRow 误用列表裁剪版 PaintRowText(文本被裁到 m_listRect),而目标行在
样式区、根本不在列表矩形内 ⇒ 信号名与 GX 标签文本被完全裁掉。改用不裁剪的 UiText,
并在 PaintRowText 注释里写明“样式区不得使用”。
- 布局:名称从内容列左边缘开始、GX 标签**右对齐到内容列右边缘**(与线宽滑轨/数值框同列宽
同两端),两者不重叠;无 GX 标签时名称可用宽度 = 整个内容列(因此更宽,仍左对齐)。
- 双击改名:EditFieldRect() 对目标行只返回 m_targetNameRect,不再返回内容列全宽
(旧实现会把 GX 标签包进输入框)。
验证(全程离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
- .preflight/perf/panelanim_main.cpp 147/147 通过(连跑 3 次稳定):
刚性平移(行高不变 / 间距恒定 / 组名行不动 / 可见行数单调减少 / 逐帧断言组名栏像素零变化 /
中间帧无行集合重建 / 帧耗时)、中途反向切换续接不跳变、筛选态折起与项数、
筛选态与普通态折叠三角逐像素 0 差异、悬浮组名命中(不穿透)、
同步滚动(逐帧钉住 + 恰好落位 + 无跳变)、真实 MF4 单点信号两种自适应逐位一致、
目标行文本像素存在 + 两端对齐 + 输入框不覆盖 GX 标签 + 无标签时名称更宽。
- A/B 五组回退分别 4 / 6 / 6 / 2 / 1 项如实失败。
- 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。
记忆:Documents/agent_mem.md 新增 §9.36、改写 §11.2 第 77–81 条并新增 82–84 条,
标注 §9.35 的“按行缩放动画”与“先滚动再折起”已被推翻。
This commit is contained in:
1 parent
1d314ddc24
commit
5831a0ea0d
3 files changed
+547
-243
No files matched your search
@@ -91,9 +91,9 @@ namespace jdi
|
||||
m_hoveredSignal = SignalRef();
|
||||
m_hoverRow = -1;
|
||||
m_hoverPart = RowPart::NONE;
|
||||
// 分组下标可能整体位移 ⇒ 折起动画与挂起的折起请求也不能继续沿用。
|
||||
// 分组下标可能整体位移 ⇒ 折起动画(含与滚动的同步)也不能继续沿用。
|
||||
m_groupAnims.clear();
|
||||
m_pendingGroupToggle = -1;
|
||||
m_rowShift.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,18 +439,32 @@ namespace jdi
|
||||
/**
|
||||
* @brief 一个分组的折起/展开动画状态。
|
||||
*
|
||||
* 用户 2026-10-16 要求:折起分组时不能下一帧立刻切换布局,而要有一段过渡动画 ——
|
||||
* 被折起的信号向上收起、下方的分组随之上升;**离组名越远的成员收起越快**,
|
||||
* 随后整体减速直到收起完成;总时长控制在一个合理范围(百余毫秒到 300ms)。
|
||||
* 用户 2026-10-16 第二轮要求(**推翻**第一轮的按行缩放):“整个展开和收起的过程类似于
|
||||
* 抽拉卷尺,卷尺上的元素相对位置是不会变的” —— 即**整组信号作为一个刚性整体平移**:
|
||||
* 收起时所有信号一起向上平移、逐条消失在组名栏(越过组名下边界的部分被裁掉),
|
||||
* 展开时逐条从组名栏下方冒出来。因为相对位置不变,所以**行高不发生变化**
|
||||
* (无需每帧重建行集合),仅一个 `m_rowShift` 平移量在变 —— 这是动画流畅的关键。
|
||||
*
|
||||
* 动画期间 `BuildRows` 会按 `GroupRowScale()` 给出每行的收缩比例,
|
||||
* 因此列表内容高度连续变化、下方分组平滑上升。
|
||||
* 另外:**悬浮(sticky)组名折起时不再“先滚动到位再折起”**,而是把滚动偏移与
|
||||
* 平移**同步推进**(`scrollSynced`),保证悬浮组名始终留在列表最上方,
|
||||
* 动画结束时滚动偏移恰好落在“组名正好处于最上方”的位置。
|
||||
*/
|
||||
struct GroupAnim
|
||||
{
|
||||
int groupIndex = -1; /**< 目标分组下标。 */
|
||||
bool collapsing = false; /**< true = 正在折起,false = 正在展开。 */
|
||||
int rowCount = 0; /**< 该分组参与动画的信号行数。 */
|
||||
float blockHeight = 0.0f; /**< 该分组全部信号行的总高(展开态)。 */
|
||||
/**
|
||||
* @brief 动画起始时的平移量(0 = 全部露出,`blockHeight` = 全部藏进组名栏)。
|
||||
*
|
||||
* 一般是“干净的一端”(折起从 0、展开从 `blockHeight`);但若动画中途被反向
|
||||
* 点击(折起没播完就点展开),就从**当前位移**续接,因此不会出现跳变。
|
||||
*/
|
||||
float fromShift = 0.0f;
|
||||
bool scrollSynced = false; /**< 是否让滚动偏移随动画同步推进。 */
|
||||
float scrollFrom = 0.0f; /**< 动画起始滚动偏移。 */
|
||||
float scrollTo = 0.0f; /**< 动画结束滚动偏移(仅 `scrollSynced` 时生效)。 */
|
||||
std::uint64_t startMs = 0; /**< 动画起始时刻(`GetTickCount64()`)。 */
|
||||
std::uint32_t durationMs = 0; /**< 动画总时长(毫秒)。 */
|
||||
};
|
||||
@@ -468,7 +482,10 @@ namespace jdi
|
||||
bool SignalScrollTarget(const SignalRef& ref, float& desired) const;
|
||||
|
||||
/**
|
||||
* @brief 列表内容中某一行顶部相对内容原点的纵向偏移(不包含滚动偏移)。
|
||||
* @brief 列表内容中某一行顶部相对内容原点的**自然**纵向偏移。
|
||||
*
|
||||
* 不含滚动偏移,也不含折起动画的刚性平移(动画只平移“动画分组之后”的行,
|
||||
* 而分组行之前的行不受影响)⇒ 该值就是滚动到“本行正好处于列表最上方”所需的偏移。
|
||||
* @param [IN] row 行下标。
|
||||
* @return 偏移量(像素)。
|
||||
*/
|
||||
@@ -477,9 +494,8 @@ namespace jdi
|
||||
/**
|
||||
* @brief 启动一段滚动动画。
|
||||
* @param [IN] desired 目标滚动偏移(内部会夹取到合法范围)。
|
||||
* @param [IN] pendingGroupToggle 滚动到位后要折起/展开的分组下标(-1 = 无)。
|
||||
*/
|
||||
void StartScrollAnimationTo(float desired, int pendingGroupToggle = -1);
|
||||
void StartScrollAnimationTo(float desired);
|
||||
|
||||
/**
|
||||
* @brief 判断某分组在**当前模式**(筛选 / 普通)下是否处于折起状态。
|
||||
@@ -513,37 +529,58 @@ namespace jdi
|
||||
int FindGroupAnimation(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 启动某分组的折起/展开动画。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] collapsing 是否折起。
|
||||
* @param [IN] rowCount 该分组参与动画的信号行数。
|
||||
* @brief 取动画的**缓动后进度**(0 → 1,ease-out cubic:起步快、末尾减速)。
|
||||
* @param [IN] anim 动画条目。
|
||||
* @return 缓动后的进度。
|
||||
*/
|
||||
void StartGroupAnimation(int groupIndex, bool collapsing, int rowCount);
|
||||
float AnimEasedProgress(const GroupAnim& anim) const;
|
||||
|
||||
/**
|
||||
* @brief 取某分组动画中第 ordinal 行的收缩比例。
|
||||
*
|
||||
* 规则(用户 2026-10-16 要求):**离组名越远的成员收起速度越快**,整体减速(ease-out)
|
||||
* 直到完成;因此越靠下的行越早消失,紧接着下方分组平滑上升。
|
||||
*
|
||||
* @brief 某个分组行在列表中的下标。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] ordinal 该行在分组内「参与列出」的序号(0 = 紧邻组名的第一行)。
|
||||
* @param [IN] rowCount 该分组参与动画的行数。
|
||||
* @return 1 = 完整高度,0 = 完全收起。
|
||||
* @return 行下标;不存在返回 -1。
|
||||
*/
|
||||
float GroupRowScale(int groupIndex, int ordinal, int rowCount) const;
|
||||
int GroupRowIndex(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 切换分组折起状态并播放动画(不处理“悬浮先滚动”的前置动作)。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @brief 全部行的自然总高度(不含折起动画的折减)。
|
||||
* @return 高度(像素)。
|
||||
*/
|
||||
void ToggleGroupCollapsedAnimated(int groupIndex);
|
||||
float NaturalContentHeight() const;
|
||||
|
||||
/**
|
||||
* @brief 点击分组行:若该组名此刻正悬浮在列表上方,先平滑滚动到顶部再播放折起动画。
|
||||
* @brief 某分组当前模式下会列出的信号行总高(展开态)。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @return 高度(像素)。
|
||||
*/
|
||||
float ShownBlockHeight(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 重新计算动画造成的“逐行刚性位移”与内容高度。
|
||||
*
|
||||
* 用户 2026-10-16 要求:悬浮状态下点击组名折起时,滚动条要先向上滚动,
|
||||
* 直到悬浮组名处于**真正完整显示**的位置(即它的自然位置),再执行折起动画。
|
||||
* 每帧(或行集合重建后)调用一次:把每个进行中的动画的位移量累加到它之后的每一行上,
|
||||
* 并据此得出 `m_contentHeight`;与滚动同步的动画同时推进 `m_scrollOffset`。
|
||||
* 行高本身不变,因此**不需要重建行集合**。
|
||||
*/
|
||||
void UpdateAnimGeometry();
|
||||
|
||||
/**
|
||||
* @brief 开始一个分组的折起/展开动画。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] collapse true = 折起,false = 展开。
|
||||
* @param [IN] scrollFrom 动画起始滚动偏移。
|
||||
* @param [IN] scrollTo 动画结束滚动偏移(仅 `syncScroll` 为 true 时生效)。
|
||||
* @param [IN] syncScroll 是否让滚动偏移随动画同步推进(悬浮组名折起时用)。
|
||||
* @return 已开始动画返回 true(行数为 0 时无需动画,返回 false)。
|
||||
*/
|
||||
bool StartGroupToggle(int groupIndex, bool collapse, float scrollFrom, float scrollTo, bool syncScroll);
|
||||
|
||||
/**
|
||||
* @brief 点击分组行:折起/展开该分组(悬浮时与滚动同步推进)。
|
||||
*
|
||||
* 用户 2026-10-16 第二轮要求:悬浮组名折起时**不再**“先滚动到位再折起”,
|
||||
* 而是**一边向上滚动一边收起**,保证悬浮组名始终在最上方;动画完成时滚动偏移
|
||||
* 恰好落在“组名正好在最上方”的位置,且信号已经收起。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] row 分组行下标。
|
||||
*/
|
||||
@@ -558,6 +595,16 @@ namespace jdi
|
||||
*/
|
||||
RowPart RowHitPart(int row, float x, float y) const;
|
||||
|
||||
/**
|
||||
* @brief 折起/展开动画中,某信号行“被藏进组名栏”的上边界(组名的**绘制**下边界)。
|
||||
*
|
||||
* 仅对本行所属分组正在播放动画的信号行有效;其余情况返回一个很小的值(等价于不裁剪)。
|
||||
* @param [IN] row 行下标。
|
||||
* @param [IN] stickyRow 当前悬浮的分组行下标(`StickyGroupRowIndex()` 的结果)。
|
||||
* @return 裁剪上边界(屏幕 Y 坐标)。
|
||||
*/
|
||||
float AnimationHideTopForRow(int row, int stickyRow) const;
|
||||
|
||||
/**
|
||||
* @brief 重新计算「目标」行内信号名与 GX 标签的矩形。
|
||||
*
|
||||
@@ -576,9 +623,12 @@ namespace jdi
|
||||
void CancelScrollAnimation()
|
||||
{
|
||||
m_scrollAnimActive = false;
|
||||
// 挂起的“滚动到位后折起”也必须取消:用户已经自己滚动了,
|
||||
// 再补一次折起会让人莫名其妙。
|
||||
m_pendingGroupToggle = -1;
|
||||
// 与滚动同步的折起动画也不能再接管滚动偏移:用户已经自己滚动了,
|
||||
// 继续同步会“抢”滚动条。折起/展开本身继续播放(只是不驱动滚动)。
|
||||
for (GroupAnim& anim : m_groupAnims)
|
||||
{
|
||||
anim.scrollSynced = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -645,7 +695,8 @@ namespace jdi
|
||||
/**
|
||||
* @brief 把后续行内绘制裁剪到指定行矩形(与列表矩形相交)。
|
||||
*
|
||||
* 折起动画期间行高连续缩小,行内容必须裁到行矩形内,否则垂直居中的文本会溢出到相邻行。
|
||||
* 行内容必须裁到行矩形内:折起动画中信号行会滑到组名栏之下,
|
||||
* 不裁剪的话行内容会溢出到相邻行(或组名栏)上。
|
||||
* @param [IN] rowRect 行矩形。
|
||||
*/
|
||||
void SetRowClip(const gd::RectF& rowRect)
|
||||
@@ -655,6 +706,21 @@ namespace jdi
|
||||
m_rowClipRect.Intersect(m_listRect);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把当前行裁剪矩形的**上边界**抬到给定值(折起动画中信号行“藏进组名栏”用)。
|
||||
* @param [IN] top 新的裁剪上边界(屏幕 Y 坐标)。
|
||||
*/
|
||||
void ClampRowClipTop(float top)
|
||||
{
|
||||
if (!m_rowClipActive || top <= m_rowClipRect.Y)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float bottom = m_rowClipRect.Y + m_rowClipRect.Height;
|
||||
m_rowClipRect.Y = top;
|
||||
m_rowClipRect.Height = std::max(0.0f, bottom - top);
|
||||
}
|
||||
|
||||
/** @brief 结束行内裁剪(恢复为整个列表矩形)。 */
|
||||
void ClearRowClip() { m_rowClipActive = false; }
|
||||
|
||||
@@ -719,11 +785,13 @@ namespace jdi
|
||||
void PaintStickyGroupRow(gd::Graphics& g, int row);
|
||||
|
||||
/**
|
||||
* @brief 在列表区内绘制行内文本(**显式裁剪到列表矩形**)。
|
||||
* @brief 在列表区内绘制行内文本(**显式裁剪到列表矩形 / 当前行矩形**)。
|
||||
*
|
||||
* ⚠️ `UiText` 走 GDI `DrawTextW`,**不遵循 GDI+ 的 `SetClip`**:列表需要滚动时,
|
||||
* 被滚出视口的行(顶部部分可见的两三行)会把组名/信号名画到上方筛选框上。
|
||||
* 因此行内文本一律经本函数绘制(见 `UiTextClipped`)。
|
||||
* 因此**列表行**内文本一律经本函数绘制(见 `UiTextClipped`)。
|
||||
* ⚠️ **样式区(筛选框上方之外的区域,如「目标」行)不得使用本函数** ——
|
||||
* 它会把文本裁到 `m_listRect`(列表矩形之外的内容全都画不出来)。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本内容。
|
||||
@@ -866,8 +934,10 @@ namespace jdi
|
||||
/**
|
||||
* @brief 计算当前输入框的矩形。
|
||||
* @return 输入框矩形,未处于编辑状态时返回空矩形。
|
||||
* @note 「目标」行编辑时需要按当前目标信号的名称 / GX 标签重算几何(名称宽度),
|
||||
* 因此不是 const。
|
||||
*/
|
||||
gd::RectF EditFieldRect() const;
|
||||
gd::RectF EditFieldRect();
|
||||
|
||||
/**
|
||||
* @brief 计算「筛选信号」输入框的内容矩形(编辑与显示共用,保证不跳动)。
|
||||
@@ -1001,17 +1071,19 @@ namespace jdi
|
||||
float m_scrollAnimTo = 0.0f;
|
||||
std::uint64_t m_scrollAnimStartMs = 0;
|
||||
std::uint32_t m_scrollAnimDurationMs = 0;
|
||||
/**
|
||||
* @brief 滚动到位后要折起/展开的分组下标(-1 = 无)。
|
||||
*
|
||||
* 悬浮组名上点击折叠时,必须先滚动到该组名完整可见,再播放折起动画
|
||||
*(用户 2026-10-16 要求)。
|
||||
*/
|
||||
int m_pendingGroupToggle = -1;
|
||||
|
||||
/** @brief 正在进行的分组折起/展开动画(可同时有多个分组)。 */
|
||||
std::vector<GroupAnim> m_groupAnims;
|
||||
|
||||
/**
|
||||
* @brief 动画造成的逐行刚性位移(与 `m_rows` 同长;内容坐标,绘制时从行顶部减去)。
|
||||
*
|
||||
* 只有“动画分组的组名行之后”的行会被平移,因此组名本身始终留在原处,
|
||||
* 信号行从它下面滑过去并被裁掉 —— 这就是“抽拉卷尺”观感的来源。
|
||||
* 行高不变 ⇒ 动画期间无需重建行集合(性能关键)。
|
||||
*/
|
||||
std::vector<float> m_rowShift;
|
||||
|
||||
/**
|
||||
* @brief 筛选模式下的分组折起状态(按分组下标)。
|
||||
*
|
||||
@@ -1022,11 +1094,11 @@ namespace jdi
|
||||
std::vector<bool> m_filterCollapsed;
|
||||
|
||||
/**
|
||||
* @brief 当前正在绘制的行的裁剪矩形(用于折起动画中的部分高度行)。
|
||||
* @brief 当前正在绘制的行的裁剪矩形(用于折起动画中“藏进组名栏”的信号行)。
|
||||
*
|
||||
* 行高在折起动画期间连续缩小,若不把行内容裁剪到行矩形内,
|
||||
* 文本会溢出到相邻行上(`UiText` 是 `DT_VCENTER` 垂直居中,
|
||||
* 在变矮的矩形里会被裁掉上下两半,这正是想要的“挤压”观感)。
|
||||
* `SetRowClip` 先把行内容裁到本行矩形(∩ 列表矩形),折起/展开动画中再用
|
||||
* `ClampRowClipTop` 把上边界抬到组名栏的下边界 ⇒ 越过的部分被裁掉,
|
||||
* 形成“滑进组名栏”的观感。
|
||||
*/
|
||||
bool m_rowClipActive = false;
|
||||
gd::RectF m_rowClipRect;
|
||||
|
||||
Reference in new issue
Block a user