欢迎光临
我们一直在努力

01 · AnimationLabel —— 动画文本控件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

01 · AnimationLabel —— 动画文本控件

DLL:DuiLibAnimationLabelPlugin.dll
类:CCAnimationLabel、CCAnimationVerLabel
XML标签:AnimationLabel / AniLabel、AnimationVerLabel / AniVerLabel

一个 DLL 导出两个类:横向动画文本 + 纵向滚动字幕。两者共用 CCControlBase 的
节拍机制,但绘制逻辑完全不同,所以分成两个类。


一、CCAnimationLabel:五种横向动画

1.1 模式枚举

enum TAnimLabelMode
{
ALM_STATIC = 0, // 不动画,普通文本
ALM_TYPEWRITER, // 逐字出现(打字机,带光标闪烁)
ALM_MARQUEE, // 水平/垂直滚动字幕(跑马灯)
ALM_FADE, // 整行淡入淡出
ALM_BLINK, // 闪烁
};

enum TMarqueeDirection
{
MD_LEFT = 0, // 从右往左
MD_RIGHT, // 从左往右
MD_UP, // 从下往上
MD_DOWN, // 从上往下
};

1.2 状态机

控件本身不存"动画进度条",只存三个量:

double m_dProgress; // 0..1 主进度(打字机 / 淡入用)
double m_dMarqueePos; // 滚动偏移(像素,跑马灯用)
int m_nTypeCount; // 已显示的字符数(打字机用)
bool m_bPlaying; // 是否在播

ResetAnim() 一次性归零:

void CCAnimationLabel::ResetAnim()
{
m_dProgress = 0.0;
m_dMarqueePos = 0.0;
m_nTypeCount = 0;
m_bPlaying = (m_eMode != ALM_STATIC) && !m_sText.IsEmpty();
}

m_bPlaying 的判定带上"文本非空"——空串时不启动动画,OnTick 里第一句就
if (!m_bPlaying) return;,空状态零开销。

1.3 打字机:字符数 = 进度 × 长度

case ALM_TYPEWRITER:
{
m_dProgress += dt * (double)m_nSpeed / (double)nLen; // 每秒 m_nSpeed 个字符
int nCount = (int)(m_dProgress * (double)nLen);
if (nCount != m_nTypeCount) { m_nTypeCount = nCount; Invalidate(); }
if (nCount >= nLen) {
if (m_bLoop) { m_dProgress = 0.0; m_nTypeCount = 0; } // 循环重来
else { m_bPlaying = false; }
}
}
break;

m_nSpeed 的语义是"每秒字符数",所以进度增量是 dt * speed / len——
长度 20 字、速度 30 时,0.67 秒打完。改文本长度不用改速度,这是比"每帧加固定量"
更好的设计。

绘制时只取前 N 个字符:

void CCAnimationLabel::PaintTypewriter(HDC hDC, const RECT& rcPaint)
{
RECT rcText = /* 按 textpadding 收缩 */;
CDuiString sPart;
int nLen = m_sText.GetLength();
int nTake = CC::Clamp(m_nTypeCount, 0, nLen);
if (nTake > 0) sPart = m_sText.Left(nTake);
DrawLine(hDC, sPart.GetData(), rcText, 255);

// 光标:动画未结束时闪烁
if (m_bPlaying) {
double w = CC::TriangleWave(m_animClock.Total(), 0.9); // 0.9 秒一个来回
if (w > 0.5) {
HFONT hFont = (m_pManager != NULL && m_nFont >= 0) ? m_pManager->GetFont(m_nFont) : NULL;
SIZE sz = CC::MeasureText(hDC, hFont, sPart.GetData());
int x = rcText.left + sz.cx + 1;
int nH = (sz.cy > 0) ? sz.cy : 14;
int y = rcText.top + (CC::RectHeight(rcText) – nH) / 2;
RECT rcCur = { x, y, x + 2, y + nH };
CC::FillRoundRect(hDC, rcCur, 0, m_dwTextColor); // 2px 宽竖条
}
}
}

光标位置是实测文本宽度决定的(MeasureText),不是估算——所以中文、英文、
混排都跟着走。

