Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ScrollView keyboardDismissMode - with crash fixed #3696

Merged
6 commits merged into from
Nov 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions change/react-native-windows-2019-11-22-14-27-58-treeDump.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "prerelease",
"comment": "Properly support ScrollView KeyboardDismissMode",
"packageName": "react-native-windows",
"email": "dida@ntdev.microsoft.com",
"commit": "6b4df0e13a371763a12ecee273011ad762bd9322",
"date": "2019-11-22T22:27:58.216Z"
}
31 changes: 26 additions & 5 deletions packages/playground/Samples/scrollViewSnapSample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class Bootstrap extends React.Component<{}, any> {
zoomValue: false,
alignToStartValue: true,
refreshing: false,
keyboardDismiss: false,
snapToOffsets: true,
pagingEnabled: false,
};
Expand Down Expand Up @@ -62,6 +63,10 @@ export default class Bootstrap extends React.Component<{}, any> {
this.setState({pagingEnabled: value});
};

toggleSwitch8 = (value: boolean) => {
this.setState({keyboardDismiss: value});
};

onRefresh = () => {
this.setState({refreshing: true});
wait(2000).then(() => this.setState({refreshing: false}));
Expand Down Expand Up @@ -190,16 +195,29 @@ export default class Bootstrap extends React.Component<{}, any> {
justifyContent: 'center',
padding: 20,
}}>
<Text>
{this.state.pagingEnabled
? 'pagingEnabled on'
: 'pagingEnabled off'}
</Text>
<Text>{'PagingEnabled'}</Text>
<Switch
onValueChange={this.toggleSwitch7}
value={this.state.pagingEnabled}
/>
</View>
<View
style={{
flexDirection: 'column',
alignSelf: 'stretch',
justifyContent: 'center',
padding: 20,
}}>
<Text>
{'KeyboardDismiss: '.concat(
this.state.keyboardDismiss ? 'on-drag' : 'none',
)}
</Text>
<Switch
onValueChange={this.toggleSwitch8}
value={this.state.keyboardDismiss}
/>
</View>
</View>
<View style={{flex: 0.8, alignSelf: 'center', flexDirection: 'column'}}>
<ScrollView
Expand All @@ -214,6 +232,9 @@ export default class Bootstrap extends React.Component<{}, any> {
onRefresh={this.onRefresh}
/>
}
keyboardDismissMode={
this.state.keyboardDismiss ? 'on-drag' : 'none'
}
snapToOffsets={
this.state.snapToOffsets ? [100.0, 500.0] : undefined
}
Expand Down
25 changes: 25 additions & 0 deletions vnext/ReactUWP/Utils/Helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,37 @@ bool IsAPIContractVxAvailable() {
return isAPIContractVxAvailable;
}

bool IsAPIContractV5Available() {
return IsAPIContractVxAvailable<5>();
}

bool IsAPIContractV6Available() {
return IsAPIContractVxAvailable<6>();
}

bool IsAPIContractV7Available() {
return IsAPIContractVxAvailable<7>();
}

bool IsAPIContractV8Available() {
return IsAPIContractVxAvailable<8>();
}

bool IsRS3OrHigher() {
return IsAPIContractV5Available();
}

bool IsRS4OrHigher() {
return IsAPIContractV6Available();
}

bool IsRS5OrHigher() {
return IsAPIContractV7Available();
}

