diff options
author | mkosiba@chromium.org <mkosiba@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-31 12:47:41 +0000 |
---|---|---|
committer | mkosiba@chromium.org <mkosiba@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-31 12:47:41 +0000 |
commit | f3693f98ddc553151c930683586b6e7999f28299 (patch) | |
tree | 4552b432e015c0868ef5978accdb5694c26da9ca | |
parent | 5ecf612f9ad4b992b54bb710795e57c5d26012e7 (diff) | |
download | chromium_src-f3693f98ddc553151c930683586b6e7999f28299.zip chromium_src-f3693f98ddc553151c930683586b6e7999f28299.tar.gz chromium_src-f3693f98ddc553151c930683586b6e7999f28299.tar.bz2 |
[android] Fire accessibility events when scrolling sublayers.
This enables the Android WebView to fire accessibility events for
sub-layer scrolling. Currently this is only limited to touch-driven
scrolling (JavaScript-initiated scrolling only works for the root
layer at the moment).
BUG=312318
android-only change, trybots are happy
NOTRY=true
Review URL: https://codereview.chromium.org/48973004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@232097 0039d316-1c4b-4281-b951-d872f2087c98
8 files changed, 227 insertions, 7 deletions
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java index 7032b72..d962d9f 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwContents.java +++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java @@ -155,6 +155,7 @@ public class AwContents { private OverScrollGlow mOverScrollGlow; // This can be accessed on any thread after construction. See AwContentsIoThreadClient. private final AwSettings mSettings; + private final ScrollAccessibilityHelper mScrollAccessibilityHelper; private boolean mIsPaused; private boolean mIsViewVisible; @@ -426,7 +427,6 @@ public class AwContents { mScrollOffsetManager.onFlingStartGesture(velocityX, velocityY); } - @Override public void onFlingCancelGesture() { mScrollOffsetManager.onFlingCancelGesture(); @@ -436,6 +436,11 @@ public class AwContents { public void onUnhandledFlingStartEvent() { mScrollOffsetManager.onUnhandledFlingStartEvent(); } + + @Override + public void onScrollUpdateGestureConsumed() { + mScrollAccessibilityHelper.postViewScrolledAccessibilityEventCallback(); + } } //-------------------------------------------------------------------------------------------- @@ -553,6 +558,7 @@ public class AwContents { mSettings.setDIPScale(mDIPScale); mScrollOffsetManager = new AwScrollOffsetManager(new AwScrollOffsetManagerDelegate(), new OverScroller(mContainerView.getContext())); + mScrollAccessibilityHelper = new ScrollAccessibilityHelper(mContainerView); setOverScrollMode(mContainerView.getOverScrollMode()); setScrollBarStyle(mInternalAccessAdapter.super_getScrollBarStyle()); @@ -1038,6 +1044,10 @@ public class AwContents { * @see View#onScrollChanged(int,int) */ public void onContainerViewScrollChanged(int l, int t, int oldl, int oldt) { + // A side-effect of View.onScrollChanged is that the scroll accessibility event being sent + // by the base class implementation. This is completely hidden from the base classes and + // cannot be prevented, which is why we need the code below. + mScrollAccessibilityHelper.removePostedViewScrolledAccessibilityEventCallback(); mScrollOffsetManager.onContainerViewScrollChanged(l, t); } @@ -1585,6 +1595,8 @@ public class AwContents { mComponentCallbacks = null; } + mScrollAccessibilityHelper.removePostedCallbacks(); + if (mPendingDetachCleanupReferences != null) { for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) { mPendingDetachCleanupReferences.get(i).cleanupNow(); diff --git a/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java b/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java new file mode 100644 index 0000000..d866dc9 --- /dev/null +++ b/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java @@ -0,0 +1,79 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview; + +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper used to post the VIEW_SCROLLED accessibility event. + * + * TODO(mkosiba): Investigate whether this is behavior we want to share with the chrome/ layer. + * TODO(mkosiba): We currently don't handle JS-initiated scrolling for layers other than the root + * layer. + */ +class ScrollAccessibilityHelper { + // This is copied straight out of android.view.ViewConfiguration. + private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 100; + + private class HandlerCallback implements Handler.Callback { + public static final int MSG_VIEW_SCROLLED = 1; + + private View mEventSender; + + public HandlerCallback(View eventSender) { + mEventSender = eventSender; + } + + @Override + public boolean handleMessage(Message msg) { + switch(msg.what) { + case MSG_VIEW_SCROLLED: + mMsgViewScrolledQueued = false; + mEventSender.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); + break; + default: + throw new IllegalStateException( + "AccessibilityInjector: unhandled message: " + msg.what); + } + return true; + } + } + + private Handler mHandler; + private boolean mMsgViewScrolledQueued; + + public ScrollAccessibilityHelper(View eventSender) { + mHandler = new Handler(new HandlerCallback(eventSender)); + } + + /** + * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. + * This event is sent at most once every + * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} + */ + public void postViewScrolledAccessibilityEventCallback() { + if (mMsgViewScrolledQueued) + return; + mMsgViewScrolledQueued = true; + + Message msg = mHandler.obtainMessage(HandlerCallback.MSG_VIEW_SCROLLED); + mHandler.sendMessageDelayed(msg, SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS); + } + + public void removePostedViewScrolledAccessibilityEventCallback() { + if (!mMsgViewScrolledQueued) + return; + mMsgViewScrolledQueued = false; + + mHandler.removeMessages(HandlerCallback.MSG_VIEW_SCROLLED); + } + + public void removePostedCallbacks() { + removePostedViewScrolledAccessibilityEventCallback(); + } +} diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java index 78e4ce7..452d4bf 100644 --- a/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java +++ b/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java @@ -279,9 +279,6 @@ public class AndroidScrollIntegrationTest extends AwTestBase { final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale); final JavascriptEventObserver onscrollObserver = new JavascriptEventObserver(); - Log.w("AndroidScrollIntegrationTest", String.format("scroll in Js (%d, %d) -> (%d, %d)", - targetScrollXCss, targetScrollYCss, targetScrollXPix, targetScrollYPix)); - getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { @@ -690,4 +687,77 @@ public class AndroidScrollIntegrationTest extends AwTestBase { break; } } + + private static class TestGestureStateListener implements ContentViewCore.GestureStateListener { + private CallbackHelper mOnScrollUpdateGestureConsumedHelper = new CallbackHelper(); + + public CallbackHelper getOnScrollUpdateGestureConsumedHelper() { + return mOnScrollUpdateGestureConsumedHelper; + } + + @Override + public void onPinchGestureStart() { + } + + @Override + public void onPinchGestureEnd() { + } + + @Override + public void onFlingStartGesture(int velocityX, int velocityY) { + } + + @Override + public void onFlingCancelGesture() { + } + + @Override + public void onUnhandledFlingStartEvent() { + } + + @Override + public void onScrollUpdateGestureConsumed() { + mOnScrollUpdateGestureConsumedHelper.notifyCalled(); + } + } + + @SmallTest + @Feature({"AndroidWebView"}) + public void testTouchScrollingConsumesScrollByGesture() throws Throwable { + final TestAwContentsClient contentsClient = new TestAwContentsClient(); + final ScrollTestContainerView testContainerView = + (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); + final TestGestureStateListener testGestureStateListener = new TestGestureStateListener(); + enableJavaScriptOnUiThread(testContainerView.getAwContents()); + + final int dragSteps = 10; + final int dragStepSize = 24; + // Watch out when modifying - if the y or x delta aren't big enough vertical or horizontal + // scroll snapping will kick in. + final int targetScrollXPix = dragStepSize * dragSteps; + final int targetScrollYPix = dragStepSize * dragSteps; + + loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, + "<div>" + + " <div style=\"width:10000px; height: 10000px;\"> force scrolling </div>" + + "</div>"); + + getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + testContainerView.getContentViewCore().setGestureStateListener( + testGestureStateListener); + } + }); + final CallbackHelper onScrollUpdateGestureConsumedHelper = + testGestureStateListener.getOnScrollUpdateGestureConsumedHelper(); + + final int callCount = onScrollUpdateGestureConsumedHelper.getCallCount(); + AwTestTouchUtils.dragCompleteView(testContainerView, + 0, -targetScrollXPix, // these need to be negative as we're scrolling down. + 0, -targetScrollYPix, + dragSteps, + null /* completionLatch */); + onScrollUpdateGestureConsumedHelper.waitForCallback(callCount); + } } diff --git a/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java b/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java index e3c5077..6090143 100644 --- a/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java +++ b/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java @@ -8,14 +8,18 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Bundle; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.FrameLayout; -import android.util.Log; import org.chromium.android_webview.AwContents; import org.chromium.content.browser.ContentViewCore; @@ -147,6 +151,32 @@ public class AwTestContainerView extends FrameLayout { super.onDraw(canvas); } + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + AccessibilityNodeProvider provider = + mAwContents.getAccessibilityNodeProvider(); + return provider == null ? super.getAccessibilityNodeProvider() : provider; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AwContents.class.getName()); + mAwContents.onInitializeAccessibilityNodeInfo(info); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AwContents.class.getName()); + mAwContents.onInitializeAccessibilityEvent(event); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return mAwContents.performAccessibilityAction(action, arguments); + } + // TODO: AwContents could define a generic class that holds an implementation similar to // the one below. private class InternalAccessAdapter implements AwContents.InternalAccessDelegate { diff --git a/content/browser/android/content_view_core_impl.cc b/content/browser/android/content_view_core_impl.cc index 8f09202..c423adb 100644 --- a/content/browser/android/content_view_core_impl.cc +++ b/content/browser/android/content_view_core_impl.cc @@ -492,6 +492,14 @@ void ContentViewCoreImpl::UnhandledFlingStartEvent() { Java_ContentViewCore_unhandledFlingStartEvent(env, j_obj.obj()); } +void ContentViewCoreImpl::OnScrollUpdateGestureConsumed() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env); + if (j_obj.is_null()) + return; + Java_ContentViewCore_onScrollUpdateGestureConsumed(env, j_obj.obj()); +} + void ContentViewCoreImpl::HasTouchEventHandlers(bool need_touch_events) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env); diff --git a/content/browser/android/content_view_core_impl.h b/content/browser/android/content_view_core_impl.h index 116174c..f1e43cc 100644 --- a/content/browser/android/content_view_core_impl.h +++ b/content/browser/android/content_view_core_impl.h @@ -258,6 +258,7 @@ class ContentViewCoreImpl : public ContentViewCore, bool HasFocus(); void ConfirmTouchEvent(InputEventAckState ack_result); void UnhandledFlingStartEvent(); + void OnScrollUpdateGestureConsumed(); void HasTouchEventHandlers(bool need_touch_events); void OnSelectionChanged(const std::string& text); void OnSelectionBoundsChanged( diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc index ad0e396..50b8472 100644 --- a/content/browser/renderer_host/render_widget_host_view_android.cc +++ b/content/browser/renderer_host/render_widget_host_view_android.cc @@ -1027,6 +1027,10 @@ void RenderWidgetHostViewAndroid::UnhandledWheelEvent( void RenderWidgetHostViewAndroid::GestureEventAck( int gesture_event_type, InputEventAckState ack_result) { + if (gesture_event_type == WebKit::WebInputEvent::GestureScrollUpdate && + ack_result == INPUT_EVENT_ACK_STATE_CONSUMED) { + content_view_core_->OnScrollUpdateGestureConsumed(); + } if (gesture_event_type == WebKit::WebInputEvent::GestureFlingStart && ack_result == INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS) { content_view_core_->UnhandledFlingStartEvent(); diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java index e4075f5..026f9b4 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java @@ -188,8 +188,8 @@ public class ContentViewCore } /** - * An interface that allows the embedder to be notified when the pinch gesture starts and - * stops. + * An interface that allows the embedder to be notified of events and state changes related to + * gesture processing. */ public interface GestureStateListener { /** @@ -216,6 +216,14 @@ public class ContentViewCore * Called when a fling event was not handled by the renderer. */ void onUnhandledFlingStartEvent(); + + /** + * Called to indicate that a scroll update gesture had been consumed by the page. + * This callback is called whenever any layer is scrolled (like a frame or div). It is + * not called when a JS touch handler consumes the event (preventDefault), it is not called + * for JS-initiated scrolling. + */ + void onScrollUpdateGestureConsumed(); } /** @@ -1307,6 +1315,14 @@ public class ContentViewCore } } + @SuppressWarnings("unused") + @CalledByNative + private void onScrollUpdateGestureConsumed() { + if (mGestureStateListener != null) { + mGestureStateListener.onScrollUpdateGestureConsumed(); + } + } + @Override public boolean sendGesture(int type, long timeMs, int x, int y, Bundle b) { if (offerGestureToEmbedder(type)) return false; |