1.4 跑马灯:偏移量 + 双重裁剪

case ALM_MARQUEE:
{
RECT rcClip = /* 按 textpadding 收缩 */;
HDC hDC = (m_pManager != NULL) ? m_pManager->GetPaintDC() : NULL;
HFONT hFont = (m_pManager != NULL && m_nFont >= 0) ? m_pManager->GetFont(m_nFont) : NULL;
SIZE szText = CC::MeasureText(hDC, hFont, m_sText.GetData());

int nTextW = szText.cx;
int nBoxW = CC::RectWidth(rcClip);
int nTextH = (szText.cy > 0) ? szText.cy : CC::RectHeight(rcClip);

// 一个完整周期 = 盒子宽 + 文字宽 + 8px 间隙
double dSpan = 0.0;
if (m_eDirection == MD_LEFT || m_eDirection == MD_RIGHT)
dSpan = (double)(nBoxW + nTextW + 8);
else
dSpan = (double)(CC::RectHeight(rcClip) + nTextH + 8);
if (dSpan <= 1.0) dSpan = 1.0;

m_dMarqueePos += dt * (double)CC::MaxOf(m_nSpeed, 1);
if (m_dMarqueePos >= dSpan) {
m_dMarqueePos = m_bLoop ? (m_dMarqueePos – dSpan) : dSpan;
if (!m_bLoop) m_bPlaying = false;
}
Invalidate();
}
break;

周期 dSpan = 盒子宽 + 文字宽 + 8 的物理含义:文字从"完全在右侧外面"滚到
"完全在左侧外面"正好走这么远。滚完一个周期才重置,视觉上无跳变。

绘制时按方向摆位置:

int nSaved = ::SaveDC(hDC);
::IntersectClipRect(hDC, rcClip.left, rcClip.top, rcClip.right, rcClip.bottom);

switch (m_eDirection) {
case MD_RIGHT: rcText.left = rcClip.right – (int)m_dMarqueePos; ... break;
case MD_UP: rcText.top = rcClip.bottom – (int)m_dMarqueePos; ... break;
case MD_DOWN: rcText.top = rcClip.top – nTextH + (int)m_dMarqueePos; ... break;
case MD_LEFT:
default: rcText.left = rcClip.left – (int)m_dMarqueePos; ... break;
}

UINT uStyle = (m_eDirection == MD_UP || m_eDirection == MD_DOWN)
? (DT_LEFT | DT_TOP | DT_SINGLELINE)
: (DT_LEFT | DT_VCENTER | DT_SINGLELINE);
DrawLine(hDC, m_sText.GetData(), rcText, 255);

if (nSaved != 0) ::RestoreDC(hDC, nSaved);

这里是双重裁剪:CCControlBase::DoPaint 已经裁到 rcPaint,PaintMarquee
再裁到 rcClip(考虑 textpadding)。DrawText 的 rcText 可以超出控件边界
(滚动中必然超出),靠 IntersectClipRect 挡住。

1.5 淡入淡出:GDI 无 alpha 文字的绕法

这是全系列最值得说的一个实现取舍。

::DrawText 画的文字没有 alpha——SetTextColor 的 RGB() 只有 24 位,没有 A 通道。
想做出"淡入",只能把文字颜色往背景色插值(这里是往黑底方向压暗):

void CCAnimationLabel::DrawLine(HDC hDC, LPCTSTR pText, RECT rcText, BYTE uAlpha)
{
if (pText == NULL || pText[0] == _T('\\0')) return;

HGDIOBJ hOldFont = NULL;
if (m_pManager != NULL && m_nFont >= 0) {
HFONT hFont = m_pManager->GetFont(m_nFont);
if (hFont != NULL) hOldFont = ::SelectObject(hDC, hFont);
}

::SetBkMode(hDC, TRANSPARENT);

// GDI 文本没有 alpha,淡入淡出时把文字色往黑底方向插值
DWORD dwColor = m_dwTextColor;
if (uAlpha < 255) {
DWORD a = (DWORD)uAlpha;
DWORD r = (((dwColor >> 16) & 0xFF) * a) / 255;
DWORD g = (((dwColor >> 8) & 0xFF) * a) / 255;
DWORD b = (( dwColor & 0xFF) * a) / 255;
dwColor = (dwColor & 0xFF000000) | (r << 16) | (g << 8) | b;
}
::SetTextColor(hDC, RGB((dwColor >> 16) & 0xFF, (dwColor >> 8) & 0xFF, dwColor & 0xFF));

UINT uStyle = m_uAlign;
if (m_bEllipsis) uStyle |= DT_END_ELLIPSIS;
::DrawText(hDC, pText, –1, &rcText, uStyle);

if (hOldFont != NULL) ::SelectObject(hDC, hOldFont);
}

