diff --git a/devtools/devtools-backend/include/api/adapter/data/domain_metas.h b/devtools/devtools-backend/include/api/adapter/data/domain_metas.h index 62cad27176a..9a1e41b54bd 100644 --- a/devtools/devtools-backend/include/api/adapter/data/domain_metas.h +++ b/devtools/devtools-backend/include/api/adapter/data/domain_metas.h @@ -22,11 +22,11 @@ #include #include -#include "api/adapter/data/serializable.h" +#include "nlohmann/json.hpp" namespace hippy::devtools { -class DomainMetas : public Serializable { +class DomainMetas { public: DomainMetas() = default; explicit DomainMetas(uint32_t node_id) : node_id_(node_id) {} @@ -44,7 +44,7 @@ class DomainMetas : public Serializable { inline void SetNodeValue(std::string node_value) { node_value_ = node_value; } inline void SetChildrenCount(uint64_t children_count) { children_count_ = children_count; } inline void AddChild(const DomainMetas& meta) { children_.emplace_back(meta); } - std::string Serialize() const override; + nlohmann::json ToJson() const; private: uint32_t node_id_; diff --git a/devtools/devtools-backend/src/api/adapter/data/domain_metas.cc b/devtools/devtools-backend/src/api/adapter/data/domain_metas.cc index d3a169ab00b..a484614433d 100644 --- a/devtools/devtools-backend/src/api/adapter/data/domain_metas.cc +++ b/devtools/devtools-backend/src/api/adapter/data/domain_metas.cc @@ -36,78 +36,28 @@ constexpr char kLayoutX[] = "x"; constexpr char kLayoutY[] = "y"; constexpr char kStyle[] = "style"; -std::string DomainMetas::Serialize() const { - std::string node_str = "{\""; - node_str += kNodeId; - node_str += "\":"; - node_str += std::to_string(node_id_); - node_str += ",\""; - node_str += kParentId; - node_str += "\":"; - node_str += std::to_string(parent_id_); - node_str += ",\""; - node_str += kRootId; - node_str += "\":"; - node_str += std::to_string(root_id_); - node_str += ",\""; - node_str += kClassName; - node_str += "\":\""; - node_str += class_name_; - node_str += "\",\""; - node_str += kNodeName; - node_str += "\":\""; - node_str += node_name_; - node_str += "\",\""; - node_str += kLocalName; - node_str += "\":\""; - node_str += local_name_; - node_str += "\",\""; - node_str += kNodeValue; - node_str += "\":\""; - node_str += node_value_; - node_str += "\",\""; - node_str += kChildNodeCount; - node_str += "\":"; - node_str += std::to_string(children_.size()); - node_str += ",\""; - node_str += kStyle; - node_str += "\":"; - node_str += style_props_; - node_str += ",\""; - node_str += kAttributes; - node_str += "\":"; - node_str += total_props_; - node_str += ",\""; - node_str += kLayoutX; - node_str += "\":"; - node_str += std::to_string(static_cast(layout_x_)); - node_str += ",\""; - node_str += kLayoutY; - node_str += "\":"; - node_str += std::to_string(static_cast(layout_y_)); - node_str += ",\""; - node_str += kWidth; - node_str += "\":"; - node_str += std::to_string(static_cast(width_)); - node_str += ",\""; - node_str += kHeight; - node_str += "\":"; - node_str += std::to_string(static_cast(height_)); - node_str += ",\""; - node_str += kChildNodeCount; - node_str += "\":"; - node_str += std::to_string(static_cast(children_count_)); +nlohmann::json DomainMetas::ToJson() const { + nlohmann::json jsonObject; + jsonObject[kNodeId] = node_id_; + jsonObject[kParentId] = parent_id_; + jsonObject[kRootId] = root_id_; + jsonObject[kClassName] = class_name_; + jsonObject[kNodeName] = node_name_; + jsonObject[kLocalName] = local_name_; + jsonObject[kNodeValue] = node_value_; + jsonObject[kChildNodeCount] = children_.size(); + jsonObject[kStyle] = nlohmann::json::parse(style_props_, nullptr, false); + jsonObject[kAttributes] = nlohmann::json::parse(total_props_, nullptr, false); + jsonObject[kLayoutX] = static_cast(layout_x_); + jsonObject[kLayoutY] = static_cast(layout_y_); + jsonObject[kWidth] = static_cast(width_); + jsonObject[kHeight] = static_cast(height_); + jsonObject[kChildNodeCount] = static_cast(children_count_); if (!children_.empty()) { - node_str += ",\"children\": ["; for (auto& child : children_) { - node_str += child.Serialize(); - node_str += ","; + jsonObject["children"].push_back(child.ToJson()); } - node_str = node_str.substr(0, node_str.length() - 1); // remove last "," - node_str += "]"; } - node_str += "}"; - return node_str; + return jsonObject; } - } // namespace hippy::devtools diff --git a/devtools/devtools-backend/src/module/domain/css_domain.cc b/devtools/devtools-backend/src/module/domain/css_domain.cc index a1232227067..8f060d6fa00 100644 --- a/devtools/devtools-backend/src/module/domain/css_domain.cc +++ b/devtools/devtools-backend/src/module/domain/css_domain.cc @@ -51,7 +51,7 @@ void CssDomain::RegisterCallback() { return; } auto response_callback = [callback, provider = self->GetDataProvider()](const DomainMetas& data) { - auto model = CssModel::CreateModel(nlohmann::json::parse(data.Serialize(), nullptr, false)); + auto model = CssModel::CreateModel(data.ToJson()); model.SetDataProvider(provider); if (callback) { callback(model); diff --git a/devtools/devtools-backend/src/module/domain/dom_domain.cc b/devtools/devtools-backend/src/module/domain/dom_domain.cc index fd553690f52..abac0f75da8 100644 --- a/devtools/devtools-backend/src/module/domain/dom_domain.cc +++ b/devtools/devtools-backend/src/module/domain/dom_domain.cc @@ -61,7 +61,7 @@ void DomDomain::RegisterCallback() { auto dom_tree_adapter = self->GetDataProvider()->dom_tree_adapter; if (dom_tree_adapter) { auto response_callback = [callback, provider = self->GetDataProvider()](const DomainMetas& data) { - auto model = DomModel::CreateModel(nlohmann::json::parse(data.Serialize(), nullptr, false)); + auto model = DomModel::CreateModel(data.ToJson()); model.SetDataProvider(provider); if (callback) { callback(model); @@ -172,8 +172,8 @@ void DomDomain::GetBoxModel(const DomNodeDataRequest& request) { } void DomDomain::GetNodeForLocation(const DomNodeForLocationRequest& request) { - if (!dom_data_call_back_) { - ResponseErrorToFrontend(request.GetId(), kErrorFailCode, "GetNodeForLocation, dom_data_callback is null"); + if (!location_for_node_call_back_) { + ResponseErrorToFrontend(request.GetId(), kErrorFailCode, "GetNodeForLocation, location_for_node_call_back is null"); return; } if (!request.HasSetXY()) { diff --git a/devtools/devtools-integration/native/include/devtools/devtools_utils.h b/devtools/devtools-integration/native/include/devtools/devtools_utils.h index cce16310167..f6821088c6c 100644 --- a/devtools/devtools-integration/native/include/devtools/devtools_utils.h +++ b/devtools/devtools-integration/native/include/devtools/devtools_utils.h @@ -25,6 +25,7 @@ #include "api/adapter/data/domain_metas.h" #include "dom/dom_manager.h" #include "dom/dom_node.h" +#include "dom/root_node.h" namespace hippy::devtools { /** @@ -45,7 +46,7 @@ class DevToolsUtil { uint32_t depth, const std::shared_ptr& dom_manager); - static DomNodeLocation GetNodeIdByDomLocation(const std::shared_ptr& root_node, double x, double y); + static DomNodeLocation GetNodeIdByDomLocation(const std::shared_ptr& root_node, double x, double y); static DomPushNodePathMetas GetPushNodeByPath(const std::shared_ptr& dom_node, std::vector> path); @@ -55,12 +56,12 @@ class DevToolsUtil { static bool ShouldAvoidPostDomManagerTask(const std::string& event_name); private: - static std::shared_ptr GetHitNode(const std::shared_ptr& root_node, const std::shared_ptr& node, double x, double y); - static bool IsLocationHitNode(const std::shared_ptr& root_node, const std::shared_ptr& dom_node, double x, double y); + static hippy::dom::DomArgument MakeLocationArgument(double x, double y); + static std::shared_ptr GetHitNode(const std::shared_ptr& root_node, double x, double y); static std::string ParseNodeKeyProps(const std::string& node_key, const NodePropsUnorderedMap& node_props); static std::string ParseNodeProps(const NodePropsUnorderedMap& node_props); static std::string ParseNodeProps(const std::unordered_map& node_props); - static std::string ParseDomValue(const HippyValue& value); + static std::string DumpHippyValue(const HippyValue& value); static void AppendDomKeyValue(std::string& node_str, bool& first_object, const std::string& node_key, diff --git a/devtools/devtools-integration/native/src/devtools_utils.cc b/devtools/devtools-integration/native/src/devtools_utils.cc index 8a49d3ff605..00593a77c4c 100644 --- a/devtools/devtools-integration/native/src/devtools_utils.cc +++ b/devtools/devtools-integration/native/src/devtools_utils.cc @@ -26,6 +26,8 @@ constexpr char kDefaultNodeName[] = "DefaultNode"; constexpr char kAttributes[] = "attributes"; constexpr char kText[] = "text"; constexpr char kGetLocationOnScreen[] = "getLocationOnScreen"; +constexpr char kGetViewTagByLocation[] = "getViewTagByLocation"; +constexpr char kHippyTag[] = "hippyTag"; constexpr char kXOnScreen[] = "xOnScreen"; constexpr char kYOnScreen[] = "yOnScreen"; constexpr char kViewWidth[] = "viewWidth"; @@ -91,8 +93,8 @@ DomainMetas DevToolsUtil::GetDomDomainData(const std::shared_ptr& root_ return metas; } -DomNodeLocation DevToolsUtil::GetNodeIdByDomLocation(const std::shared_ptr& root_node, double x, double y) { - auto hit_node = GetHitNode(root_node, root_node, x, y); +DomNodeLocation DevToolsUtil::GetNodeIdByDomLocation(const std::shared_ptr& root_node, double x, double y) { + auto hit_node = GetHitNode(root_node, x, y); FOOTSTONE_LOG(INFO) << "GetNodeIdByDomLocation hit_node:" << hit_node << ", " << x << ",y:" << y; if (hit_node == nullptr) { hit_node = root_node; @@ -127,36 +129,6 @@ DomPushNodePathMetas DevToolsUtil::GetPushNodeByPath(const std::shared_ptr DevToolsUtil::GetHitNode(const std::shared_ptr& root_node, const std::shared_ptr& node, double x, double y) { - if (node == nullptr || !IsLocationHitNode(root_node, node, x, y)) { - return nullptr; - } - std::shared_ptr hit_node = node; - for (auto& child : node->GetChildren()) { - if (!IsLocationHitNode(root_node, child, x, y)) { - continue; - } - auto new_node = GetHitNode(root_node, child, x, y); - if (hit_node == nullptr) { - hit_node = new_node; - } else if (new_node != nullptr) { - auto hit_node_area = hit_node->GetLayoutNode()->GetWidth() * hit_node->GetLayoutNode()->GetHeight(); - auto new_node_area = new_node->GetLayoutNode()->GetWidth() * new_node->GetLayoutNode()->GetHeight(); - hit_node = hit_node_area > new_node_area ? new_node : hit_node; - } - } - return hit_node; -} - -bool DevToolsUtil::IsLocationHitNode(const std::shared_ptr& root_node, const std::shared_ptr& dom_node, double x, double y) { - LayoutResult layout_result = GetLayoutOnScreen(root_node, dom_node); - double self_x = static_cast(layout_result.left); - double self_y = static_cast(layout_result.top); - bool in_top_offset = (x >= self_x) && (y >= self_y); - bool in_bottom_offset = (x <= self_x + layout_result.width) && (y <= self_y + layout_result.height); - return in_top_offset && in_bottom_offset; -} - template auto MakeCopyable(F&& f) { auto s = std::make_shared>(std::forward(f)); @@ -165,6 +137,60 @@ auto MakeCopyable(F&& f) { }; } +hippy::dom::DomArgument DevToolsUtil::MakeLocationArgument(double x, double y) { + footstone::value::HippyValue::HippyValueObjectType hippy_value_object; + hippy_value_object[kXOnScreen] = footstone::value::HippyValue(x); + hippy_value_object[kYOnScreen] = footstone::value::HippyValue(y); + footstone::value::HippyValue::HippyValueArrayType hippy_value_array; + hippy_value_array.push_back(footstone::value::HippyValue(hippy_value_object)); + footstone::value::HippyValue argument_hippy_value(hippy_value_array); + hippy::dom::DomArgument argument(argument_hippy_value); + return argument; +} + +std::shared_ptr DevToolsUtil::GetHitNode(const std::shared_ptr& root_node, double x, double y) { + std::shared_ptr base_node = nullptr; + auto children = root_node->GetChildren(); + if (!children.empty()) { + base_node = children[0]; + } + std::promise layout_promise; + std::future read_file_future = layout_promise.get_future(); + if (!base_node) { + return root_node; + } + + hippy::dom::DomArgument argument = MakeLocationArgument(x, y); + auto get_view_tag_callback = + MakeCopyable([promise = std::move(layout_promise)](std::shared_ptr arg) mutable { + footstone::value::HippyValue result_hippy_value; + arg->ToObject(result_hippy_value); + footstone::value::HippyValue::HippyValueObjectType result_dom_object; + if (result_hippy_value.IsArray() && !result_hippy_value.ToArrayChecked().empty()) { + result_dom_object = result_hippy_value.ToArrayChecked()[0].ToObjectChecked(); + } else if (result_hippy_value.IsObject()) { + result_dom_object = result_hippy_value.ToObjectChecked(); + } else { + promise.set_value(-1); + return; + } + int hippyTag = static_cast(result_dom_object.find(kHippyTag)->second.ToInt32Checked()); + promise.set_value(hippyTag); + }); + base_node->CallFunction(kGetViewTagByLocation, argument, get_view_tag_callback); + std::chrono::milliseconds span(10); + if (read_file_future.wait_for(span) == std::future_status::timeout) { + FOOTSTONE_DLOG(WARNING) << kDevToolsTag << "GetHitNode wait_for timeout"; + return base_node; + } + std::shared_ptr targetNode = base_node; + int hippyTag = read_file_future.get(); + if (hippyTag > 0) { + targetNode = root_node->GetNode(static_cast(hippyTag)); + } + return targetNode; +} + LayoutResult DevToolsUtil::GetLayoutOnScreen(const std::shared_ptr& root_node, const std::shared_ptr& dom_node) { std::shared_ptr find_node = nullptr; if (dom_node == root_node) { @@ -218,79 +244,51 @@ LayoutResult DevToolsUtil::GetLayoutOnScreen(const std::shared_ptr& roo return layout_result; } -std::string DevToolsUtil::ParseDomValue(const HippyValue& hippy_value) { - if (!hippy_value.IsObject()) { - FOOTSTONE_DLOG(INFO) << kDevToolsTag << "ParseTotalProps, node props is not object"; - return "{}"; - } - std::string node_str = "{"; - bool first_object = true; - for (auto iterator : hippy_value.ToObjectChecked()) { - if (iterator.first == "uri" || iterator.first == "src") { - iterator.second = ""; +std::string DevToolsUtil::DumpHippyValue(const HippyValue& hippy_value) { + std::string result_str = ""; + if (hippy_value.IsBoolean()) { + result_str = hippy_value.ToBooleanChecked() ? "true" : "false"; + } else if (hippy_value.IsInt32()) { + result_str = std::to_string(hippy_value.ToInt32Checked()); + } else if (hippy_value.IsUInt32()) { + result_str = std::to_string(hippy_value.ToUint32Checked()); + } else if (hippy_value.IsDouble()) { + result_str = std::to_string(hippy_value.ToDoubleChecked()); + } else if (hippy_value.IsString()) { + nlohmann::json hippy_value_json = hippy_value.ToStringChecked(); + result_str += hippy_value_json.dump(); + } else if (hippy_value.IsArray()) { + auto props_array = hippy_value.ToArrayChecked(); + result_str = "["; + for (auto it = props_array.begin(); it != props_array.end(); ++it) { + if (it->IsNull() || it->IsUndefined()) { + continue; + } + result_str += DumpHippyValue(*it); + if (it != props_array.end() - 1) { + result_str += ","; + } } - std::string key = iterator.first; - if (iterator.second.IsBoolean()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += iterator.second.ToBooleanChecked() ? "true" : "false"; - first_object = false; - } else if (iterator.second.IsInt32()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += std::to_string(iterator.second.ToInt32Checked()); - first_object = false; - } else if (iterator.second.IsUInt32()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += std::to_string(iterator.second.IsUInt32()); - first_object = false; - } else if (iterator.second.IsDouble()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += std::to_string(iterator.second.ToDoubleChecked()); - first_object = false; - } else if (iterator.second.IsString()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":\""; - node_str += iterator.second.ToStringChecked(); - node_str += "\""; - first_object = false; - } else if (iterator.second.IsArray()) { - auto props_array = iterator.second.ToArrayChecked(); - std::string array = "["; - for (auto it = props_array.begin(); it != props_array.end(); ++it) { - if (it->IsNull() || it->IsUndefined()) { - continue; - } - array += ParseDomValue(*it); - if (it != props_array.end() - 1) { - array += ","; - } + result_str += "]"; + } else if (hippy_value.IsObject()) { + result_str = "{"; + for (auto iterator : hippy_value.ToObjectChecked()) { + if (iterator.first == "uri" || iterator.first == "src") { + iterator.second = ""; } - array += "]"; - - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += array; - first_object = false; - - } else if (iterator.second.IsObject()) { - node_str += first_object ? "\"" : ",\""; - node_str += key; - node_str += "\":"; - node_str += ParseDomValue(iterator.second); - first_object = false; + std::string key = iterator.first; + nlohmann::json key_json = key; + result_str += key_json.dump(); + result_str += ":"; + result_str += DumpHippyValue(iterator.second); + result_str += ","; } + result_str.pop_back(); + result_str += "}"; + } else { + result_str = ""; } - node_str += "}"; - return node_str; + return result_str; } std::string DevToolsUtil::ParseNodeKeyProps(const std::string& node_key, const NodePropsUnorderedMap& node_props) { @@ -348,62 +346,14 @@ void DevToolsUtil::AppendDomKeyValue(std::string& node_str, bool& first_object, const std::string& node_key, const HippyValue& hippy_value) { - if (hippy_value.IsBoolean()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += hippy_value.ToBooleanChecked() ? "true" : "false"; - first_object = false; - } else if (hippy_value.IsInt32()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += std::to_string(hippy_value.ToInt32Checked()); - first_object = false; - } else if (hippy_value.IsUInt32()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += std::to_string(hippy_value.IsUInt32()); - first_object = false; - } else if (hippy_value.IsDouble()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += std::to_string(hippy_value.ToDoubleChecked()); - first_object = false; - } else if (hippy_value.IsString()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":\""; - node_str += hippy_value.ToStringChecked(); - node_str += "\""; - first_object = false; - } else if (hippy_value.IsArray()) { - auto props_array = hippy_value.ToArrayChecked(); - std::string array = "["; - for (auto it = props_array.begin(); it != props_array.end(); ++it) { - if (it->IsNull() || it->IsUndefined()) { - continue; - } - array += ParseDomValue(*it); // ParseDomValue(*it); - if (it != props_array.end() - 1) { - array += ","; - } - } - array += "]"; - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += array; - first_object = false; - } else if (hippy_value.IsObject()) { - node_str += first_object ? "\"" : ",\""; - node_str += node_key; - node_str += "\":"; - node_str += ParseDomValue(hippy_value); - first_object = false; + std::string hippy_value_str = DumpHippyValue(hippy_value); + if (hippy_value_str == "") { + return; } + nlohmann::json node_key_json = node_key; + node_str += first_object ? "" : ","; + node_str += node_key_json.dump() + ":" + hippy_value_str; + first_object = false; } void DevToolsUtil::PostDomTask(const std::weak_ptr& weak_dom_manager, std::function func) { @@ -419,7 +369,7 @@ void DevToolsUtil::PostDomTask(const std::weak_ptr& weak_dom_manager * callback must not be posted in the same task runner. */ bool DevToolsUtil::ShouldAvoidPostDomManagerTask(const std::string& event_name) { - return event_name == kGetLocationOnScreen; + return event_name == kGetLocationOnScreen || event_name == kGetViewTagByLocation; } } // namespace hippy::devtools diff --git a/docs/api/hippy-react/components.md b/docs/api/hippy-react/components.md index d625e1e11ed..59e903df54b 100644 --- a/docs/api/hippy-react/components.md +++ b/docs/api/hippy-react/components.md @@ -130,6 +130,24 @@ import icon from './qb_icon_new.png'; | editable | 是否可编辑,开启侧滑删除时需要设置为 `true`。`最低支持版本2.9.0` | `boolean` | `iOS` | | delText | 侧滑删除文本。`最低支持版本2.9.0` | `string` | `iOS` | | onDelete | 在列表项侧滑删除时调起。`最低支持版本2.9.0` | `(nativeEvent: { index: number}) => void` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`:当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 方法 @@ -253,6 +271,24 @@ import icon from './qb_icon_new.png'; | showScrollIndicator | 是否显示滚动条。 `default: false` | `boolean` | `Android、hippy-react-web、Voltron` | | showsHorizontalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏水平的滚动条。`default: true` | `boolean` | `iOS、Voltron` | | showsVerticalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏垂直的滚动条。 `default: true` | `boolean` | `iOS、Voltron` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 方法 diff --git a/docs/api/hippy-vue/components.md b/docs/api/hippy-vue/components.md index f1ed07144cb..c2fa9c550e1 100644 --- a/docs/api/hippy-vue/components.md +++ b/docs/api/hippy-vue/components.md @@ -68,6 +68,24 @@ | showsVerticalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏垂直的滚动条。 `default: true` `(仅在 overflow-y/x: scroll 时适用)`| `boolean` | `iOS、Voltron` | | nativeBackgroundAndroid | 配置水波纹效果,`最低支持版本 2.13.1`;配置项为 `{ borderless: boolean, color: Color, rippleRadius: number }`; `borderless` 表示波纹是否有边界,默认false;`color` 波纹颜色;`rippleRadius` 波纹半径,若不设置,默认容器边框为边界; `注意:设置水波纹后默认不显示,需要在对应触摸事件中调用 setPressed 和 setHotspot 方法进行水波纹展示,详情参考相关`[demo](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-demo/src/components/demos/demo-div.vue) | `Object`| `Android` | | pointerEvents | 用于控制视图是否可以成为触摸事件的目标。 | `enum('box-none', 'none', 'box-only', 'auto')` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 * pointerEvents 的参数含义: * `auto`(默认值) - 视图可以是触摸事件的目标; @@ -379,6 +397,24 @@ Hippy 的重点功能,高性能的可复用列表组件,在终端侧会被 | endReached | 当所有的数据都已经渲染过,并且列表被滚动到最后一条时,将触发 `endReached` 回调。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | editable | 是否可编辑,开启侧滑删除时需要设置为 `true`。`最低支持版本2.9.0` | `boolean` | `iOS` | | delText | 侧滑删除文本。`最低支持版本2.9.0` | `string` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 事件 diff --git a/framework/ios/base/bridge/HippyBridge+BundleLoad.mm b/framework/ios/base/bridge/HippyBridge+BundleLoad.mm index 3d797e6dc18..c320121b7e2 100644 --- a/framework/ios/base/bridge/HippyBridge+BundleLoad.mm +++ b/framework/ios/base/bridge/HippyBridge+BundleLoad.mm @@ -216,16 +216,14 @@ - (void)fetchBundleWithURL:(NSURL *)bundleURL completion:(void (^)(NSData *sourc HippyAssertParam(completion); // Fetch the bundle // Call the completion handler with the fetched data or error - [self loadContentsAsynchronouslyFromUrl:bundleURL.absoluteString - method:@"get" - params:nil - body:nil - queue:nil - progress:nil - completionHandler:^(NSData * _Nullable data, - NSDictionary * _Nullable userInfo, - NSURLResponse * _Nullable response, - NSError * _Nullable error) { + [self loadContentsAsyncFromUrl:bundleURL.absoluteString + params:nil + queue:nil + progress:nil + completionHandler:^(NSData * _Nullable data, + NSDictionary * _Nullable userInfo, + NSURLResponse * _Nullable response, + NSError * _Nullable error) { completion(data, error); }]; } diff --git a/framework/ios/base/bridge/HippyBridge.h b/framework/ios/base/bridge/HippyBridge.h index 07aea37dab7..d114f42a566 100644 --- a/framework/ios/base/bridge/HippyBridge.h +++ b/framework/ios/base/bridge/HippyBridge.h @@ -390,3 +390,8 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass); HIPPY_EXTERN void HippyBridgeFatal(NSError *error, HippyBridge *bridge); NS_ASSUME_NONNULL_END + + +// For compile compatibility +#import "HippyBridge+BundleLoad.h" +#import "HippyBridge+ModuleManage.h" diff --git a/framework/ios/module/imageloader/HippyImageLoaderModule.mm b/framework/ios/module/imageloader/HippyImageLoaderModule.mm index 37926663ad8..4fbee2c178b 100644 --- a/framework/ios/module/imageloader/HippyImageLoaderModule.mm +++ b/framework/ios/module/imageloader/HippyImageLoaderModule.mm @@ -59,13 +59,11 @@ @implementation HippyImageLoaderModule } HIPPY_EXPORT_METHOD(getSize:(NSString *)urlString resolver:(HippyPromiseResolveBlock)resolve rejecter:(HippyPromiseRejectBlock)reject) { - [self.bridge loadContentsAsynchronouslyFromUrl:urlString - method:@"Get" - params:nil - body:nil - queue:nil - progress:nil - completionHandler:^(NSData *data, NSDictionary *userInfo, NSURLResponse *response, NSError *error) { + [self.bridge loadContentsAsyncFromUrl:urlString + params:nil + queue:nil + progress:nil + completionHandler:^(NSData *data, NSDictionary *userInfo, NSURLResponse *response, NSError *error) { if (!error) { id imageProvider = [self imageProviderForData:data]; if (!imageProvider) { diff --git a/framework/ios/module/network/HippyNetWork.mm b/framework/ios/module/network/HippyNetWork.mm index 8c1ddc6430c..5174460302a 100644 --- a/framework/ios/module/network/HippyNetWork.mm +++ b/framework/ios/module/network/HippyNetWork.mm @@ -95,23 +95,29 @@ @implementation HippyNetWork } }]; - NSData *data = nil; - if (body) { - data = [body dataUsingEncoding:NSUTF8StringEncoding]; - } - // Record request start time CFTimeInterval startTime = CACurrentMediaTime(); + + // Construct url request + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:HippyURLWithString(url, nil)]; + if (method) { + [request setHTTPMethod:method]; + } + if (vfsParams) { + for (NSString *key in vfsParams) { + [request setValue:vfsParams[key] forHTTPHeaderField:key]; + } + } + if (body) { + [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]]; + } // Send Request - [self.bridge loadContentsAsynchronouslyFromUrl:url - method:method ?: @"GET" - params:vfsParams - body:data - queue:nil - progress:nil - completionHandler:^(NSData *data, NSDictionary *userInfo, - NSURLResponse *response, NSError *error) { + [self.bridge loadContentsAsyncWithRequest:request + queue:nil + progress:nil + completionHandler:^(NSData *data, NSDictionary *userInfo, + NSURLResponse *response, NSError *error) { NSStringEncoding encoding = GetStringEncodingFromURLResponse(response); NSString *dataStr = [[NSString alloc] initWithData:data encoding:encoding]; NSUInteger statusCode = 0; diff --git a/modules/vfs/ios/HippyBridge+VFSLoader.h b/modules/vfs/ios/HippyBridge+VFSLoader.h index 3fb132e9b5c..508db3ee8b1 100644 --- a/modules/vfs/ios/HippyBridge+VFSLoader.h +++ b/modules/vfs/ios/HippyBridge+VFSLoader.h @@ -25,15 +25,34 @@ NS_ASSUME_NONNULL_BEGIN +/// Category of HippyBridge responsible for loading data @interface HippyBridge (VFSLoader) -- (void)loadContentsAsynchronouslyFromUrl:(NSString *)urlString - method:(NSString *_Nullable)method - params:(NSDictionary *_Nullable)httpHeaders - body:(NSData *_Nullable)body - queue:(NSOperationQueue *_Nullable)queue - progress:(VFSHandlerProgressBlock _Nullable)progress - completionHandler:(VFSHandlerCompletionBlock)completionHandler; +/// Load data from url (GET) +/// - Parameters: +/// - urlString: request url +/// - httpHeaders: http headers, optional +/// - queue: operation queue, optional +/// - progress: progress callback, optional +/// - completionHandler: completion callback +- (void)loadContentsAsyncFromUrl:(NSString *)urlString + params:(nullable NSDictionary *)httpHeaders + queue:(nullable NSOperationQueue *)queue + progress:(nullable VFSHandlerProgressBlock)progress + completionHandler:(VFSHandlerCompletionBlock)completionHandler; + + +/// Load data using given request +/// - Parameters: +/// - request: URLRequest +/// - queue: operation queue, optional +/// - progress: progress callback, optional +/// - completionHandler: completion callback +- (void)loadContentsAsyncWithRequest:(NSURLRequest *)request + queue:(nullable NSOperationQueue *)queue + progress:(nullable VFSHandlerProgressBlock)progress + completionHandler:(VFSHandlerCompletionBlock)completionHandler; + @end NS_ASSUME_NONNULL_END diff --git a/modules/vfs/ios/HippyBridge+VFSLoader.mm b/modules/vfs/ios/HippyBridge+VFSLoader.mm index 4c2de791b7b..a3f13d55d5d 100644 --- a/modules/vfs/ios/HippyBridge+VFSLoader.mm +++ b/modules/vfs/ios/HippyBridge+VFSLoader.mm @@ -29,13 +29,11 @@ @implementation HippyBridge (VFSLoader) -- (void)loadContentsAsynchronouslyFromUrl:(NSString *)urlString - method:(NSString *_Nullable)method - params:(NSDictionary *)httpHeaders - body:(NSData *)body - queue:(NSOperationQueue *_Nullable)queue - progress:(VFSHandlerProgressBlock)progress - completionHandler:(VFSHandlerCompletionBlock)completionHandler { +- (void)loadContentsAsyncFromUrl:(NSString *)urlString + params:(NSDictionary *)httpHeaders + queue:(NSOperationQueue *)queue + progress:(VFSHandlerProgressBlock)progress + completionHandler:(VFSHandlerCompletionBlock)completionHandler { if (!urlString || !completionHandler) { return; } @@ -43,17 +41,24 @@ - (void)loadContentsAsynchronouslyFromUrl:(NSString *)urlString if (loader) { NSURL *url = HippyURLWithString(urlString, nil); NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - if (method) { - [request setHTTPMethod:method]; - } if (httpHeaders) { for (NSString *key in httpHeaders) { [request setValue:httpHeaders[key] forHTTPHeaderField:key]; } } - if (body) { - [request setHTTPBody:body]; - } + loader->RequestUntrustedContent(request, nil, queue, progress, completionHandler); + } +} + +- (void)loadContentsAsyncWithRequest:(NSURLRequest *)request + queue:(NSOperationQueue *_Nullable)queue + progress:(VFSHandlerProgressBlock)progress + completionHandler:(VFSHandlerCompletionBlock)completionHandler { + if (!request || !completionHandler) { + return; + } + std::shared_ptr loader = [self vfsUriLoader].lock(); + if (loader) { loader->RequestUntrustedContent(request, nil, queue, progress, completionHandler); } } diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/utils/DevtoolsUtil.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/utils/DevtoolsUtil.java index 2a32858bfac..0edf61622e6 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/utils/DevtoolsUtil.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/utils/DevtoolsUtil.java @@ -32,6 +32,7 @@ import android.view.PixelCopy; import android.view.PixelCopy.OnPixelCopyFinishedListener; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnDrawListener; import android.view.Window; @@ -59,12 +60,14 @@ public class DevtoolsUtil { public static final String ADD_FRAME_CALLBACK = "addFrameCallback"; public static final String REMOVE_FRAME_CALLBACK = "removeFrameCallback"; public static final String GET_LOCATION_IN_SCREEN = "getLocationOnScreen"; + public static final String GET_VIEW_TAG_BY_LOCATION = "getViewTagByLocation"; private static final String TAG = "tdf_DevtoolsUtil"; private static final String SCREEN_SHOT = "screenShot"; private static final String SCREEN_WIDTH = "width"; private static final String SCREEN_HEIGHT = "height"; private static final String VIEW_WIDTH = "viewWidth"; private static final String VIEW_HEIGHT = "viewHeight"; + private static final String HIPPY_TAG = "hippyTag"; private static final String SCREEN_SCALE = "screenScale"; private static final String FRAME_CALLBACK_ID = "frameCallbackId"; private static final String X_ON_SCREEN = "xOnScreen"; @@ -96,6 +99,9 @@ public static void dispatchDevtoolsFunction(@NonNull View view, @NonNull String case DevtoolsUtil.GET_LOCATION_IN_SCREEN: DevtoolsUtil.getLocationOnScreen(view, promise); break; + case DevtoolsUtil.GET_VIEW_TAG_BY_LOCATION: + DevtoolsUtil.getViewTagByLocation(params, view, promise); + break; default: break; } @@ -120,6 +126,54 @@ private static void getLocationOnScreen(@NonNull View view, @NonNull Promise pro promise.resolve(resultMap); } + private static View findSmallestViewContainingPoint(@NonNull View rootView, int x, int y) { + Rect rect = new Rect(); + rootView.getGlobalVisibleRect(rect); + if (!rect.contains(x, y)) { + return null; + } + View targetView = rootView; + if (rootView instanceof ViewGroup) { + ViewGroup rootViewGroup = (ViewGroup) rootView; + for (int i = 0; i < rootViewGroup.getChildCount(); i++) { + View child = rootViewGroup.getChildAt(i); + View subview = findSmallestViewContainingPoint(child, x, y); + if (subview != null) { + int subArea = subview.getWidth() * subview.getHeight(); + int targetArea = targetView.getWidth() * targetView.getHeight(); + if (subArea < targetArea) { + targetView = subview; + } + } + } + } + return targetView; + } + + private static void getViewTagByLocation(@NonNull List params, @NonNull View view, @NonNull Promise promise) { + HippyMap resultMap = new HippyMap(); + resultMap.pushInt(HIPPY_TAG, -1); + NativeRender renderer = NativeRendererManager.getNativeRenderer(view.getContext()); + View rootView = renderer == null ? null : renderer.getRootView(view); + if (params.isEmpty() || rootView == null) { + promise.resolve(resultMap); + return; + } + Map param = ArrayUtils.getMapValue(params, 0); + if (param != null) { + int xOnScreen = (int) MapUtils.getDoubleValue(param, X_ON_SCREEN, 0); + int yOnScreen = (int) MapUtils.getDoubleValue(param, Y_ON_SCREEN, 0); + int[] rootLocation = new int[2]; + rootView.getLocationOnScreen(rootLocation); + View targetView = findSmallestViewContainingPoint(rootView, xOnScreen + rootLocation[0], + yOnScreen + rootLocation[1]); + if (targetView != null) { + resultMap.pushInt(HIPPY_TAG, targetView.getId()); + } + } + promise.resolve(resultMap); + } + public static void addFrameCallback(@NonNull List params, @NonNull View view, @NonNull final Promise promise) { NativeRender nativeRenderer = NativeRendererManager.getNativeRenderer(view.getContext()); if (nativeRenderer == null) { diff --git a/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m b/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m index 48d57088841..9f7e97aa6a5 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m @@ -46,6 +46,12 @@ @implementation HippyNextBaseListViewManager HIPPY_EXPORT_VIEW_PROPERTY(showScrollIndicator, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(horizontal, BOOL) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) + - (UIView *)view { return [[HippyNextBaseListView alloc] init]; diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h index af83e54a19a..1041e062060 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h @@ -21,6 +21,7 @@ */ #import +#import "HippyNestedScrollProtocol.h" NS_ASSUME_NONNULL_BEGIN @@ -35,8 +36,11 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface HippyNextListTableView : UICollectionView +/// Custom tableView (collectionView) of Hippy +@interface HippyNextListTableView : UICollectionView + +/// Layout delegate @property (nonatomic, weak) id layoutDelegate; @end diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m index f4fd24790e5..3e848e0c6b6 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m @@ -24,6 +24,8 @@ @implementation HippyNextListTableView +HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP + /** * we need scroll indicator to be at top * indicator is UIImageView type at lower ios version @@ -45,4 +47,14 @@ - (void)layoutSubviews { } } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (self.nestedGestureDelegate && + gestureRecognizer == self.panGestureRecognizer && + [self.nestedGestureDelegate respondsToSelector:@selector(shouldRecognizeScrollGestureSimultaneouslyWithView:)]) { + return [self.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:otherGestureRecognizer.view]; + } + return NO; +} + @end diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h new file mode 100644 index 00000000000..d5ea962ff64 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h @@ -0,0 +1,50 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "HippyScrollView.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A coordinator responsible for managing scroll priorities +@interface HippyNestedScrollCoordinator : NSObject + +/// Priority of nestedScroll in all direction. +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollPriority; +/// Priority of nestedScroll in specific direction (finger move from bottom to top). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollTopPriority; +/// Priority of nestedScroll in specific direction (finger move from right to left). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollLeftPriority; +/// Priority of nestedScroll in specific direction (finger move from top to bottom). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollBottomPriority; +/// Priority of nestedScroll in specific direction (finger move from left to right). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollRightPriority; + +/// The inner scrollable view +@property (nonatomic, weak) UIScrollView *innerScrollView; +/// The outer scrollable view +@property (nonatomic, weak) UIScrollView *outerScrollView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m new file mode 100644 index 00000000000..f6ed3c4b8f5 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m @@ -0,0 +1,391 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "HippyNestedScrollCoordinator.h" +#import "HippyAssert.h" +#import "HippyLog.h" + + +static NSString *const kHippyNestedScrollLog= @"NestedScroll"; +#define HippyNSLogTrace(...) HippyLogTrace(@"%@ %p %@", kHippyNestedScrollLog, self, [NSString stringWithFormat:__VA_ARGS__]) +#define HIPPY_NESTED_OPEN_BOUNCES 0 // Turn off the outer bounces feature for now + + +typedef NS_ENUM(char, HippyNestedScrollDirection) { + HippyNestedScrollDirectionNone = 0, + HippyNestedScrollDirectionLeft, + HippyNestedScrollDirectionRight, + HippyNestedScrollDirectionUp, + HippyNestedScrollDirectionDown, +}; + +typedef NS_ENUM(char, HippyNestedScrollDragType) { + HippyNestedScrollDragTypeUndefined = 0, + HippyNestedScrollDragTypeOuterOnly, + HippyNestedScrollDragTypeBoth, +}; + +static CGFloat const kHippyNestedScrollFloatThreshold = 0.1; + +@interface HippyNestedScrollCoordinator () + +/// Current drag type, used to judge the sliding order. +@property (nonatomic, assign) HippyNestedScrollDragType dragType; + +/// Whether should `unlock` the outerScrollView +/// One thing to note is the OuterScrollView may jitter in PrioritySelf mode since lock is a little bit late, +/// we need to make sure the initial state is NO to lock the outerScrollView. +@property (nonatomic, assign) BOOL shouldUnlockOuterScrollView; + +/// Whether should `unlock` the innerScrollView +@property (nonatomic, assign) BOOL shouldUnlockInnerScrollView; + +@end + + +@implementation HippyNestedScrollCoordinator + +- (void)setInnerScrollView:(UIScrollView *)innerScrollView { + HippyAssertParam(innerScrollView); + _innerScrollView = innerScrollView; + // Disable inner's bounces when nested scroll. + _innerScrollView.bounces = NO; +} + +- (void)setOuterScrollView:(UIScrollView *)outerScrollView { + _outerScrollView = outerScrollView; + _outerScrollView.bounces = NO; +} + + +#pragma mark - Private + +- (BOOL)isDirection:(HippyNestedScrollDirection)direction hasPriority:(HippyNestedScrollPriority)priority { + // Note that the top and bottom defined in the nestedScroll attribute refer to the finger orientation, + // as opposed to the page orientation. + HippyNestedScrollPriority presetPriority = HippyNestedScrollPriorityUndefined; + switch (direction) { + case HippyNestedScrollDirectionUp: + presetPriority = self.nestedScrollBottomPriority; + break; + case HippyNestedScrollDirectionDown: + presetPriority = self.nestedScrollTopPriority; + break; + case HippyNestedScrollDirectionLeft: + presetPriority = self.nestedScrollRightPriority; + break; + case HippyNestedScrollDirectionRight: + presetPriority = self.nestedScrollLeftPriority; + break; + default: + break; + } + if ((presetPriority == HippyNestedScrollPriorityUndefined) && + (self.nestedScrollPriority == HippyNestedScrollPriorityUndefined)) { + // Default value is `PrioritySelf`. + return (HippyNestedScrollPrioritySelf == priority); + } + return ((presetPriority == HippyNestedScrollPriorityUndefined) ? + (self.nestedScrollPriority == priority) : + (presetPriority == priority)); +} + +static inline BOOL hasScrollToTheDirectionEdge(const UIScrollView *scrollview, + const HippyNestedScrollDirection direction) { + if (HippyNestedScrollDirectionDown == direction) { + return ((scrollview.contentOffset.y + CGRectGetHeight(scrollview.frame)) + >= scrollview.contentSize.height - kHippyNestedScrollFloatThreshold); + } else if (HippyNestedScrollDirectionUp == direction) { + return scrollview.contentOffset.y <= kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionLeft == direction) { + return scrollview.contentOffset.x <= kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionRight == direction) { + return ((scrollview.contentOffset.x + CGRectGetWidth(scrollview.frame)) + >= scrollview.contentSize.width - kHippyNestedScrollFloatThreshold); + } + return NO; +} + +static inline BOOL isScrollInSpringbackState(const UIScrollView *scrollview, + const HippyNestedScrollDirection direction) { + if (HippyNestedScrollDirectionDown == direction) { + return scrollview.contentOffset.y <= -kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionUp == direction) { + return (scrollview.contentOffset.y + CGRectGetHeight(scrollview.frame) + >= scrollview.contentSize.height + kHippyNestedScrollFloatThreshold); + } if (HippyNestedScrollDirectionLeft == direction) { + return scrollview.contentOffset.x <= -kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionRight == direction) { + return (scrollview.contentOffset.x + CGRectGetWidth(scrollview.frame) + >= scrollview.contentSize.width - kHippyNestedScrollFloatThreshold); + } + return NO; +} + +static inline void lockScrollView(const UIScrollView *scrollView) { + scrollView.contentOffset = scrollView.lastContentOffset; + scrollView.isLockedInNestedScroll = YES; +} + +#pragma mark - ScrollEvents Delegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + const UIScrollView *sv = (UIScrollView *)scrollView; + const UIScrollView *outerScrollView = self.outerScrollView; + const UIScrollView *innerScrollView = self.innerScrollView; + BOOL isOuter = (sv == outerScrollView); + BOOL isInner = (sv == innerScrollView); + + HippyNSLogTrace(@"%@(%p) did scroll: %@", + isOuter ? @"Outer" : @"Inner", sv, + isOuter ? + NSStringFromCGPoint(outerScrollView.contentOffset) : + NSStringFromCGPoint(innerScrollView.contentOffset)); + + // 0. Exclude irrelevant scroll events using `activeInnerScrollView` + if (outerScrollView.activeInnerScrollView && + outerScrollView.activeInnerScrollView != innerScrollView) { + HippyNSLogTrace(@"Not active inner return."); + return; + } + + // 1. Determine direction of scrolling + HippyNestedScrollDirection direction = HippyNestedScrollDirectionNone; + if (sv.lastContentOffset.y > sv.contentOffset.y) { + direction = HippyNestedScrollDirectionUp; + } else if (sv.lastContentOffset.y < sv.contentOffset.y) { + direction = HippyNestedScrollDirectionDown; + } else if (sv.lastContentOffset.x > sv.contentOffset.x) { + direction = HippyNestedScrollDirectionLeft; + } else if (sv.lastContentOffset.x < sv.contentOffset.x) { + direction = HippyNestedScrollDirectionRight; + } + if (direction == HippyNestedScrollDirectionNone) { + HippyNSLogTrace(@"No direction return. %p", sv); + return; + } + + // 2. Lock inner scrollview if necessary + if ([self isDirection:direction hasPriority:HippyNestedScrollPriorityParent]) { + if (isOuter || (isInner && !self.shouldUnlockInnerScrollView)) { + if (hasScrollToTheDirectionEdge(outerScrollView, direction)) { + // Outer has slipped to the edge, + // need to further determine whether the Inner can still slide + if (hasScrollToTheDirectionEdge(innerScrollView, direction)) { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"set lock inner !"); + } else { + self.shouldUnlockInnerScrollView = YES; + HippyNSLogTrace(@"set unlock inner ~"); + } + } else { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"set lock inner !!"); + } + } + + // Do lock inner action! + if (isInner && !self.shouldUnlockInnerScrollView) { + HippyNSLogTrace(@"lock inner (%p) !!!!", sv); + lockScrollView(innerScrollView); + } + + // Handle the scenario where the Inner can slide when the Outer's bounces on. + if (HIPPY_NESTED_OPEN_BOUNCES && + self.shouldUnlockInnerScrollView && + isOuter && sv.bounces == YES && + self.dragType == HippyNestedScrollDragTypeBoth && + hasScrollToTheDirectionEdge(outerScrollView, direction)) { + // When the finger is dragging, the Outer has slipped to the edge and is ready to bounce, + // but the Inner can still slide. + // At this time, the sliding of the Outer needs to be locked. + lockScrollView(outerScrollView); + HippyNSLogTrace(@"lock outer due to inner scroll"); + } + + // Deal with the multi-level nesting (greater than or equal to three layers). + // If inner has an activeInnerScrollView, that means it has a 'scrollable' nested inside it. + // In this case, if the outer-layer locks inner, it should be passed to the outer of the inner-layer. + if (!self.shouldUnlockInnerScrollView && + isOuter && innerScrollView.activeInnerScrollView) { + innerScrollView.cascadeLockForNestedScroll = YES; + innerScrollView.activeInnerScrollView.cascadeLockForNestedScroll = YES; + if (outerScrollView.cascadeLockForNestedScroll) { + outerScrollView.cascadeLockForNestedScroll = NO; + } + HippyNSLogTrace(@"set cascadeLock to %p", innerScrollView); + } + + // Also need to handle unlock conflicts when multiple levels are nested + // (greater than or equal to three levels) and priorities are different. + // When the inner of the inner-layer and the outer of outer-layer are unlocked at the same time, + // if the inner layer has locked the outer, the outer of outer layer should be locked too. + if (self.shouldUnlockInnerScrollView && + isInner && outerScrollView.activeOuterScrollView) { + outerScrollView.activeOuterScrollView.cascadeLockForNestedScroll = YES; + } + + // Do cascade lock action! + if (isOuter && outerScrollView.cascadeLockForNestedScroll) { + lockScrollView(outerScrollView); + HippyNSLogTrace(@"lock outer due to cascadeLock"); + outerScrollView.cascadeLockForNestedScroll = NO; + } else if (isInner && innerScrollView.cascadeLockForNestedScroll) { + lockScrollView(innerScrollView); + HippyNSLogTrace(@"lock outer due to cascadeLock"); + innerScrollView.cascadeLockForNestedScroll = NO; + } + } + + // 3. Lock outer scrollview if necessary + else if ([self isDirection:direction hasPriority:HippyNestedScrollPrioritySelf]) { + if (isInner || (isOuter && !self.shouldUnlockOuterScrollView)) { + if (hasScrollToTheDirectionEdge(innerScrollView, direction)) { + self.shouldUnlockOuterScrollView = YES; + HippyNSLogTrace(@"set unlock outer ~"); + } else { + self.shouldUnlockOuterScrollView = NO; + HippyNSLogTrace(@"set lock outer !"); + } + } + + // Handle the effect of outerScroll auto bouncing back when bounces is on. + if (HIPPY_NESTED_OPEN_BOUNCES && + !self.shouldUnlockOuterScrollView && + isOuter && sv.bounces == YES && + self.dragType == HippyNestedScrollDragTypeUndefined && + isScrollInSpringbackState(outerScrollView, direction)) { + self.shouldUnlockOuterScrollView = YES; + } + + // Do lock outer action! + if (self.dragType != HippyNestedScrollDragTypeOuterOnly && + isOuter && !self.shouldUnlockOuterScrollView) { + HippyNSLogTrace(@"lock outer (%p) !!!!", sv); + lockScrollView(outerScrollView); + } + + // Deal with the multi-level nesting (greater than or equal to three layers). + // If the outer has an activeOuterScrollView, this means it has a scrollable nested around it. + // At this point, if the inner-layer lock `Outer`, it should be passed to the Inner in outer-layer. + if (isInner && !self.shouldUnlockOuterScrollView && + outerScrollView.activeOuterScrollView) { + outerScrollView.cascadeLockForNestedScroll = YES; + outerScrollView.activeOuterScrollView.cascadeLockForNestedScroll = YES; + HippyNSLogTrace(@"set cascadeLock to %p", innerScrollView); + } + + // Do cascade lock action! + if (isInner && innerScrollView.cascadeLockForNestedScroll) { + lockScrollView(innerScrollView); + HippyNSLogTrace(@"lock outer due to cascadeLock"); + innerScrollView.cascadeLockForNestedScroll = NO; + } else if (isOuter && outerScrollView.cascadeLockForNestedScroll) { + lockScrollView(outerScrollView); + HippyNSLogTrace(@"lock outer due to cascadeLock"); + outerScrollView.cascadeLockForNestedScroll = NO; + } + } + + // 4. Update the lastContentOffset record + sv.lastContentOffset = sv.contentOffset; + HippyNSLogTrace(@"end handle %@(%p) scroll -------------", + isOuter ? @"Outer" : @"Inner", sv); +} + + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + if (scrollView == self.outerScrollView) { + self.shouldUnlockOuterScrollView = NO; + HippyNSLogTrace(@"reset outer scroll lock"); + } else if (scrollView == self.innerScrollView) { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"reset inner scroll lock"); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (scrollView == self.innerScrollView) { + // record active scroll for filtering events in scrollViewDidScroll + self.outerScrollView.activeInnerScrollView = self.innerScrollView; + self.innerScrollView.activeOuterScrollView = self.outerScrollView; + + self.dragType = HippyNestedScrollDragTypeBoth; + } else if (self.dragType == HippyNestedScrollDragTypeUndefined) { + self.dragType = HippyNestedScrollDragTypeOuterOnly; + } + }); +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + self.dragType = HippyNestedScrollDragTypeUndefined; +} + + +#pragma mark - HippyNestedScrollGestureDelegate + +- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view { + // Setup outer scrollview if needed + if (!self.outerScrollView) { + id scrollableView = [self.class findNestedOuterScrollView:self.innerScrollView]; + if (scrollableView) { + [scrollableView addScrollListener:self]; + self.outerScrollView = (UIScrollView *)scrollableView.realScrollView; + } + } + + if (view == self.outerScrollView) { + if (self.nestedScrollPriority > HippyNestedScrollPriorityNone || + self.nestedScrollTopPriority > HippyNestedScrollPriorityNone || + self.nestedScrollBottomPriority > HippyNestedScrollPriorityNone || + self.nestedScrollLeftPriority > HippyNestedScrollPriorityNone || + self.nestedScrollRightPriority > HippyNestedScrollPriorityNone) { + return YES; + } + } else if (self.outerScrollView.nestedGestureDelegate) { + return [self.outerScrollView.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:view]; + } + return NO; +} + +#pragma mark - Utils + ++ (id)findNestedOuterScrollView:(UIScrollView *)innerScrollView { + // Use superview.superview since scrollview is a subview of hippy view. + UIView *innerScrollable = (UIView *)innerScrollView.superview; + UIView *outerScrollView = innerScrollable.superview; + while (outerScrollView) { + if ([outerScrollView conformsToProtocol:@protocol(HippyScrollableProtocol)]) { + UIView *outerScrollable = (UIView *)outerScrollView; + // Make sure to find scrollable with same direction. + BOOL isInnerHorizontal = [innerScrollable respondsToSelector:@selector(horizontal)] ? [innerScrollable horizontal] : NO; + BOOL isOuterHorizontal = [outerScrollable respondsToSelector:@selector(horizontal)] ? [outerScrollable horizontal] : NO; + if (isInnerHorizontal == isOuterHorizontal) { + break; + } + } + outerScrollView = outerScrollView.superview; + } + return (id)outerScrollView; +} + +@end + diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h new file mode 100644 index 00000000000..167b4be6357 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h @@ -0,0 +1,75 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HippyNestedScrollProtocol_h +#define HippyNestedScrollProtocol_h + +#import + + +#define HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP \ +@synthesize lastContentOffset; \ +@synthesize activeInnerScrollView; \ +@synthesize activeOuterScrollView; \ +@synthesize nestedGestureDelegate; \ +@synthesize cascadeLockForNestedScroll; \ +@synthesize isLockedInNestedScroll; \ + + +/// Delegate for handling nested scrolls' gesture conflict +@protocol HippyNestedScrollGestureDelegate + +/// Ask the delegate whether gesture should recognize simultaneously +/// For nested scroll +/// @param view the other view +- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view; + +@end + + +/// Protocol for nested scrollview +@protocol HippyNestedScrollProtocol + +/// Record the last content offset for scroll lock. +@property (nonatomic, assign) CGPoint lastContentOffset; + +/// Record the current active inner scrollable view. +/// Used to judge the responder when outer has more than one inner scrollview. +@property (nonatomic, weak) UIScrollView *activeInnerScrollView; + +/// Record the current active outer scrollable view. +/// Used to pass the cascadeLock when more than three scrollable views nested. +@property (nonatomic, weak) UIScrollView *activeOuterScrollView; + +/// Gesture delegate for handling nested scroll. +@property (nonatomic, weak) id nestedGestureDelegate; + +/// Cascade lock for nestedScroll +@property (nonatomic, assign) BOOL cascadeLockForNestedScroll; + +/// Whether is temporarily locked in current DidScroll callback. +/// It is used to determine whether to block the sending of onScroll events. +@property (nonatomic, assign) BOOL isLockedInNestedScroll; + +@end + +#endif /* HippyNestedScrollProtocol_h */ diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h index 1a4976983bd..3d10e5e47b3 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h @@ -20,18 +20,22 @@ * limitations under the License. */ -#import -#import "HippyScrollableProtocol.h" +#import #import "HippyView.h" +#import "HippyScrollableProtocol.h" +#import "HippyNestedScrollProtocol.h" -@protocol UIScrollViewDelegate; -@interface HippyCustomScrollView : UIScrollView +/// The hippy's custom scrollView +@interface HippyCustomScrollView : UIScrollView +/// Whether the content needs to be centered. @property (nonatomic, assign) BOOL centerContent; @end + +/// The HippyScrollView component @interface HippyScrollView : HippyView /** diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm index 26a2284dde4..dd466052bed 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm @@ -26,9 +26,13 @@ #import "UIView+MountEvent.h" #import "UIView+DirectionalLayout.h" #import "HippyRenderUtils.h" +#import "HippyNestedScrollCoordinator.h" + @implementation HippyCustomScrollView +HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP + - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)]; @@ -171,6 +175,16 @@ - (void)setContentOffset:(CGPoint)contentOffset { super.contentOffset = contentOffset; } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (self.nestedGestureDelegate && + gestureRecognizer == self.panGestureRecognizer && + [self.nestedGestureDelegate respondsToSelector:@selector(shouldRecognizeScrollGestureSimultaneouslyWithView:)]) { + return [self.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:otherGestureRecognizer.view]; + } + return NO; +} + @end @interface HippyScrollView () { @@ -191,6 +205,9 @@ @interface HippyScrollView () { int _recordedScrollIndicatorSwitchValue[2]; // default -1 } +/// Nested scroll coordinator +@property (nonatomic, strong) HippyNestedScrollCoordinator *nestedScrollCoordinator; + @end @implementation HippyScrollView @@ -373,6 +390,44 @@ - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { [_scrollView zoomToRect:rect animated:animated]; } + +#pragma mark - Nested Scroll + +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollPriority:nestedScrollPriority]; +} + +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollTopPriority:nestedScrollTopPriority]; +} + +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollLeftPriority:nestedScrollLeftPriority]; +} + +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollBottomPriority:nestedScrollBottomPriority]; +} + +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollRightPriority:nestedScrollRightPriority]; +} + +- (void)setupNestedScrollCoordinatorIfNeeded { + if (!_nestedScrollCoordinator) { + _nestedScrollCoordinator = [HippyNestedScrollCoordinator new]; + _nestedScrollCoordinator.innerScrollView = _scrollView; + _scrollView.nestedGestureDelegate = _nestedScrollCoordinator; + [self addScrollListener:_nestedScrollCoordinator]; + } +} + + #pragma mark - ScrollView delegate - (void)addScrollListener:(NSObject *)scrollListener { @@ -409,6 +464,20 @@ - (NSDictionary *)scrollEventBody { } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + for (NSObject *scrollViewListener in _scrollListeners) { + if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { + [scrollViewListener scrollViewDidScroll:scrollView]; + } + } + + id sv = (id)scrollView; + if (sv.isLockedInNestedScroll) { + // This method is still called when nested scrolling, + // and we should ignore subsequent logic execution when simulating locking. + sv.isLockedInNestedScroll = NO; // reset + return; + } + NSTimeInterval now = CACurrentMediaTime(); NSTimeInterval ti = now - _lastScrollDispatchTime; BOOL flag = (_scrollEventThrottle > 0 && _scrollEventThrottle < ti); @@ -419,11 +488,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; } - for (NSObject *scrollViewListener in _scrollListeners) { - if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { - [scrollViewListener scrollViewDidScroll:scrollView]; - } - } } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm index 7e64562ff82..355ba2a2db0 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm @@ -87,6 +87,13 @@ - (UIView *)view { HIPPY_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, HippyDirectEventBlock) HIPPY_EXPORT_VIEW_PROPERTY(onScrollAnimationEnd, HippyDirectEventBlock) +// Nested scroll props +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) + HIPPY_EXPORT_METHOD(getContentSize:(nonnull NSNumber *)hippyTag callback:(nonnull HippyPromiseResolveBlock)callback) { diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h index d05c1d04602..b0247faa981 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h @@ -21,6 +21,7 @@ */ #import +#import "HippyConvert+NativeRender.h" /** * Protocol for any scrollable components inherit from UIScrollView @@ -60,6 +61,9 @@ @optional +/// Return whether is horizontal, optional, default NO. +- (BOOL)horizontal; + /** * Set components scroll to location offset * @@ -76,4 +80,23 @@ */ - (void)scrollToIndex:(NSInteger)index animated:(BOOL)animated; + +#pragma mark - Nested Scroll Props + +/// Priority of nestedScroll, see `HippyNestedScrollCoordinator` for more +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority; + +/// Priority of nestedScroll in specific direction (finger move from bottom to top) +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority; + +/// Priority of nestedScroll in specific direction (finger move from right to left) +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority; + +/// Priority of nestedScroll in specific direction (finger move from top to bottom) +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority; + +/// Set priority of nestedScroll in specific direction (finger move from left to right) +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority; + + @end diff --git a/renderer/native/ios/renderer/component/view/HippyViewManager.mm b/renderer/native/ios/renderer/component/view/HippyViewManager.mm index 05cbaa8948e..6d0f509fa38 100644 --- a/renderer/native/ios/renderer/component/view/HippyViewManager.mm +++ b/renderer/native/ios/renderer/component/view/HippyViewManager.mm @@ -64,6 +64,13 @@ - (HippyViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDict static NSString * const HippyViewManagerGetBoundingRelToContainerKey = @"relToContainer"; static NSString * const HippyViewManagerGetBoundingErrMsgrKey = @"errMsg"; +static NSString * const HippyXOnScreenKey = @"xOnScreen"; +static NSString * const HippyYOnScreenKey = @"yOnScreen"; +static NSString * const HippyViewWidthKey = @"viewWidth"; +static NSString * const HippyViewHeightKey = @"viewHeight"; +static NSString * const HippyTagKey = @"hippyTag"; +static int const InvalidTag = -1; + HIPPY_EXPORT_METHOD(getBoundingClientRect:(nonnull NSNumber *)hippyTag options:(nullable NSDictionary *)options callback:(HippyPromiseResolveBlock)callback ) { @@ -178,6 +185,34 @@ - (void)measureInAppWindow:(NSNumber *)componentTag }]; } +HIPPY_EXPORT_METHOD(getViewTagByLocation:(nonnull NSNumber *)componentTag + params:(NSDictionary *__nonnull)params + callback:(HippyPromiseResolveBlock)callback) { + [self.bridge.uiManager addUIBlock:^(__unused HippyUIManager *uiManager, NSDictionary *viewRegistry) { + NSMutableDictionary *locationDict = [NSMutableDictionary dictionaryWithDictionary:@{ + HippyTagKey: @(InvalidTag), + }]; + UIView *view = viewRegistry[componentTag]; + if (view == nil) { + callback(locationDict); + return; + } + UIView *rootView = viewRegistry[view.rootTag]; + if (rootView == nil) { + callback(locationDict); + return; + } + double locationX = [params[HippyXOnScreenKey] doubleValue]; + double locationY = [params[HippyYOnScreenKey] doubleValue]; + UIView* hitView = [rootView hitTest:{locationX, locationY} withEvent:nil]; + // TODO: The hitView may not a hippy view (such as a sub native view). Should trace to hippy view. + if (hitView.hippyTag) { + [locationDict setObject:hitView.hippyTag forKey:HippyTagKey]; + } + callback(@[locationDict]); + }]; +} + HIPPY_EXPORT_METHOD(getLocationOnScreen:(nonnull NSNumber *)componentTag params:(NSDictionary *__nonnull)params callback:(HippyPromiseResolveBlock)callback) { @@ -187,12 +222,19 @@ - (void)measureInAppWindow:(NSNumber *)componentTag callback(@[]); return; } + UIView *rootView = viewRegistry[view.rootTag]; + CGFloat rootX = 0, rootY = 0; + if (rootView) { + CGRect windowFrame = [rootView.window convertRect:rootView.frame fromView:rootView.superview]; + rootX = windowFrame.origin.x; + rootY = windowFrame.origin.y; + } CGRect windowFrame = [view.window convertRect:view.frame fromView:view.superview]; NSDictionary *locationDict = @{ - @"xOnScreen": @(static_cast(windowFrame.origin.x)), - @"yOnScreen": @(static_cast(windowFrame.origin.y)), - @"viewWidth": @(static_cast(CGRectGetHeight(windowFrame))), - @"viewHeight": @(static_cast(CGRectGetWidth(windowFrame))) + HippyXOnScreenKey: @(static_cast(windowFrame.origin.x - rootX)), + HippyYOnScreenKey: @(static_cast(windowFrame.origin.y - rootY)), + HippyViewWidthKey: @(static_cast(CGRectGetWidth(windowFrame))), + HippyViewHeightKey: @(static_cast(CGRectGetHeight(windowFrame))) }; callback(@[locationDict]); }]; diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm index e95500f7efc..93330d7d8a0 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm @@ -33,6 +33,7 @@ #import "HippyWaterfallViewCell.h" #import "HippyRootView.h" #import "HippyShadowListView.h" +#import "HippyNestedScrollCoordinator.h" static NSString *kCellIdentifier = @"HippyWaterfallCellIdentifier"; @@ -56,6 +57,9 @@ @interface HippyWaterfallView () { /// Hippy root view @property (nonatomic, weak) HippyRootView *rootView; +/// Nested scroll coordinator +@property (nonatomic, strong) HippyNestedScrollCoordinator *nestedScrollCoordinator; + @end @implementation HippyWaterfallView { @@ -71,15 +75,49 @@ - (instancetype)initWithFrame:(CGRect)frame { _scrollListeners = [NSHashTable weakObjectsHashTable]; _scrollEventThrottle = 100.f; _cachedWeakCellViews = [NSMapTable strongToWeakObjectsMapTable]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; [self initCollectionView]; - if (@available(iOS 11.0, *)) { - self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } } return self; } +- (void)setupNestedScrollCoordinatorIfNeeded { + if (!_nestedScrollCoordinator) { + _nestedScrollCoordinator = [HippyNestedScrollCoordinator new]; + _nestedScrollCoordinator.innerScrollView = self.collectionView; + self.collectionView.nestedGestureDelegate = _nestedScrollCoordinator; + [self addScrollListener:_nestedScrollCoordinator]; + } +} + +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollPriority:nestedScrollPriority]; +} + +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollTopPriority:nestedScrollTopPriority]; +} + +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollLeftPriority:nestedScrollLeftPriority]; +} + +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollBottomPriority:nestedScrollBottomPriority]; +} + +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollRightPriority:nestedScrollRightPriority]; +} + - (void)initCollectionView { _layout = [self collectionViewLayout]; HippyNextListTableView *collectionView = [[HippyNextListTableView alloc] initWithFrame:self.bounds collectionViewLayout:_layout]; @@ -89,6 +127,7 @@ - (void)initCollectionView { collectionView.layoutDelegate = self; collectionView.alwaysBounceVertical = YES; collectionView.backgroundColor = [UIColor clearColor]; + collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; _collectionView = collectionView; [self registerCells]; [self registerSupplementaryViews]; @@ -402,6 +441,19 @@ - (BOOL)manualScroll { #pragma mark - UIScrollView Delegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + for (NSObject *scrollViewListener in [self scrollListeners]) { + if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { + [scrollViewListener scrollViewDidScroll:scrollView]; + } + } + id sv = (id)scrollView; + if (sv.isLockedInNestedScroll) { + // This method is still called when nested scrolling, + // and we should ignore subsequent logic execution when simulating locking. + sv.isLockedInNestedScroll = NO; // reset + return; + } + if (_onScroll) { CFTimeInterval now = CACurrentMediaTime(); CFTimeInterval ti = (now - _lastOnScrollEventTimeInterval) * 1000.0; @@ -412,11 +464,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { _allowNextScrollNoMatterWhat = NO; } } - for (NSObject *scrollViewListener in [self scrollListeners]) { - if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { - [scrollViewListener scrollViewDidScroll:scrollView]; - } - } [_headerRefreshView scrollViewDidScroll:scrollView]; [_footerRefreshView scrollViewDidScroll:scrollView]; } diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m index c1ac185eef0..a24f8f4b660 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m @@ -46,6 +46,11 @@ @implementation HippyWaterfallViewManager HIPPY_EXPORT_VIEW_PROPERTY(containPullFooter, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(scrollEventThrottle, double) HIPPY_EXPORT_VIEW_PROPERTY(onScroll, HippyDirectEventBlock) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) HIPPY_REMAP_VIEW_PROPERTY(showScrollIndicator, collectionView.showsVerticalScrollIndicator, BOOL) - (UIView *)view { diff --git a/renderer/native/ios/utils/HippyConvert+NativeRender.h b/renderer/native/ios/utils/HippyConvert+NativeRender.h index 54b3b9cd084..dc5d2fda840 100644 --- a/renderer/native/ios/utils/HippyConvert+NativeRender.h +++ b/renderer/native/ios/utils/HippyConvert+NativeRender.h @@ -114,4 +114,20 @@ typedef NS_ENUM(NSInteger, HippyPaintType) { @end + +typedef NS_ENUM(char, HippyNestedScrollPriority) { + HippyNestedScrollPriorityUndefined = 0, + HippyNestedScrollPriorityNone, + HippyNestedScrollPrioritySelf, + HippyNestedScrollPriorityParent, +}; + +@interface HippyConvert (NestedScroll) + +/// Convert NestedScroll config to enum +/// - Parameter json: string ++ (HippyNestedScrollPriority)HippyNestedScrollPriority:(id)json; + +@end + NS_ASSUME_NONNULL_END diff --git a/renderer/native/ios/utils/HippyConvert+NativeRender.m b/renderer/native/ios/utils/HippyConvert+NativeRender.m index 80804de7fac..7617f447c70 100644 --- a/renderer/native/ios/utils/HippyConvert+NativeRender.m +++ b/renderer/native/ios/utils/HippyConvert+NativeRender.m @@ -231,3 +231,13 @@ @implementation HippyConvert (HippyPaintType) }), HippyPaintTypeUndefined, integerValue) @end + +@implementation HippyConvert (NestedScroll) + +HIPPY_ENUM_CONVERTER(HippyNestedScrollPriority, (@{ + @"none": @(HippyNestedScrollPriorityNone), + @"self": @(HippyNestedScrollPrioritySelf), + @"parent": @(HippyNestedScrollPriorityParent), +}), HippyNestedScrollPriorityUndefined, charValue) + +@end diff --git a/tests/ios/HippyNestedScrollTest.m b/tests/ios/HippyNestedScrollTest.m new file mode 100644 index 00000000000..1f51bad2268 --- /dev/null +++ b/tests/ios/HippyNestedScrollTest.m @@ -0,0 +1,98 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + + +@interface HippyNestedScrollCoordinator (UnitTest) + +/// Whether is the given direction has specified priority +/// direction param see `HippyNestedScrollDirection` +- (BOOL)isDirection:(char)direction hasPriority:(HippyNestedScrollPriority)priority; + +@end + +@interface HippyNestedScrollTest : XCTestCase + +@end + +@implementation HippyNestedScrollTest + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testNestedScrollCoordinatorSetPriority { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + XCTAssertTrue([coordinator isDirection:0 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPrioritySelf]); + + coordinator.nestedScrollPriority = HippyNestedScrollPrioritySelf; + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertFalse([coordinator isDirection:1 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:2 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:3 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:4 hasPriority:HippyNestedScrollPriorityParent]); + + coordinator.nestedScrollRightPriority = HippyNestedScrollPriorityParent; + coordinator.nestedScrollLeftPriority = HippyNestedScrollPrioritySelf; + coordinator.nestedScrollBottomPriority = HippyNestedScrollPriorityNone; + coordinator.nestedScrollTopPriority = HippyNestedScrollPriorityParent; + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPriorityNone]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPriorityParent]); +} + +- (void)testShouldRecognizeScrollGestureSimultaneously { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + HippyScrollView *scrollView = [HippyScrollView new]; + coordinator.outerScrollView = (UIScrollView *)scrollView.realScrollView; + XCTAssertFalse([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); + coordinator.nestedScrollPriority = HippyNestedScrollPriorityNone; + XCTAssertFalse([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); + coordinator.nestedScrollPriority = HippyNestedScrollPrioritySelf; + XCTAssertTrue([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); +} + +- (void)testNestedScrollDoScrollViewDidScroll { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + HippyScrollView *scrollView = [HippyScrollView new]; + UIScrollView *sv = (UIScrollView *)scrollView.realScrollView; + [sv setContentOffset:CGPointMake(100.0, 200.0)]; + XCTAssert(CGPointEqualToPoint(sv.lastContentOffset, CGPointZero)); + [coordinator scrollViewDidScroll:scrollView.realScrollView]; + XCTAssert(CGPointEqualToPoint(sv.lastContentOffset, CGPointMake(100.0, 200.0))); +} + +@end