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

相关推荐
大千UI工场20 分钟前
移动UI: 什么特征会被认为是简洁风格,用案例告诉你
ui
Wincyfu1 小时前
FuTalk设计周刊-Vol.064
人工智能·ui·aigc
西邮彭于晏1 小时前
差分进化算法
windows·python·算法
分享者花花5 小时前
恢复出厂设置后如何从 iPhone 恢复数据
windows·macos·ios·智能手机·excel·cocoa·iphone
web守墓人5 小时前
【前端】解决element-ui两层dialog嵌套,遮罩层消失的问题。
前端·ui
苦藤新鸡6 小时前
用网络编程完成windows和linux跨平台之间的通信(服务器)
linux·网络·windows
雪 狼9 小时前
unity对于文件夹的操作
windows·unity·游戏引擎
爱分享的码瑞哥16 小时前
Rust 进阶教程
开发语言·windows·rust
初学️计算18 小时前
网络协议与标准
运维·服务器·windows
ac-er888819 小时前
win10使用小技巧二
windows·技巧·虚拟键盘·防止复制·安装字体