透明度用三角波驱动,得到"渐亮 → 渐暗"的循环:

void CCAnimationLabel::PaintFade(HDC hDC, const RECT& rcPaint)
{
RECT rcText = /* 按 textpadding 收缩 */;
// 进度 0..1 映射成 0..255..0 的透明度
double dAlphaT = CC::TriangleWave(m_dProgress, 1.0);
BYTE uAlpha = (BYTE)(255.0 * dAlphaT);
DrawLine(hDC, m_sText.GetData(), rcText, uAlpha);
}

这个方案的适用边界要说清楚:它假设背景是深色。浅色皮肤下"压暗"会变成"变黑字",
观感不对。要真正透明文字,得走 BeginPath + TextOut + AlphaBlend 的路径,
成本高很多。本系列所有控件都跑在深色工业界面上,所以选了便宜方案。

1.6 闪烁:半周期显示

void CCAnimationLabel::PaintBlink(HDC hDC, const RECT& rcPaint)
{
RECT rcText = /* 按 textpadding 收缩 */;
// 简单闪烁:半个周期显示
if (CC::TriangleWave(m_animClock.Total(), 1.0) >= 0.5) {
DrawLine(hDC, m_sText.GetData(), rcText, 255);
}
}

注意它用 m_animClock.Total()(累计时间)而不是 m_dProgress——闪烁不需要
进度状态,直接用全局时钟,OnTick 里只要 Invalidate() 就行。

1.7 EstimateSize:按实测文本宽度撑开

SIZE CCAnimationLabel::EstimateSize(SIZE szAvailable)
{
(void)szAvailable;
SIZE sz = { 0, 0 };
if (m_sText.IsEmpty()) {
sz.cx = m_cXY.cx;
sz.cy = m_cXY.cy;
if (sz.cy == 0) sz.cy = 20; // 空文本给个最小高度,避免被压成 0
return sz;
}

HDC hDC = (m_pManager != NULL) ? m_pManager->GetPaintDC() : NULL;
HFONT hFont = (m_pManager != NULL && m_nFont >= 0) ? m_pManager->GetFont(m_nFont) : NULL;
SIZE tsz = CC::MeasureText(hDC, hFont, m_sText.GetData());
sz.cx = tsz.cx + m_rcTextPadding.left + m_rcTextPadding.right + 4;
sz.cy = tsz.cy + m_rcTextPadding.top + m_rcTextPadding.bottom + 2;
if (sz.cy <= 0) sz.cy = 20;
return sz;
}

+4 / +2 是防裁边的余量——GetTextExtentPoint32 返回的宽度偶尔比 DrawText
实际占用少 1px(特别是斜体/粗体),不留余量会切掉最后一个字的边缘。

1.8 属性表

属性值说明
text 字符串 DuiLib 原生
mode typewriter / marquee / fade / blink / static 动画模式
speed int 打字机=字符/秒;滚动=像素/秒;夹到 1–2000
direction left / right / up / down 滚动方向
loop bool 是否循环,默认 true
textcolor color 文字色,默认 #FFE0E8F0
font int DuiLib 字体索引,-1 = 默认
align left / center / right 水平对齐
textpadding l,t,r,b 文字内缩
ellipsis bool 超长显示省略号
play bool 属性驱动重播:play="true" 会 Restart()

GetCNAttribute 额外提供两个只读查询:

属性返回
advertising 当前文本(老 XML 的兼容名)
playing "true" / "false"

1.9 play 属性的妙用

