动画是小型界面库的“难题”、“通病”
几年前就有人分享了如何用direct UI制作多标签选项卡界面的方法。还有人出了一个简易的浏览器demo。但是他们的标签栏都没有Chrome浏览器那样的动画特效。
如何给界面添加布局是的动画特效呢?
动画使界面看起来高大上,使用起来也更直观。
我调查了一些小型界面库,包括imgui、lcui等,都没有内置这样的组件。
难道仅仅为了这一个小的控件效果,真的要内置一个浏览器?(sortablejs?)
多标签选项卡拖拽效果 【三百行精简版本】
Duilib多标签选项卡拖拽效果 - 知乎
洋洋洒洒八百行 —— 大多是图标啊,背景啊之类的。然后他还特别设计了。子控件类型和父控件配套使用。太麻烦了。
我简化一番,将原理呈现,只需三百行:
class CTabBarUI :public CHorizontalLayoutUI
{
public:
CTabBarUI();
~CTabBarUI();
LPCTSTR GetClass() const;
LPVOID GetInterface(LPCTSTR pstrName);
//添加一个
CControlUI* AddItem(LPCTSTR pstrText);
//drag
void DoDragBegin(CControlUI *pTab);
void DoDragMove(CControlUI *pTab, const RECT& rcPaint);
void DoDragEnd(CControlUI *pTab, const POINT& Pt);
private:
CControlUI *m_pZhanWeiOption = NULL;
CControlUI *m_pDragOption = NULL;
};
#define DUI_MSGTYPE_OPTIONTABCLOSE (_T("closeitem_tabbar"))
//
std::function<bool(CControlUI* this_, HDC hDC, const RECT& rcPaint)> postDraw;
std::function<bool(CControlUI* this_, TEventUI& evt)> evtListener;
POINT m_ptLastMouse;
POINT m_ptLButtonDownMouse;
RECT m_rcNewPos;
//判断开始拖拽
bool m_bFirstDrag = true;
//判断是否忽略拖拽,首次需要鼠标按住拖拽一定距离才触发拖拽
bool m_bIgnoreDrag = true;
//
//
CTabBarUI::CTabBarUI()
{
m_pZhanWeiOption = new CControlUI();
m_pZhanWeiOption->SetMaxWidth(0);
m_pZhanWeiOption->SetForeColor(0x000000ff);
m_pZhanWeiOption->SetEnabled(false);
Add(m_pZhanWeiOption);
auto box = this;
postDraw = [box](CControlUI* this_, HDC hDC, const RECT& rcPaint)
{
return true;
};
evtListener = [box](CControlUI* this_, TEventUI& event)
{
//if (!this_->IsMouseEnabled() && event.Type > UIEVENT__MOUSEBEGIN && event.Type < UIEVENT__MOUSEEND) {
// if (box != NULL) box->DoEvent(event);
// else COptionUI::DoEvent(event);
// return true;
//}
auto _manager = box->GetManager();
auto & m_rcItem = this_->GetPos();
if (event.Type == UIEVENT_BUTTONDOWN)
{
if (::PtInRect(&this_->GetPos(), event.ptMouse) && this_->IsEnabled())
{
this_->m_uButtonState |= UISTATE_PUSHED | UISTATE_CAPTURED;
this_->Invalidate();
if (this_->IsRichEvent()) _manager->SendNotify(this_, DUI_MSGTYPE_BUTTONDOWN);
if (::PtInRect(&this_->GetPos(), event.ptMouse)/* && !::PtInRect(&rcClose, event.ptMouse)*/)
{
this_->Activate();
}
m_bIgnoreDrag = true;
m_ptLButtonDownMouse = event.ptMouse;
m_ptLastMouse = event.ptMouse;
m_rcNewPos = m_rcItem;
if (_manager)
{
_manager->RemovePostPaint(this_);
_manager->AddPostPaint(this_);
}
}
}
else if (event.Type == UIEVENT_MOUSEMOVE)
{
if ((this_->m_uButtonState & UISTATE_CAPTURED) != 0)
{
LONG cx = event.ptMouse.x - m_ptLastMouse.x;
LONG cy = event.ptMouse.y - m_ptLastMouse.y;
m_ptLastMouse = event.ptMouse;
RECT rcCurPos = m_rcNewPos;
rcCurPos.left += cx;
rcCurPos.right += cx;
rcCurPos.top += cy;
rcCurPos.bottom += cy;
//将当前拖拽块的位置 和 当前拖拽块的前一时刻的位置,刷新
CDuiRect rcInvalidate = m_rcNewPos;
m_rcNewPos = rcCurPos;
rcInvalidate.Join(m_rcNewPos);
if (_manager) _manager->Invalidate(rcInvalidate);
this_->NeedParentUpdate();
}
}
else if (event.Type == UIEVENT_BUTTONUP)
{
if ((this_->m_uButtonState & UISTATE_CAPTURED) != 0)
{
this_->m_uButtonState &= ~(UISTATE_PUSHED | UISTATE_CAPTURED);
this_->Invalidate();
CTabBarUI* pParent = static_cast<CTabBarUI*>(box);
if (pParent)
{
pParent->DoDragEnd(this_, m_ptLastMouse);
}
if (_manager)
{
_manager->RemovePostPaint(this_);
_manager->Invalidate(m_rcNewPos);
}
this_->NeedParentUpdate();
m_bFirstDrag = true;
}
}
if ((this_->m_uButtonState & UISTATE_CAPTURED) != 0)
{
auto & m_rcItem = this_->GetPos();
lxxx(m_bIgnoreDrag dd, 13)
if (m_bIgnoreDrag && abs(m_ptLastMouse.x - m_ptLButtonDownMouse.x) < 15)
{
return true;
}
m_bIgnoreDrag = false;
lxxx(dd, 13)
CTabBarUI* pParent = static_cast<CTabBarUI*>(box);
//if (!pParent) return true;
if (m_bFirstDrag)
{
pParent->DoDragBegin(this_);
m_bFirstDrag = false;
return true;
}
CDuiRect rcParent = box->GetPos();
RECT rcUpdate = { 0 };
rcUpdate.left = m_rcNewPos.left < rcParent.left ? rcParent.left : m_rcNewPos.left;
rcUpdate.top = m_rcItem.top < rcParent.top ? rcParent.top : m_rcItem.top;
rcUpdate.right = m_rcNewPos.right > rcParent.right ? rcParent.right : m_rcNewPos.right;
rcUpdate.bottom = m_rcItem.bottom > rcParent.bottom ? rcParent.bottom : m_rcItem.bottom;
//CRenderEngine::DrawColor(hDC, rcUpdate, 0xAAFFFFFF);
pParent->DoDragMove(this_, rcUpdate);
}
return true;
};
}
CTabBarUI::~CTabBarUI()
{
}
LPCTSTR CTabBarUI::GetClass() const
{
return _T("TabBarUI");
}
LPVOID CTabBarUI::GetInterface(LPCTSTR pstrName)
{
if (_tcsicmp(pstrName, _T("TabBar")) == 0) return static_cast<CTabBarUI*>(this);
return CHorizontalLayoutUI::GetInterface(pstrName);
}
CControlUI* CTabBarUI::AddItem(LPCTSTR pstrText)
{
if (!pstrText)
{
return NULL;
}
CLabelUI* pTab = new CLabelUI();
pTab->evtListeners.push_back(evtListener);
pTab->postDraws.push_back(postDraw);
pTab->SetRichEvent(true);
//pTab->SetName(_T("tabbaritem"));
//pTab->SetGroup(_T("tabbaritem"));
pTab->SetTextColor(0xff333333);
//pTab->SetNormalImage(_T("file='img/bk_tabbar_item.png' source='0,0,10,8' corner='4,4,4,2'"));
//pTab->SetHotImage(_T("file='img/bk_tabbar_item.png' source='10,0,20,8' corner='4,4,4,2'"));
//pTab->SetSelectedImage(_T("file='img/bk_tabbar_item.png' source='20,0,30,8' corner='4,4,4,2'"));
pTab->SetMaxWidth(226);
//pTab->SetFixedWidth(100);
pTab->SetMinWidth(20);
//pTab->SetBorderRound({ 2, 2 });
pTab->SetText(pstrText);
pTab->SetAttribute(_T("align"), _T("left"));
pTab->SetAttribute(_T("textpadding"), _T("28,0,16,0"));
pTab->SetAttribute(_T("iconsize"), _T("16,16"));
pTab->SetAttribute(_T("iconpadding"), _T("6,0,0,0"));
pTab->SetAttribute(_T("iconimage"), _T("img/icon_360.png"));
pTab->SetAttribute(_T("selectediconimage"), _T("img/icon_baidu.png"));
pTab->SetAttribute(_T("endellipsis"), _T("true"));
pTab->SetAttribute(_T("haveclose"), _T("true"));
pTab->SetAttribute(_T("closepadding"), _T("0,0,6,0"));
pTab->SetAttribute(_T("closesize"), _T("16,16"));
pTab->SetAttribute(_T("closeimage"), _T("file='img/btn_tabbaritem.png' source='0,0,16,16'"));
pTab->SetAttribute(_T("closehotimage"), _T("file='img/btn_tabbaritem.png' source='16,0,32,16'"));
pTab->SetAttribute(_T("closepushimage"), _T("file='img/btn_tabbaritem.png' source='32,0,48,16'"));
//pTab->OnNotify += MakeDelegate(this, &CTabBarUI::OnItemClose);
if (Add(pTab))
{
return pTab;
}
return NULL;
}
void CTabBarUI::DoDragBegin(CControlUI *pTab)
{
if (!pTab)
{
return;
}
int index = GetItemIndex(pTab);
if (index < 0)
{
return;
}
int index_blue = GetItemIndex(m_pZhanWeiOption);
if (index_blue < 0)
{
return;
}
m_pDragOption = pTab;
m_items.SetAt(index, m_pZhanWeiOption);
m_items.SetAt(index_blue, m_pDragOption);
m_pZhanWeiOption->SetMaxWidth(m_pDragOption->GetWidth());
m_pDragOption->SetMaxWidth(0);
}
void CTabBarUI::DoDragMove(CControlUI *pTab, const RECT& rcPaint)
{
if (m_pDragOption != pTab)
{
return;
}
int x = rcPaint.left + (rcPaint.right - rcPaint.left) / 2;
int y = rcPaint.top + (rcPaint.bottom - rcPaint.top) / 2;
if (x < m_rcItem.left || x > m_rcItem.right)
{
return;
}
int index = -1;
for (int it1 = 0; it1 < m_items.GetSize(); it1++)
{
CControlUI* pControl = static_cast<CControlUI*>(m_items[it1]);
if (!pControl) continue;
if(pControl!=m_pZhanWeiOption)
if (/*_tcsicmp(pControl->GetClass(), _T("tabbaritemui")) == 0 && */::PtInRect(&pControl->GetPos(), { x, y }))
{
index = it1;
break;
}
}
if (index == -1)
{
return;
}
CControlUI *pOption = static_cast<CControlUI*>(GetItemAt(index));
int index_blue = GetItemIndex(m_pZhanWeiOption);
m_items.SetAt(index, m_pZhanWeiOption);
m_items.SetAt(index_blue, pOption);
}
void CTabBarUI::DoDragEnd(CControlUI *pTab, const POINT& Pt)
{
if (m_pDragOption != pTab)
{
return;
}
int index = GetItemIndex(m_pDragOption);
if (index < 0)
{
return;
}
int index_blue = GetItemIndex(m_pZhanWeiOption);
if (index_blue < 0)
{
return;
}
m_items.SetAt(index, m_pZhanWeiOption);
m_items.SetAt(index_blue, m_pDragOption);
m_pDragOption->SetMaxWidth(m_pZhanWeiOption->GetWidth());
m_pZhanWeiOption->SetMaxWidth(0);
}
和chrome浏览器不同的是他没有使用标准的拖拽事件,而是分别处理了点击触摸移动事件。
DirectUI 动画方案入门
Direct是比较早的,他的技术比较老。他是直接用那个hdc绘制。和普通的win程序是一样的。区别仅仅是使用自己的布局系统。然后他的控件大多是没有句柄的。所以说比较直接。
最初的DirectUI 公开方案里的动画。那个是dx插特效,是不一样的,在播放dx特效之时,会有一个阻塞之类的,特效组合也不是很自由。
其实很简单,无非是三种方法:
- 最简单的timer
- 循环Invalidate
- 用一个新的线程去控制它刷新。
第三和第二很相似。第二个循环Invalidate是一个折中。
为了入门,简单实现上面动图中的滚动跑马灯特效:
float xx;
int tick;
auto updateFun = [newbar, menu](float spd){
int t = GetTickCount64(), dt = t-tick[i];
xx += dt * spd;
tick = t;
menu->SetFixedXY({(int)round(xx),0});
if (xx>newbar->GetWidth()-menu->GetWidth())
{
xx = 0;
}
return dt;
};
if (开始滚动)
{
newbar->postDraws.push_back([updateFun, newbar](CControlUI* thiz, HDC hDC, const RECT& rcPaint){
int dt = updateFun(.45f);
newbar->NeedUpdate();
Sleep(1);
return true;
});
}
这个需要修改界面库代码在绘制之后调用传进去的函数:
DuiLib\Core\UIControl.cpp
bool CControlUI::DoPaint(HDC hDC, const RECT& rcPaint, CControlUI* pStopControl)
{
...
if (postDraws.size())
{
for (size_t i = 0; i < postDraws.size(); i++)
{
auto ret = postDraws[i](this, hDC, rcPaint);
if (!ret)
{
postDraws.erase(postDraws.begin()+i);
}
}
}
return true;
}
类似于安卓的循环postInvalidate。
注意需要睡眠一秒钟。不然跑的太快,CPU飙升过于明显。当然最大值也不是很大,就是sleep调度一下的话,性能变得很轻盈。
WinQkUI 标签动画
有了这个基础之后,我们就可以实现界面拖拽排序之时的动画效果。
也是需要修改这个源代码库。循环Invalidate还是在dopaint方法内部末尾调用,但是设置位置偏移的话,须在setpos之后调用。
void CTabBarUI::DoDragMove(CControlUI *pTab, const RECT& rcPaint)
{
...
AnimationJob* job = new AnimationJob{true, pItem->GetPos().left, pItem->GetPos().top
, GetTickCount64(), 200};
auto animator = [job](CControlUI* this_, RECT& rcItem)
{
int ww = rcItem.right - rcItem.left;
int hh = rcItem.bottom - rcItem.top;
int time = GetTickCount64() - job->start;
if (time>job->duration)
time = job->duration;
if (time>=job->duration)
job->active = false;
rcItem.left = job->xx + (rcItem.left - job->xx)*1.f/job->duration*time;
rcItem.top = job->yy + (rcItem.top - job->yy)*1.f/job->duration*time;
rcItem.right = rcItem.left + ww;
rcItem.bottom = rcItem.top + hh;
//this_->NeedParentUpdate();
//this_->GetParent()->NeedUpdate();
//Sleep(1);
return job->active;
};
pItem->postSize.resize(0);
pItem->postSize.push_back(animator);
//if (1)
//{
// return;
//}
pItem->_view_states |= VIEWSTATEMASK_IsAnimating;
pItem->postDraws.push_back([job](CControlUI* thiz, HDC hDC, const RECT& rcPaint)
{
if (job->active)
{
//RECT* rcItem = (RECT*)&thiz->GetPos();
int time = GetTickCount64() - job->start;
if (time>job->duration)
time = job->duration;
//if (time>=job->duration)
// job->active = false;
thiz->GetParent()->NeedUpdate();
//Sleep(1);
} else {
thiz->postSize.resize(0);
thiz->_view_states &= ~VIEWSTATEMASK_IsAnimating;
delete job;
}
return job->active;
});
}
后面的代码不是很完整,但原理已经讲得十分清楚了。待我整理一番再上传。
只需在DoDragMove方法。在触发交换元素位置的时候,为每个被移动的元素安排动画 AnimationJob
就行。
struct AnimationJob{
bool active;
LONG xx;
LONG yy;
ULONGLONG start;
int duration;
};
AnimationJob 结构体记录起始位置,然后根据一个动画时长,一路插值到目标位置即可。
目标位置由父容器布局,由 setPos 决定。
在postSize的循环中,实时修改动画过程中控件的位置,不直接采用setPos 的值,从而实现布局动画,原理十分的简单。