动画是小型界面库的“难题”、“通病”
几年前就有人分享了如何用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);//dragvoid 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 的值,从而实现布局动画,原理十分的简单。