else if (_tcsicmp(pstrName, _T("play")) == 0) {
// 允许外部用属性驱动重播
if (_tcsicmp(pstrValue, _T("true")) == 0) Restart();
else m_bPlaying = false;
}

因为 SetAttribute 是公开的,外部脚本(Lua)可以:

ctrl:SetAttribute("play", "true") — 重新播放

不用链接插件接口,也不用加专门的 Restart() 绑定。


二、CCAnimationVerLabel:纵向滚动字幕

2.1 和 AnimationLabel 的分工

AnimationLabelAnimationVerLabel
文本 单行 多行(\\n 分隔)
滚动 横向为主 纵向
模式 5 种 2 种(无缝 / 逐行)

2.2 多行拆分:支持字面量 \\n

这里有一个必须记住的 DuiLib 限制:

DuiLib 的 XML 解析器只认 &amp; &lt; &gt; &quot; &apos; 这五个实体。
&#xD;、&#xA; 这类数字实体会被原样当文本传进来,不会转成回车换行。

所以多行文本在 XML 属性里只能用字面量反斜杠转义:

<AnimationVerLabel text="第一行\\n第二行\\n第三行" speed="60" seamless="true" />

SplitLines() 同时支持真实换行符和字面量转义:

void CCAnimationVerLabel::SplitLines()
{
m_lines.clear();
LPCTSTR p = m_sText.GetData();
if (p == NULL || p[0] == _T('\\0')) return;

CDuiString sCur;
while (*p != _T('\\0')) {
// 1) 真实 CR / LF
if (*p == _T('\\r')) {
m_lines.push_back(sCur); sCur.Empty(); ++p;
if (*p == _T('\\n')) ++p; // 吃掉 CRLF 的 LF
continue;
}
if (*p == _T('\\n')) {
m_lines.push_back(sCur); sCur.Empty(); ++p;
continue;
}

// 2) XML 属性里写的字面量转义 \\n / \\r\\n / \\t
if (*p == _T('\\\\') && p[1] != _T('\\0')) {
if (p[1] == _T('n')) {
m_lines.push_back(sCur); sCur.Empty(); p += 2; continue;
}
if (p[1] == _T('r')) {
m_lines.push_back(sCur); sCur.Empty(); p += 2;
if (*p == _T('\\n')) ++p; continue;
}
if (p[1] == _T('t')) { sCur += _T(' '); p += 2; continue; } // Tab 变空格
// 其它反斜杠原样保留(比如 Windows 路径 C:\\Temp)
sCur += *p; ++p; continue;
}

sCur += *p;
++p;
}
m_lines.push_back(sCur); // ← 最后一行,即使为空也要推(保持行数语义)
}

其它反斜杠原样保留这条很关键——文本可能是路径 D:\\VCPro\\…,
不认识的转义不能吞掉字符。

2.3 无缝滚动:重复绘制形成首尾相接

纵向滚动的核心问题是"滚到底之后上面空了"。解法是把整段文本重复画 N 遍,
形成无限延伸的带子,只裁可视区:

