本文正在參加星光計劃3.0–夏日挑戰賽
作者:巴延興
前言:
ACE全稱是Ability Cross-platform Environment (元能力跨平台執行環境),是應用在OpenHarmony上的UI架構。作為一個UI架構,需要提供視圖布局,UI元件,事件響應機制等的支援,并且目前主流的應用終端都為觸摸屏,UI的操作大都通過手勢完成,我們這裡就對ACE架構的手勢處理做一個簡單的分析。
手勢分類:
從鴻蒙開發者網站提供的API上我們可以看到,在基于TS擴充的開發範式說明裡,單獨對手勢做了一個類别,而在基于JS擴充的開發範式說明裡,則是歸類到通用事件裡,名稱也略有不同:

代碼結構與簡單類圖:
代碼結構:
./ace/ace\_engine/core
|-- gesture #gesture主檔案夾,定義了各種手勢的具體實作類與對應的Recongnizer
| |-- include
| |-- src
|-- components #元件檔案夾
| |-- gesture\_listenser #對觸摸目标對象做一些處理,注冊Recongnizer等。
gesture檔案夾下的類圖:
這裡僅對gesture檔案下的檔案做一個類圖梳理,可以很清晰的看到Gesture類和Recongnizer類的對應關系,其中Gesture類會建立一個對應的Recongnizer,并set對應相關的OnActionID,priority和Mask,而Recongnizer類則是處理手勢相關的事件邏輯等。
手勢簡單代碼流程:
我們以Pinch為例,當應用調用相關接口後,JS進行Binding操作:
JSClass\<JSPinchGesture\>::Declare("PinchGesture");
JSClass\<JSPinchGesture\>::StaticMethod("create", &JSPinchGesture::Create, opt);
JSClass\<JSPinchGesture\>::StaticMethod("pop", &JSGesture::Pop);
JSClass\<JSPinchGesture\>::StaticMethod("onActionStart", &JSGesture::JsHandlerOnActionStart);
JSClass\<JSPinchGesture\>::StaticMethod("onActionUpdate", &JSGesture::JsHandlerOnActionUpdate);
JSClass\<JSPinchGesture\>::StaticMethod("onActionEnd", &JSGesture::JsHandlerOnActionEnd);
JSClass\<JSPinchGesture\>::StaticMethod("onActionCancel", &JSGesture::JsHandlerOnActionCancel);
JSClass\<JSPinchGesture\>::Bind(globalObj);
Create函數裡面會建立一個Gesture執行個體,并将其push到gestureComponent中:
void JSPinchGesture::Create(const JSCallbackInfo& args)
{
int32\_t fingersNum = DEFAULT\_PINCH\_FINGER;
double distanceNum = DEFAULT\_PINCH\_DISTANCE;
if (args.Length() \> 0 && args[0]-\>IsObject()) {
JSRef\<JSObject\> obj = JSRef\<JSObject\>::Cast(args[0]);
JSRef\<JSVal\> fingers = obj-\>GetProperty(GESTURE\_FINGERS);
JSRef\<JSVal\> distance = obj-\>GetProperty(GESTURE\_DISTANCE);
if (fingers-\>IsNumber()) {
int32\_t fingersNumber = fingers-\>ToNumber\<int32\_t\>();
fingersNum = fingersNumber \<= DEFAULT\_PINCH\_FINGER ? DEFAULT\_PINCH\_FINGER : fingersNumber;
}
if (distance-\>IsNumber()) {
double distanceNumber = distance-\>ToNumber\<double\>();
distanceNum = LessNotEqual(distanceNumber, 0.0) ? DEFAULT\_PINCH\_DISTANCE : distanceNumber;
}
}
auto gestureComponent = ViewStackProcessor::GetInstance()-\>GetGestureComponent();
auto gesture = AceType::MakeRefPtr\<OHOS::Ace::PinchGesture\>(fingersNum, distanceNum);
gestureComponent-\>PushGesture(gesture);
}
Gesutre建立對應的Reconizer,來添加對應的事件回調以及設定priority和gestureMask
GestureMask枚舉說明:
| 名稱 | 描述 |
| :------------- | :----------------------------------------------------------- |
| Normal | 不屏蔽子元件的手勢,按照預設手勢識别順序進行識别。 |
| IgnoreInternal | 屏蔽子元件的手勢,僅目前容器的手勢進行識别。子元件上系統内置的手勢不會被屏蔽,如子元件為List元件時,内置的滑動手勢仍然會觸發。 |
RefPtr\<GestureRecognizer\> PinchGesture::CreateRecognizer(WeakPtr\<PipelineContext\> context)
{
auto newContext = context.Upgrade();
if (!newContext) {
LOGE("fail to create pinch recognizer due to context is nullptr");
return nullptr;
}
double distance = newContext-\>NormalizeToPx(Dimension(distance\_, DimensionUnit::VP));
auto pinchRecognizer = AceType::MakeRefPtr\<OHOS::Ace::PinchRecognizer\>(fingers\_, distance);
//JS支援什麼回調事件pinchRecognizer就需要添加對應的事件
if (onActionStartId\_) {
pinchRecognizer-\>SetOnActionStart(\*onActionStartId\_);
}
if (onActionUpdateId\_) {
pinchRecognizer-\>SetOnActionUpdate(\*onActionUpdateId\_);
}
if (onActionEndId\_) {
pinchRecognizer-\>SetOnActionEnd(\*onActionEndId\_);
}
if (onActionCancelId\_) {
pinchRecognizer-\>SetOnActionCancel(\*onActionCancelId\_);
}
pinchRecognizer-\>SetPriority(priority\_);//設定優先級
pinchRecognizer-\>SetPriorityMask(gestureMask\_);//設定GestureMask
return pinchRecognizer;
}
手勢事件的處理流程上,手勢事件均歸類于Touch事件(預設摸到螢幕才能進行操作,隔空手勢暫不做讨論),RenderNode類有個函數TouchTest(EventManager中TouchTest調用RenderNode的TouchTest),TouchTest類似前端的事件冒泡機制,他的作用是觸發Touch事件時收集對應的觸摸事件目标清單,它先從頂部節點開始對所有RenderNode進行深度周遊,然後從最底層的節點開始向上将每個節點收集到一個為TouchTestResult類型的result變量中,最後根據該result進行事件分發。
bool RenderNode::TouchTest(const Point& globalPoint, const Point& parentLocalPoint, const TouchRestrict& touchRestrict,
TouchTestResult& result)
{
if (disableTouchEvent_ || disabled_) {
return false;
}
Point transformPoint = GetTransformPoint(parentLocalPoint);
//判斷觸摸是否在元件區域
if (!InTouchRectList(transformPoint, GetTouchRectList())) {
return false;
}
const auto localPoint = transformPoint - GetPaintRect().GetOffset();
bool dispatchSuccess = false;
const auto& sortedChildren = SortChildrenByZIndex(GetChildren());
//進行深度周遊
if (IsChildrenTouchEnable()) {
for (auto iter = sortedChildren.rbegin(); iter != sortedChildren.rend(); ++iter) {
auto& child = *iter;
if (!child->GetVisible() || child->disabled_ || child->disableTouchEvent_) {
continue;
}
if (child->TouchTest(globalPoint, localPoint, touchRestrict, result)) {
dispatchSuccess = true;
break;
}
if (child->IsTouchable() && (child->InterceptTouchEvent() || IsExclusiveEventForChild())) {
auto localTransformPoint = child->GetTransformPoint(localPoint);
for (auto& rect : child->GetTouchRectList()) {
if (rect.IsInRegion(localTransformPoint)) {
dispatchSuccess = true;
break;
}
}
}
}
}
auto beforeSize = result.size();
for (auto& rect : GetTouchRectList()) {
if (touchable_ && rect.IsInRegion(transformPoint)) {
// Calculates the coordinate offset in this node.
globalPoint_ = globalPoint;
const auto coordinateOffset = globalPoint - localPoint;
coordinatePoint_ = Point(coordinateOffset.GetX(), coordinateOffset.GetY());
OnTouchTestHit(coordinateOffset, touchRestrict, result);
break;
}
}
auto endSize = result.size();
return (dispatchSuccess || beforeSize != endSize) && IsNotSiblingAddRecognizerToResult();
}
事件的分發,touchTestResults_是上面代碼TouchTest裡面擷取:
bool EventManager::DispatchTouchEvent(const TouchEvent& point)
{
ACE\_FUNCTION\_TRACE();
const auto iter = touchTestResults\_.find(point.id);
if (iter != touchTestResults\_.end()) {
bool dispatchSuccess = true;
for (auto entry = iter-\>second.rbegin(); entry != iter-\>second.rend(); ++entry) {
if (!(\*entry)-\>DispatchEvent(point)) {
dispatchSuccess = false;
break;
}
}
//如果有一個手勢識别器已經獲勝,其他手勢識别器仍然會受到事件影響,每個識别器需要自己過濾額外的事件。
if (dispatchSuccess) {
for (const auto& entry : iter-\>second) {
if (!entry-\>HandleEvent(point)) {
break;
}
}
}
//如果事件類型為UP(結束)或者CANCEL(被打斷),則移除該事件
if (point.type == TouchType::UP || point.type == TouchType::CANCEL) {
GestureReferee::GetInstance().CleanGestureScope(point.id);
touchTestResults\_.erase(point.id);
}
return true;
}
LOGI("the %{public}d touch test result does not exist!", point.id);
return false;
}
RenderGestureListener繼承RenderProxy類,RenderProxy繼承自RenderNode類,RenderGestureListener重新實作了OnTouchTestHit函數,以傳回用于接收觸摸事件的觸摸目标對象,coordinateOffset作為recognizer來計算觸摸點的局部位置。這裡面注冊pinchRecognizer,這樣在接收到pinch事件時即可觸發建立pinchRecognizer時添加的事件回調:
void RenderGestureListener::OnTouchTestHit(const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result)
{
/\*\*\*省略一些的Recongnizer注冊代碼\*\*/
if (clickRecognizer\_) {
clickRecognizer\_-\>SetCoordinateOffset(coordinateOffset);
result.emplace\_back(clickRecognizer\_);
}
if (pinchRecognizer\_) {
pinchRecognizer\_-\>SetCoordinateOffset(coordinateOffset);
result.emplace\_back(pinchRecognizer\_);
}
}
此外,每個Recongnizer類都重新實作ReconcileFrom函數将給定recognizer的狀态轉換為this。 實作必須檢查給定的recognizer類型是否與目前的類型比對。 如果比對失敗,傳回值應該為false 如果成功則為true
bool PinchRecognizer::ReconcileFrom(const RefPtr\<GestureRecognizer\>& recognizer)
{
RefPtr\<PinchRecognizer\> curr = AceType::DynamicCast\<PinchRecognizer\>(recognizer);
if (!curr) {
Reset();
return false;
}
if (curr-\>fingers\_ != fingers\_ || curr-\>distance\_ != distance\_ || curr-\>priorityMask\_ != priorityMask\_) {
Reset();
return false;
}
onActionStart\_ = std::move(curr-\>onActionStart\_);
onActionUpdate\_ = std::move(curr-\>onActionUpdate\_);
onActionEnd\_ = std::move(curr-\>onActionEnd\_);
onActionCancel\_ = std::move(curr-\>onActionCancel\_);
return true;
}
最後每個Recongnizer類裡都有相應事件的具體處理邏輯函數HandleXXXEVENT對事件做處理:
void HandleTouchDownEvent(const TouchEvent& event) override;
void HandleTouchUpEvent(const TouchEvent& event) override;
void HandleTouchMoveEvent(const TouchEvent& event) override;
void HandleTouchCancelEvent(const TouchEvent& event) override;
總結:
在ACE架構中,當手指接觸螢幕到對應元件收到事件響應有兩個過程,觸摸測試和事件分發,觸摸測試用于收集那些可以收到事件的元件,事件分發用于執行對應元件的接收事件函數,這樣元件就可以在接收到事件後處理對應的邏輯。此外TS開發範式通用手勢API中,除了Gesture外也定義了priorityGesture(綁定優先識别手勢。)和parallelGesture(綁定可與子元件手勢同時觸發的手勢),手勢事件為非冒泡事件,父元件設定parallelGesture時,父子元件相同的手勢事件都可以觸發,實作類似冒泡效果,這些原理和流程讀者有興趣的可以考慮進一步研究。
引用:
通用事件與通用手勢說明:
https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-components-common-events-0000001051151132
https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-gesture-settings-0000001113113018
更多原創内容請關注:深開鴻技術團隊
入門到精通、技巧到案例,系統化分享HarmonyOS開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。