bool Is19H1OrHigher() {
return IsAPIContractV8Available();
}

} // namespace uwp
}; // namespace react
3 changes: 3 additions & 0 deletions vnext/ReactUWP/Utils/Helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ inline typename T asEnum(folly::dynamic const &obj) {
ReactId getViewId(_In_ IReactInstance *instance, winrt::FrameworkElement const &fe);
std::int32_t CountOpenPopups();

bool IsRS3OrHigher();
bool IsRS4OrHigher();
bool IsRS5OrHigher();
bool Is19H1OrHigher();
} // namespace uwp
} // namespace react
10 changes: 6 additions & 4 deletions vnext/ReactUWP/Views/KeyboardEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ PreviewKeyboardEventHandler::PreviewKeyboardEventHandler(KeyboardEventCallback &

void PreviewKeyboardEventHandler::hook(XamlView xamlView) {
auto uiElement = xamlView.as<winrt::UIElement>();
if (m_keyDownCallback)
m_previewKeyDownRevoker = uiElement.PreviewKeyDown(winrt::auto_revoke, m_keyDownCallback);
if (uiElement.try_as<winrt::IUIElement7>()) {
if (m_keyDownCallback)
m_previewKeyDownRevoker = uiElement.PreviewKeyDown(winrt::auto_revoke, m_keyDownCallback);

if (m_keyUpCallback)
m_previewKeyUpRevoker = uiElement.PreviewKeyUp(winrt::auto_revoke, m_keyUpCallback);
if (m_keyUpCallback)
m_previewKeyUpRevoker = uiElement.PreviewKeyUp(winrt::auto_revoke, m_keyUpCallback);
}
}

void PreviewKeyboardEventHandler::unhook() {
Expand Down
1 change: 1 addition & 0 deletions vnext/ReactUWP/Views/ReactControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ void ReactControl::AttachRoot() noexcept {

m_touchEventHandler->AddTouchHandlers(m_xamlRootView);
m_previewKeyboardEventHandlerOnRoot->hook(m_xamlRootView);
m_SIPEventHandler->AttachView(m_xamlRootView, true /*fireKeyboradEvents*/);

auto initialProps = m_initialProps;
m_reactInstance->AttachMeasuredRootView(m_pParent, std::move(initialProps));
Expand Down
106 changes: 80 additions & 26 deletions vnext/ReactUWP/Views/SIPEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <Modules/NativeUIManager.h>

#include <ReactUWP\Utils\Helpers.h>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <winrt/Windows.Foundation.h>

Expand All @@ -15,43 +16,96 @@ using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI::ViewManagement::Core;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Media;
} // namespace winrt
namespace react {
namespace uwp {

SIPEventHandler::SIPEventHandler(const std::weak_ptr<IReactInstance> &reactInstance)
: m_wkReactInstance(reactInstance) {
auto coreInputView = winrt::CoreInputView::GetForCurrentView();
if (coreInputView) {
m_occlusionsChanged_revoker = coreInputView.OcclusionsChanged(
winrt::auto_revoke, [=](auto &&, const winrt::CoreInputViewOcclusionsChangedEventArgs &e) {
if (!e.Handled()) {
winrt::Rect finalRect = winrt::RectHelper::Empty();
winrt::IVectorView<winrt::CoreInputViewOcclusion> occlusions = e.Occlusions();
for (uint32_t i = 0; i < occlusions.Size(); i++) {
winrt::CoreInputViewOcclusion occlusion = occlusions.GetAt(i);
if (occlusion.OcclusionKind() == winrt::CoreInputViewOcclusionKind::Docked) {
finalRect = winrt::RectHelper::Union(finalRect, occlusion.OccludingRect());
: m_wkReactInstance(reactInstance), m_fireKeyboradEvents(false), m_finalRect(winrt::RectHelper::Empty()){};

SIPEventHandler::~SIPEventHandler() {
m_occlusionsChanged_revoker = {};
m_loadedRevoker = {};
}
// keyboardDidHide and keyboardDidShow events works on >= RS3
// TryShow and TryHide works on >= RS5

void SIPEventHandler::AttachView(XamlView xamlView, bool fireKeyboardEvents) {
m_fireKeyboradEvents = fireKeyboardEvents;
// hookup CoreInputView only after element is in the tree
m_view = winrt::make_weak(xamlView);
if (winrt::VisualTreeHelper::GetParent(xamlView)) {
InitializeCoreInputView();
} else {
m_loadedRevoker = xamlView.as<winrt::FrameworkElement>().Loaded(
winrt::auto_revoke, [this](const auto &sender, const auto &) { InitializeCoreInputView(); });
}
}

void SIPEventHandler::InitializeCoreInputView() {
if (const auto xamlView = m_view.get()) {
if (!IsRS3OrHigher()) {
return; // CoreInputView is only supported on >= RS3.
}

if (Is19H1OrHigher()) {
// 19H1 and higher supports island scenarios
auto uiElement(xamlView.as<winrt::UIElement>());
m_coreInputView = winrt::CoreInputView::GetForUIContext(uiElement.UIContext());
} else {
m_coreInputView = winrt::CoreInputView::GetForCurrentView();
}

if (m_coreInputView) {
auto occlusions = m_coreInputView.GetCoreInputViewOcclusions();
m_isShowing = !IsOcclusionsEmpty(occlusions);
m_occlusionsChanged_revoker = m_coreInputView.OcclusionsChanged(
winrt::auto_revoke, [this](auto &&, const winrt::CoreInputViewOcclusionsChangedEventArgs &e) {
if (!e.Handled()) {
bool wasShowing = m_isShowing;
m_isShowing = !IsOcclusionsEmpty(e.Occlusions());
if (wasShowing != m_isShowing && m_fireKeyboradEvents) {
if (!m_isShowing) {
folly::dynamic params = folly::dynamic::object("screenY", 0)("screenX", 0)("width", 0)("height", 0);
SendEvent("keyboardDidHide", std::move(params));
} else {
folly::dynamic params = folly::dynamic::object(
"endCoordinates",
folly::dynamic::object("screenY", m_finalRect.Y)("screenX", m_finalRect.X)(
"width", m_finalRect.Width)("height", m_finalRect.Height));
SendEvent("keyboardDidShow", std::move(params));
}
}
}
});
}
}
}
/*
void SIPEventHandler::TryShow() {
if (IsRS5OrHigher() && m_coreInputView && !m_isShowing) { // CoreInputView.TryShow is only avaliable after RS5
m_coreInputView.TryShow();
}
}
*/

if (winrt::RectHelper::GetIsEmpty(finalRect)) {
folly::dynamic params = folly::dynamic::object("screenY", 0)("screenX", 0)("width", 0)("height", 0);
SendEvent("keyboardDidHide", std::move(params));
} else {
folly::dynamic params = folly::dynamic::object(
"endCoordinates",
folly::dynamic::object("screenY", finalRect.Y)("screenX", finalRect.X)("width", finalRect.Width)(
"height", finalRect.Height));
SendEvent("keyboardDidShow", std::move(params));
}
}
});
void SIPEventHandler::TryHide() {
if (IsRS5OrHigher() && m_coreInputView && m_isShowing) { // CoreInputView.TryHide is only avaliable after RS5
m_coreInputView.TryHide();
}
}

SIPEventHandler::~SIPEventHandler() {
m_occlusionsChanged_revoker = {};
bool SIPEventHandler::IsOcclusionsEmpty(winrt::IVectorView<winrt::CoreInputViewOcclusion> occlusions) {
m_finalRect = winrt::RectHelper::Empty();
if (occlusions) {
for (const auto &occlusion : occlusions) {
if (occlusion.OcclusionKind() == winrt::CoreInputViewOcclusionKind::Docked) {
m_finalRect = winrt::RectHelper::Union(m_finalRect, occlusion.OccludingRect());
}
}
}
return (winrt::RectHelper::GetIsEmpty(m_finalRect));
}

void SIPEventHandler::SendEvent(std::string &&eventName, folly::dynamic &&parameters) {
Expand Down
19 changes: 18 additions & 1 deletion vnext/ReactUWP/Views/SIPEventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <winrt/Windows.UI.ViewManagement.Core.h>

namespace winrt {
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI::ViewManagement::Core;
} // namespace winrt

Expand All @@ -20,10 +20,27 @@ class SIPEventHandler {
SIPEventHandler(const std::weak_ptr<IReactInstance> &reactInstance);
virtual ~SIPEventHandler();

bool IsSIPShowing() {
return m_isShowing;
}

void AttachView(XamlView xamlView, bool fireKeyboardEvents);
// void TryShow();
void TryHide();

private:
bool IsOcclusionsEmpty(winrt::IVectorView<winrt::CoreInputViewOcclusion> occlusions);
void SendEvent(std::string &&eventName, folly::dynamic &&parameters);
std::weak_ptr<IReactInstance> m_wkReactInstance;
winrt::CoreInputView::OcclusionsChanged_revoker m_occlusionsChanged_revoker;
winrt::Rect m_finalRect;
winrt::CoreInputView m_coreInputView{nullptr};
winrt::weak_ref<XamlView> m_view{};
winrt::Windows::UI::Xaml::FrameworkElement::Loaded_revoker m_loadedRevoker{};
bool m_isShowing{false};
bool m_fireKeyboradEvents;

void InitializeCoreInputView();
};

} // namespace uwp
Expand Down
26 changes: 25 additions & 1 deletion vnext/ReactUWP/Views/ScrollViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "pch.h"

#include <ReactUWP\Views\SIPEventHandler.h>
#include <Views/ShadowNodeBase.h>
#include "Impl/ScrollViewUWPImplementation.h"
#include "ScrollViewManager.h"
Expand All @@ -20,6 +21,7 @@ class ScrollViewShadowNode : public ShadowNodeBase {

public:
ScrollViewShadowNode();
~ScrollViewShadowNode();
void dispatchCommand(int64_t commandId, const folly::dynamic &commandArgs) override;
void createView() override;
void updateProperties(const folly::dynamic &&props) override;
Expand All @@ -44,6 +46,9 @@ class ScrollViewShadowNode : public ShadowNodeBase {
bool m_isHorizontal = false;
bool m_isScrollingEnabled = true;
bool m_changeViewAfterLoaded = false;
bool m_dismissKeyboardOnDrag = false;

std::shared_ptr<SIPEventHandler> m_SIPEventHandler;

winrt::FrameworkElement::SizeChanged_revoker m_scrollViewerSizeChangedRevoker{};
winrt::FrameworkElement::SizeChanged_revoker m_contentSizeChangedRevoker{};
Expand All @@ -56,6 +61,10 @@ class ScrollViewShadowNode : public ShadowNodeBase {

ScrollViewShadowNode::ScrollViewShadowNode() {}

ScrollViewShadowNode::~ScrollViewShadowNode() {
m_SIPEventHandler.reset();
}

void ScrollViewShadowNode::dispatchCommand(int64_t commandId, const folly::dynamic &commandArgs) {
const auto scrollViewer = GetView().as<winrt::ScrollViewer>();
if (scrollViewer == nullptr)
Expand Down Expand Up @@ -186,6 +195,16 @@ void ScrollViewShadowNode::updateProperties(const folly::dynamic &&reactDiffMap)
if (valid) {
ScrollViewUWPImplementation(scrollViewer).SnapToEnd(snapToEnd);
}
} else if (propertyName == "keyboardDismissMode") {
m_dismissKeyboardOnDrag = false;
if (propertyValue.isString()) {
m_dismissKeyboardOnDrag = (propertyValue.getString() == "on-drag");
if (m_dismissKeyboardOnDrag) {
auto wkinstance = GetViewManager()->GetReactInstance();
m_SIPEventHandler = std::make_unique<SIPEventHandler>(wkinstance);
m_SIPEventHandler->AttachView(GetView(), false /*fireKeyboardEvents*/);
}
}
} else if (propertyName == "snapToAlignment") {
const auto [valid, snapToAlignment] = getPropertyAndValidity(propertyValue, winrt::SnapPointsAlignment::Near);
if (valid) {
Expand Down Expand Up @@ -241,6 +260,11 @@ void ScrollViewShadowNode::AddHandlers(const winrt::ScrollViewer &scrollViewer)
m_scrollViewerDirectManipulationStartedRevoker =
scrollViewer.DirectManipulationStarted(winrt::auto_revoke, [this](const auto &sender, const auto &) {
m_isScrolling = true;

if (m_dismissKeyboardOnDrag && m_SIPEventHandler) {
m_SIPEventHandler->TryHide();
}

const auto scrollViewer = sender.as<winrt::ScrollViewer>();
EmitScrollEvent(
scrollViewer,
Expand Down Expand Up @@ -402,7 +426,7 @@ folly::dynamic ScrollViewManager::GetNativeProps() const {
"showsHorizontalScrollIndicator", "boolean")("showsVerticalScrollIndicator", "boolean")(
"minimumZoomScale", "float")("maximumZoomScale", "float")("zoomScale", "float")("snapToInterval", "float")(
"snapToOffsets", "array")("snapToAlignment", "number")("snapToStart", "boolean")("snapToEnd", "boolean")(
"pagingEnabled", "boolean"));
"pagingEnabled", "boolean")("keyboardDismissMode", "string"));

return props;
}
Expand Down