Duilib多标签选项卡拖拽效果:添加动画特效!

动画是小型界面库的"难题"、"通病"

几年前就有人分享了如何用direct UI制作多标签选项卡界面的方法。还有人出了一个简易的浏览器demo。但是他们的标签栏都没有Chrome浏览器那样的动画特效。

如何给界面添加布局是的动画特效呢?

动画使界面看起来高大上,使用起来也更直观。

我调查了一些小型界面库,包括imgui、lcui等,都没有内置这样的组件。

难道仅仅为了这一个小的控件效果,真的要内置一个浏览器?(sortablejs?)


多标签选项卡拖拽效果 【三百行精简版本】

Duilib多标签选项卡拖拽效果 - 知乎

洋洋洒洒八百行 ------ 大多是图标啊,背景啊之类的。然后他还特别设计了。子控件类型和父控件配套使用。太麻烦了。

我简化一番,将原理呈现,只需三百行:

cpp 复制代码
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特效之时,会有一个阻塞之类的,特效组合也不是很自由。

其实很简单,无非是三种方法:

  1. 最简单的timer
  2. 循环Invalidate
  3. 用一个新的线程去控制它刷新。

第三和第二很相似。第二个循环Invalidate是一个折中。

为了入门,简单实现上面动图中的滚动跑马灯特效:

cpp 复制代码
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之后调用。

cpp 复制代码
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 就行。

cpp 复制代码
struct AnimationJob{
	bool active;
	LONG xx;
	LONG yy;
	ULONGLONG start;
	int duration;
};

AnimationJob 结构体记录起始位置,然后根据一个动画时长,一路插值到目标位置即可。

目标位置由父容器布局,由 setPos 决定。

在postSize的循环中,实时修改动画过程中控件的位置,不直接采用setPos 的值,从而实现布局动画,原理十分的简单。

相关推荐
Aevget12 小时前
DevExpress WPF中文教程:Data Grid - 如何使用虚拟源?(四)
ui·.net·wpf·devexpress·wpf控件
元直数字电路验证12 小时前
ASP.NET Core Web APP(MVC)开发中无法全局配置 NuGet 包,该怎么解?
前端·javascript·ui·docker·asp.net·.net
女程序猿!!!13 小时前
视频分辨率
windows
不讲废话的小白14 小时前
文件拖不进企微了怎么办
windows·企微
聪明努力的积极向上14 小时前
【.net framework】WINDOWS服务和控制台程序简单介绍
windows·.net
程序员霸哥哥18 小时前
snipaste免费版下载安装使用教程(附安装包)
windows·microsoft·snipaste
程序员霸哥哥19 小时前
Keil5下载教程及安装教程(附安装包)
windows·keil5·keil5下载教程·keil5安装教程
AI大模型学徒19 小时前
Chatbox 安装 for Windows
windows·语言模型·chatgpt
千里马学框架1 天前
windows系统上aosp15上winscope离线html如何使用?
android·windows·html·framework·安卓窗口系统·winscope
2501_938963961 天前
Flutter 3.19 桌面应用开发:适配 Windows/macOS 端窗口大小与菜单栏自定义
windows·flutter·macos