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 的值,从而实现布局动画,原理十分的简单。

相关推荐
集成显卡13 小时前
windows 下使用 bat 批处理运行 Chrome 无头模式刷一波访问量
windows·程序员
Yasin Chen3 天前
Unity UI坐标说明
ui·unity
路由侠内网穿透3 天前
本地部署 GPS 跟踪系统 Traccar 并实现外部访问
运维·服务器·网络·windows·tcp/ip
研华嵌入式3 天前
如何在高通跃龙QCS6490 Arm架构上使用Windows 11 IoT企业版?
arm开发·windows·嵌入式硬件
带娃的IT创业者4 天前
Windows 平台上基于 MCP 构建“文心一言+彩云天气”服务实战
人工智能·windows·文心一言·mcp
csdn_aspnet4 天前
Windows Node.js 安装及环境配置详细教程
windows·node.js
摇滚侠4 天前
java语言中,list<String>转成字符串,逗号分割;List<Integer>转字符串,逗号分割
java·windows·list
Source.Liu4 天前
【Pywinauto库】12.2 pywinauto.element_info 后端内部实施模块
windows·python·自动化
Source.Liu4 天前
【Pywinauto库】12.1 pywinauto.backend 后端内部实施模块
开发语言·windows·python·自动化
私人珍藏库4 天前
[Windows] FileOptimizer v17.1.0_一款文件批量压缩工具
windows·批量压缩