void CCAnimationVerLabel::PaintCNContent(HDC hDC, const RECT& rcPaint)
{
if (m_lines.empty()) return; // ← 空状态直接返回

/* 选字体、SetBkMode、SetTextColor */

int nLineH = ResolveLineHeight(hDC);
int nStep = nLineH + m_nGap; // 每行占高 = 行高 + 间距
int nBoxH = CC::RectHeight(m_rcItem);
if (nBoxH <= 0) return;

int nSaved = ::SaveDC(hDC);
::IntersectClipRect(hDC, m_rcItem.left, m_rcItem.top, m_rcItem.right, m_rcItem.bottom);

int nCount = (int)m_lines.size();
int nTotal = nCount * nStep;
if (nTotal <= 0) nTotal = 1;

// 无缝模式下把整段文本重复绘制,形成首尾相接的滚动
int nRepeats = 2;
if (m_eMode == VSM_SEAMLESS && nTotal < nBoxH * 2 + nStep) {
nRepeats = (nBoxH * 2) / nTotal + 1;
if (nRepeats > 8) nRepeats = 8; // ← 上限 8 遍,防极端参数爆 CPU
}

int nStartY = m_rcItem.top – (int)m_dOffset;
for (int r = 0; r < nRepeats; ++r) {
for (int i = 0; i < nCount; ++i) {
int y = nStartY + r * nTotal + i * nStep;
// 完全在可视区外的行直接跳过
if (y > m_rcItem.bottom || y + nLineH < m_rcItem.top) continue;
RECT rcLine = { m_rcItem.left, y, m_rcItem.right, y + nLineH };
::DrawText(hDC, m_lines[i].GetData(), –1, &rcLine,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
}

if (nSaved != 0) ::RestoreDC(hDC, nSaved);
/* 还原字体 */
}

三个要点:

  • nRepeats 按需计算,且上限 8——文本很短时(比如只有 1 行 20px),
    nBoxH*2/nTotal 可能算出上百遍,必须夹住;
  • 逐行跳过不可见行——重复 8 遍 × 50 行 = 400 次 DrawText 调用,
    不做可见性判断会很慢;
  • m_dOffset 是像素偏移,绘制时整段上移。
  • 2.4 滚动推进:两种模式

    void CCAnimationVerLabel::OnTick(double dt, double dTotal)
    {
    (void)dTotal;
    if (m_lines.empty()) return; // ← 空状态不耗 CPU

    HDC hDC = (m_pManager != NULL) ? m_pManager->GetPaintDC() : NULL;
    int nLineH = ResolveLineHeight(hDC);
    int nStep = nLineH + m_nGap;
    int nTotal = (int)m_lines.size() * nStep;
    int nBoxH = CC::RectHeight(m_rcItem);
    if (nTotal <= 0 || nBoxH <= 0) return;

    // —- 模式 A:无缝连续滚动 —-
    if (m_eMode == VSM_SEAMLESS) {
    m_dOffset += dt * (double)m_nSpeed;
    if (m_dOffset >= (double)nTotal) {
    if (m_bLoop) {
    while (m_dOffset >= (double)nTotal) m_dOffset -= (double)nTotal; // ← while 不是 if
    }
    else {
    m_dOffset = (double)nTotal;
    }
    }
    Invalidate();
    return;
    }

    // —- 模式 B:逐行推入:滚一行 -> 停 1.2 秒 -> 再滚下一行 —-
    if (m_dStepHold > 0.0) { // 停留中
    m_dStepHold -= dt;
    return; // ← 停留期间不 Invalidate,省 CPU
    }
    m_dOffset += dt * (double)m_nSpeed;
    if (m_dOffset >= (double)nStep) {
    m_dOffset = 0.0;
    m_dStepHold = 1.2;
    // 完成一行后把首行挪到末尾
    CDuiString sFirst = m_lines[0];
    m_lines.erase(m_lines.begin());
    if (m_bLoop) m_lines.push_back(sFirst);
    Invalidate();
    }
    else {
    Invalidate();
    }
    }

    模式 B 用 erase(begin()) + push_back() 轮转行序,比维护"当前首行索引"更简单,
    而且 m_lines 只有几十行,O(n) 的代价可以忽略。

    while 而不是 if 的重置很关键:如果 dt 很大(或 speed 极高),
    一次 Tick 可能超过多个周期,用 if 只会减一次,偏移会一直累积变大。

    停留期间直接 return 不 Invalidate()——1.2 秒内画面完全静止,
    没必要重绘。

    2.5 行高解析

    int CCAnimationVerLabel::ResolveLineHeight(HDC hDC) const
    {
    if (m_nLineHeight > 0) return m_nLineHeight; // 属性显式指定
    HFONT hFont = (m_pManager != NULL && m_nFont >= 0) ? m_pManager->GetFont(m_nFont) : NULL;
    SIZE sz = CC::MeasureText(hDC, hFont, _T("\\u56FD")); // 用"国"字量高度
    return (sz.cy > 0) ? sz.cy : 16;
    }

    用汉字"国"而不是 “A” 来量高度——中文字形的实际行高比拉丁字母高,
    用 “A” 量会导致中英文混排时行间距挤在一起。\\u56FD 是转义写法,源码里写汉字也行,
    但转义能避免编码问题。

    2.6 属性表

    属性值说明
    text 多行字符串 用 \\n 分隔
    speed int 像素/秒,夹到 1–2000
    seamless bool true = 无缝连续,false = 逐行推入
    scrollmode seamless / step 同上,语义化写法
    gap int 行间距,夹到 0–200
    loop bool 默认 true
    textcolor color 默认 #FFDCE6F0
    font int 字体索引
    lineheight int 0 = 按字体自动;否则固定像素
    scrolltop 任意 出现该属性就 ScrollToTop()

    只读查询:speed、linecount(行数)、offset(当前偏移)。


    三、踩过的坑

    3.1 XML 里的多行文本只能用字面量 \\n

    原以为可以写:

    <!– ✗ 无效 –>
    <AnimationVerLabel text="第一行&#xD;第二行" />

    实测 DuiLib 的 CDialogBuilder 只处理五个命名实体,数字实体原样保留,
    &#xD; 会变成屏幕上显示的 "
    " 六个字符。

    正确写法:

    <!– ✓ 有效 –>
    <AnimationVerLabel text="第一行\\n第二行" />

    所以 SplitLines() 里必须专门处理 \\\\ + n。

    3.2 edit 工具会剥掉 UTF-8 BOM

    用文本编辑工具改 .cpp 后,BOM 会被剥掉。MSVC 随即按 GBK 读这个文件,
    中文注释变成乱码,并且注释末尾的换行被吞掉——下一行的 inline 被吃进注释里,
    报出 error C2001: 字符串字面量中的换行符 这种完全指向错误位置的错误。

    改完文件必须补 BOM:

    python _tools\\fix_bom.py .

    3.3 淡入淡出假设深色背景

    DrawLine 的 alpha 实现是把颜色往黑色压。如果控件放在浅色背景上,
    “淡出"会变成"变黑字”,不是"变透明"。

    要真正透明,需要:

    // 代价高得多的做法:路径 + 离屏位图 + AlphaBlend
    ::BeginPath(hDC);
    ::TextOut(hDC, x, y, text, len);
    ::EndPath(hDC);
    // 把路径填充到 32bpp DIB,再 AlphaBlend 到目标

    本系列所有控件都跑在深色工业界面,选了便宜方案。换浅色皮肤时需要重做这里。


    四、空状态处理

    控件空状态行为
    AnimationLabel text="" m_bPlaying = false,OnTick 立即返回;PaintStatic 里 DrawLine 遇到空串直接返回
    AnimationVerLabel text="" m_lines 为空,PaintCNContent 与 OnTick 首句返回
    AnimationLabel 未设 mode mode=static 就是普通文本,不启动定时器(m_bPlaying=false,但 StartTick 仍会调——节拍在,只是 OnTick 空转,开销极小)

    五、XML 示例

    <!– 打字机:每秒 30 字,带光标 –>
    <AnimationLabel text="欢迎使用本款软件" mode="typewriter" speed="30"
    font="1" textcolor="#FF20D0FF" align="center" />

    <!– 跑马灯:从右往左,每秒 80 像素 –>
    <AnimationLabel text="请注意,软件为简单Demo" mode="marquee"
    direction="left" speed="80" loop="true" textcolor="#FFE8C33B" />

    <!– 淡入淡出 –>
    <AnimationLabel text="软件系统检测中" mode="fade" speed="40" textcolor="#FF35C0FF" />

    <!– 闪烁 –>
    <AnimationLabel text="检测" mode="blink" textcolor="#FFE04A3B" />

    <!– 纵向无缝滚动字幕(三行) –>
    <AnimationVerLabel text="08:00 软件启动\\n08:02 检测无更新\\n08:05 启动完毕"
    speed="60" seamless="true" gap="4" lineheight="20"
    textcolor="#FFDCE6F0" />

    <!– 纵向逐行推入(每行滚完停 1.2 秒) –>
    <AnimationVerLabel text="第一项:通过\\n第二项:通过\\n第三项:通过"
    scrollmode="step" speed="100" gap="6" />

    Demo示例效果

    赞(0)
    未经允许不得转载:171主机测评 » 01 · AnimationLabel —— 动画文本控件
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址