/** * 描述:TODO * * @see android.view.View#onKeyUp(int, android.view.KeyEvent) * @author: zhaoqp * @date:2013-11-28 上午11:14:35 * @version v1.0 */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if (mReceivedInvokeKeyDown) { if (mItemCount > 0) { dispatchPress(mSelectedChild); postDelayed( new Runnable() { public void run() { dispatchUnpress(); } }, ViewConfiguration.getPressedStateDuration()); int selectedIndex = mSelectedPosition - mFirstPosition; performItemClick( getChildAt(selectedIndex), mSelectedPosition, mAdapter.getItemId(mSelectedPosition)); } } // Clear the flag mReceivedInvokeKeyDown = false; return true; } } return super.onKeyUp(keyCode, event); }
public boolean dispatchTouchEvent(MotionEvent paramMotionEvent) { switch (paramMotionEvent.getAction()) { case 2: default: case 0: case 1: case 3: case 4: } while (true) { return super.dispatchTouchEvent(paramMotionEvent); if (this.mHighlighEnabled) { startDelayTimer(); continue; if (this.mHighlighEnabled) removeDelayTimer(); if (this.mPressed) { this.mPressed = false; invalidate(); } else if (this.mHighlighEnabled) { if (this.mUnsetPressedState == null) this.mUnsetPressedState = new UnsetPressedState(null); this.mPressed = true; invalidate(); this.mPressDelayHandler.postDelayed( this.mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); continue; if (this.mHighlighEnabled) removeDelayTimer(); if (this.mPressed) { this.mPressed = false; invalidate(); } } } } }
private void initView(Context context) { mPaint = new Paint(); mPaint.setColor(Color.WHITE); Resources resources = context.getResources(); // get viewConfiguration mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // get Bitmap mBottom = BitmapFactory.decodeResource(resources, R.drawable.bottom); mBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.btn_pressed); mBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.btn_unpressed); mFrame = BitmapFactory.decodeResource(resources, R.drawable.frame); mMask = BitmapFactory.decodeResource(resources, R.drawable.mask); mCurBtnPic = mBtnNormal; mBtnWidth = mBtnPressed.getWidth(); mMaskWidth = mMask.getWidth(); mMaskHeight = mMask.getHeight(); mBtnOffPos = mBtnWidth / 2; mBtnOnPos = mMaskWidth - mBtnWidth / 2; mBtnPos = mChecked ? mBtnOnPos : mBtnOffPos; mRealPos = getRealPos(mBtnPos); final float density = getResources().getDisplayMetrics().density; mVelocity = (int) (VELOCITY * density + 0.5f); mExtendOffsetY = (int) (EXTENDED_OFFSET_Y * density + 0.5f); mSaveLayerRectF = new RectF(0, mExtendOffsetY, mMask.getWidth(), mMask.getHeight() + mExtendOffsetY); mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); }
@Override public boolean onTouchEvent(@NonNull MotionEvent event) { // Stop receiving touch events if we aren't within the bounds, including some slop. final int x = (int) event.getX(); final int y = (int) event.getY(); if (!pointInCloseBounds(x, y, mTouchSlop)) { setClosePressed(false); super.onTouchEvent(event); return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setClosePressed(true); break; case MotionEvent.ACTION_CANCEL: // Cancelled by a parent setClosePressed(false); break; case MotionEvent.ACTION_UP: if (isClosePressed()) { // Delay setting the unpressed state so that the button remains pressed // at least long enough to respond to the close event. if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); performClose(); } break; } return true; }
private void init(Context context, AttributeSet attrs) { mPaint = new Paint(); mPaint.setColor(Color.WHITE); Resources resources = context.getResources(); // get attrConfiguration TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton); int width = (int) array.getDimensionPixelSize(R.styleable.SwitchButton_bmWidth, 0); int height = (int) array.getDimensionPixelSize(R.styleable.SwitchButton_bmHeight, 0); array.recycle(); // size width or height if (width <= 0 || height <= 0) { width = COMMON_WIDTH_IN_PIXEL; height = COMMON_HEIGHT_IN_PIXEL; } else { float scale = (float) COMMON_WIDTH_IN_PIXEL / COMMON_HEIGHT_IN_PIXEL; if ((float) width / height > scale) { width = (int) (height * scale); } else if ((float) width / height < scale) { height = (int) (width / scale); } } // get viewConfiguration mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // get Bitmap bmBgGreen = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_bg_green); bmBgWhite = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_bg_white); bmBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_normal); bmBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_pressed); // size Bitmap bmBgGreen = Bitmap.createScaledBitmap(bmBgGreen, width, height, true); bmBgWhite = Bitmap.createScaledBitmap(bmBgWhite, width, height, true); bmBtnNormal = Bitmap.createScaledBitmap(bmBtnNormal, height, height, true); bmBtnPressed = Bitmap.createScaledBitmap(bmBtnPressed, height, height, true); bmCurBtnPic = bmBtnNormal; // 初始按钮图片 bmCurBgPic = mChecked ? bmBgGreen : bmBgWhite; // 初始背景图片 bgWidth = bmBgGreen.getWidth(); // 背景宽度 bgHeight = bmBgGreen.getHeight(); // 背景高度 btnWidth = bmBtnNormal.getWidth(); // 按钮宽度 offBtnPos = 0; // 关闭时在最左边 onBtnPos = bgWidth - btnWidth; // 开始时在右边 curBtnPos = mChecked ? onBtnPos : offBtnPos; // 按钮当前为初始位置 // get density float density = resources.getDisplayMetrics().density; mVelocity = (int) (VELOCITY * density + 0.5f); // 动画距离 mSaveLayerRectF = new RectF(0, 0, bgWidth, bgHeight); }
@Override public boolean onTouchEvent(MotionEvent event) { boolean superOnTouchEvent = super.onTouchEvent(event); if (!isEnabled()) return superOnTouchEvent; boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY()); if (isEventInBounds) { previousCoords.set(currentCoords.x, currentCoords.y); currentCoords.set((int) event.getX(), (int) event.getY()); } int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_UP: if (pressed) { childView.setPressed(true); postDelayed( new Runnable() { @Override public void run() { childView.setPressed(false); } }, ViewConfiguration.getPressedStateDuration()); } cancelPressedEvent(); break; case MotionEvent.ACTION_CANCEL: // currentCoords.set(); childView.onTouchEvent(event); if (!pressed) startRipple(); cancelPressedEvent(); break; case MotionEvent.ACTION_DOWN: eventCancelled = false; pressEvent = new PressEvent(event); pressEvent.run(); /** *这里注意当这个控件是在ScrollView内部的时候的问题 ** */ break; case MotionEvent.ACTION_MOVE: if (isEventInBounds && !eventCancelled) invalidate(); else if (!isEventInBounds) startRipple(); if (!isEventInBounds) { cancelPressedEvent(); if (hoverAnimator != null) hoverAnimator.cancel(); childView.onTouchEvent(event); eventCancelled = true; } break; } return true; }
private void initView(Context context) { mPaint = new Paint(); mPaint.setColor(Color.WHITE); Resources resources = context.getResources(); // get viewConfiguration mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // get Bitmap mBottom = BitmapFactory.decodeResource(resources, R.drawable.kaiguan); mBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.yuan); mBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.yuan); mFrame = BitmapFactory.decodeResource(resources, R.drawable.guan); mMask = BitmapFactory.decodeResource(resources, R.drawable.kai); mCurBtnPic = mBtnNormal; mBtnWidth = mBtnPressed.getWidth(); mMaskWidth = mMask.getWidth(); mMaskHeight = mMask.getHeight(); mBtnOffPos = mBtnWidth / 2; mBtnOnPos = mMaskWidth - mBtnWidth / 2; // 判断起始位置,如果设定了mChecked为true,起始位置为 mBtnOnPos mBtnPos = mChecked ? mBtnOnPos : mBtnOffPos; mRealPos = getRealPos(mBtnPos); // density 密度 final float density = getResources().getDisplayMetrics().density; // 方法是获取资源密度(Density) mVelocity = (int) (VELOCITY * density + 0.5f); mExtendOffsetY = (int) (EXTENDED_OFFSET_Y * density + 0.5f); // 创建一个新的矩形与指定的坐标。 mSaveLayerRectF = new RectF(0, mExtendOffsetY, mMask.getWidth(), mMask.getHeight() + mExtendOffsetY); mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); // PorterDuff.Mode.SRC_IN // :这个属性代表 // 取两层绘制交集。显示上层。 }
@Test public void methodsShouldReturnAndroidConstants() { Activity context = new Activity(); ViewConfiguration viewConfiguration = ViewConfiguration.get(context); assertEquals(10, ViewConfiguration.getScrollBarSize()); assertEquals(250, ViewConfiguration.getScrollBarFadeDuration()); assertEquals(300, ViewConfiguration.getScrollDefaultDelay()); assertEquals(12, ViewConfiguration.getFadingEdgeLength()); assertEquals(125, ViewConfiguration.getPressedStateDuration()); assertEquals(500, ViewConfiguration.getLongPressTimeout()); assertEquals(115, ViewConfiguration.getTapTimeout()); assertEquals(500, ViewConfiguration.getJumpTapTimeout()); assertEquals(300, ViewConfiguration.getDoubleTapTimeout()); assertEquals(12, ViewConfiguration.getEdgeSlop()); assertEquals(16, ViewConfiguration.getTouchSlop()); assertEquals(16, ViewConfiguration.getWindowTouchSlop()); assertEquals(50, ViewConfiguration.getMinimumFlingVelocity()); assertEquals(4000, ViewConfiguration.getMaximumFlingVelocity()); assertEquals(320 * 480 * 4, ViewConfiguration.getMaximumDrawingCacheSize()); assertEquals(3000, ViewConfiguration.getZoomControlsTimeout()); assertEquals(500, ViewConfiguration.getGlobalActionKeyTimeout()); assertEquals(0.015f, ViewConfiguration.getScrollFriction()); assertEquals(1f, context.getResources().getDisplayMetrics().density); assertEquals(10, viewConfiguration.getScaledScrollBarSize()); assertEquals(12, viewConfiguration.getScaledFadingEdgeLength()); assertEquals(12, viewConfiguration.getScaledEdgeSlop()); assertEquals(16, viewConfiguration.getScaledTouchSlop()); assertEquals(32, viewConfiguration.getScaledPagingTouchSlop()); assertEquals(100, viewConfiguration.getScaledDoubleTapSlop()); assertEquals(16, viewConfiguration.getScaledWindowTouchSlop()); assertEquals(50, viewConfiguration.getScaledMinimumFlingVelocity()); assertEquals(4000, viewConfiguration.getScaledMaximumFlingVelocity()); }
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); boolean wasHeaderChildBeingPressed = mHeaderChildBeingPressed; if (mHeaderChildBeingPressed) { final View tempHeader = getHeaderAt(mMotionHeaderPosition); final View headerHolder = mMotionHeaderPosition == MATCHED_STICKIED_HEADER ? tempHeader : getChildAt(mMotionHeaderPosition); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mHeaderChildBeingPressed = false; } if (tempHeader != null) { tempHeader.dispatchTouchEvent(transformEvent(ev, mMotionHeaderPosition)); tempHeader.invalidate(); tempHeader.postDelayed( new Runnable() { public void run() { invalidate( 0, headerHolder.getTop(), getWidth(), headerHolder.getTop() + headerHolder.getHeight()); } }, ViewConfiguration.getPressedStateDuration()); invalidate( 0, headerHolder.getTop(), getWidth(), headerHolder.getTop() + headerHolder.getHeight()); } } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForHeaderTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); final int y = (int) ev.getY(); mMotionY = y; mMotionHeaderPosition = findMotionHeader(y); if (mMotionHeaderPosition == NO_MATCHED_HEADER || mScrollState == SCROLL_STATE_FLING) { // Don't consume the event and pass it to super because we // can't handle it yet. break; } else { View tempHeader = getHeaderAt(mMotionHeaderPosition); if (tempHeader != null) { if (tempHeader.dispatchTouchEvent(transformEvent(ev, mMotionHeaderPosition))) { mHeaderChildBeingPressed = true; tempHeader.setPressed(true); } tempHeader.invalidate(); if (mMotionHeaderPosition != MATCHED_STICKIED_HEADER) { tempHeader = getChildAt(mMotionHeaderPosition); } invalidate( 0, tempHeader.getTop(), getWidth(), tempHeader.getTop() + tempHeader.getHeight()); } } mTouchMode = TOUCH_MODE_DOWN; return true; case MotionEvent.ACTION_MOVE: if (mMotionHeaderPosition != NO_MATCHED_HEADER && Math.abs(ev.getY() - mMotionY) > mTouchSlop) { // Detected scroll initiation so cancel touch completion on // header. mTouchMode = TOUCH_MODE_REST; // if (!mHeaderChildBeingPressed) { final View header = getHeaderAt(mMotionHeaderPosition); if (header != null) { header.setPressed(false); header.invalidate(); } final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } mMotionHeaderPosition = NO_MATCHED_HEADER; // } } break; case MotionEvent.ACTION_UP: if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) { mTouchMode = TOUCH_MODE_REST; return true; } if (mTouchMode == TOUCH_MODE_REST || mMotionHeaderPosition == NO_MATCHED_HEADER) { break; } final View header = getHeaderAt(mMotionHeaderPosition); if (!wasHeaderChildBeingPressed) { if (header != null) { if (mTouchMode != TOUCH_MODE_DOWN) { header.setPressed(false); } if (mPerformHeaderClick == null) { mPerformHeaderClick = new PerformHeaderClick(); } final PerformHeaderClick performHeaderClick = mPerformHeaderClick; performHeaderClick.mClickMotionPosition = mMotionHeaderPosition; performHeaderClick.rememberWindowAttachCount(); if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks( mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } if (!mDataChanged) { /* * Got here so must be a tap. The long press * would have triggered on the callback handler. */ mTouchMode = TOUCH_MODE_TAP; header.setPressed(true); setPressed(true); if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mMotionHeaderPosition = NO_MATCHED_HEADER; mTouchModeReset = null; mTouchMode = TOUCH_MODE_REST; header.setPressed(false); setPressed(false); header.invalidate(); invalidate(0, header.getTop(), getWidth(), header.getHeight()); if (!mDataChanged) { performHeaderClick.run(); } } }; postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; } } else if (!mDataChanged) { performHeaderClick.run(); } } } mTouchMode = TOUCH_MODE_REST; return true; } return super.onTouchEvent(ev); }
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForHeaderTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); final int y = (int) ev.getY(); mMotionY = y; mMotionHeaderPosition = findMotionHeader(y); if (mMotionHeaderPosition == NO_MATCHED_HEADER || mScrollState == SCROLL_STATE_FLING) { // Don't consume the event and pass it to super because we // can't handle it yet. break; } mTouchMode = TOUCH_MODE_DOWN; return true; case MotionEvent.ACTION_MOVE: if (mMotionHeaderPosition != NO_MATCHED_HEADER && Math.abs(ev.getY() - mMotionY) > mTouchSlop) { // Detected scroll initiation so cancel touch completion on // header. mTouchMode = TOUCH_MODE_REST; final View header = getHeaderAt(mMotionHeaderPosition); if (header != null) { header.setPressed(false); } final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } mMotionHeaderPosition = NO_MATCHED_HEADER; } break; case MotionEvent.ACTION_UP: if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) { return true; } if (mTouchMode == TOUCH_MODE_REST || mMotionHeaderPosition == NO_MATCHED_HEADER) { break; } final View header = getHeaderAt(mMotionHeaderPosition); if (header != null && !header.hasFocusable()) { if (mTouchMode != TOUCH_MODE_DOWN) { header.setPressed(false); } if (mPerformHeaderClick == null) { mPerformHeaderClick = new PerformHeaderClick(); } final PerformHeaderClick performHeaderClick = mPerformHeaderClick; performHeaderClick.mClickMotionPosition = mMotionHeaderPosition; performHeaderClick.rememberWindowAttachCount(); if (mTouchMode != TOUCH_MODE_DOWN || mTouchMode != TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks( mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } if (!mDataChanged) { // Got here so must be a tap. The long press would // have trigger on the callback handler. Probably. mTouchMode = TOUCH_MODE_TAP; header.setPressed(true); setPressed(true); if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mTouchMode = TOUCH_MODE_REST; header.setPressed(false); setPressed(false); if (!mDataChanged) { performHeaderClick.run(); } } }; postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; } } else if (!mDataChanged) { performHeaderClick.run(); } } mTouchMode = TOUCH_MODE_REST; return true; } return super.onTouchEvent(ev); }
@Override public boolean onTouchEvent(MotionEvent event) { boolean superOnTouchEvent = super.onTouchEvent(event); if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent; boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY()); if (isEventInBounds) { previousCoords.set(currentCoords.x, currentCoords.y); currentCoords.set((int) event.getX(), (int) event.getY()); } boolean gestureResult = gestureDetector.onTouchEvent(event); if (gestureResult || mHasPerformedLongPress) { return true; } else { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_UP: pendingClickEvent = new PerformClickEvent(); if (prePressed) { childView.setPressed(true); postDelayed( new Runnable() { @Override public void run() { childView.setPressed(false); } }, ViewConfiguration.getPressedStateDuration()); } if (isEventInBounds) { startRipple(pendingClickEvent); } else if (!rippleHover) { setRadius(0); } if (!rippleDelayClick && isEventInBounds) { pendingClickEvent.run(); } cancelPressedEvent(); break; case MotionEvent.ACTION_DOWN: setPositionInAdapter(); eventCancelled = false; pendingPressEvent = new PressedEvent(event); if (isInScrollingContainer()) { cancelPressedEvent(); prePressed = true; postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout()); } else { pendingPressEvent.run(); } break; case MotionEvent.ACTION_CANCEL: if (rippleInAdapter) { // dont use current coords in adapter since they tend to jump drastically on scroll currentCoords.set(previousCoords.x, previousCoords.y); previousCoords = new Point(); } childView.onTouchEvent(event); if (rippleHover) { if (!prePressed) { startRipple(null); } } else { childView.setPressed(false); } cancelPressedEvent(); break; case MotionEvent.ACTION_MOVE: if (rippleHover) { if (isEventInBounds && !eventCancelled) { invalidate(); } else if (!isEventInBounds) { startRipple(null); } } if (!isEventInBounds) { cancelPressedEvent(); if (hoverAnimator != null) { hoverAnimator.cancel(); } childView.onTouchEvent(event); eventCancelled = true; } break; } return true; } }
/** * Perform asynchronous dispatch of input events in a {@link WebView}. * * <p>This dispatcher is shared by the UI thread ({@link WebViewClassic}) and web kit thread ({@link * WebViewCore}). The UI thread enqueues events for processing, waits for the web kit thread to * handle them, and then performs additional processing depending on the outcome. * * <p>How it works: * * <p>1. The web view thread receives an input event from the input system on the UI thread in its * {@link WebViewClassic#onTouchEvent} handler. It sends the input event to the dispatcher, then * immediately returns true to the input system to indicate that it will handle the event. * * <p>2. The web kit thread is notified that an event has been enqueued. Meanwhile additional events * may be enqueued from the UI thread. In some cases, the dispatcher may decide to coalesce motion * events into larger batches or to cancel events that have been sitting in the queue for too long. * * <p>3. The web kit thread wakes up and handles all input events that are waiting for it. After * processing each input event, it informs the dispatcher whether the web application has decided to * handle the event itself and to prevent default event handling. * * <p>4. If web kit indicates that it wants to prevent default event handling, then web kit consumes * the remainder of the gesture and web view receives a cancel event if needed. Otherwise, the web * view handles the gesture on the UI thread normally. * * <p>5. If the web kit thread takes too long to handle an input event, then it loses the right to * handle it. The dispatcher synthesizes a cancellation event for web kit and then tells the web * view on the UI thread to handle the event that timed out along with the rest of the gesture. * * <p>One thing to keep in mind about the dispatcher is that what goes into the dispatcher is not * necessarily what the web kit or UI thread will see. As mentioned above, the dispatcher may tweak * the input event stream to improve responsiveness. Both web view and web kit are guaranteed to * perceive a consistent stream of input events but they might not always see the same events * (especially if one decides to prevent the other from handling a particular gesture). * * <p>This implementation very deliberately does not refer to the {@link WebViewClassic} or {@link * WebViewCore} classes, preferring to communicate with them only via interfaces to avoid * unintentional coupling to their implementation details. * * <p>Currently, the input dispatcher only handles pointer events (includes touch, hover and scroll * events). In principle, it could be extended to handle trackball and key events if needed. * * @hide */ final class WebViewInputDispatcher { private static final String TAG = "WebViewInputDispatcher"; private static final boolean DEBUG = false; // This enables batching of MotionEvents. It will combine multiple MotionEvents // together into a single MotionEvent if more events come in while we are // still waiting on the processing of a previous event. // If this is set to false, we will instead opt to drop ACTION_MOVE // events we cannot keep up with. // TODO: If batching proves to be working well, remove this private static final boolean ENABLE_EVENT_BATCHING = true; private final Object mLock = new Object(); // Pool of queued input events. (guarded by mLock) private static final int MAX_DISPATCH_EVENT_POOL_SIZE = 10; private DispatchEvent mDispatchEventPool; private int mDispatchEventPoolSize; // Posted state, tracks events posted to the dispatcher. (guarded by mLock) private final TouchStream mPostTouchStream = new TouchStream(); private boolean mPostSendTouchEventsToWebKit; private boolean mPostDoNotSendTouchEventsToWebKitUntilNextGesture; private boolean mPostLongPressScheduled; private boolean mPostClickScheduled; private boolean mPostShowTapHighlightScheduled; private boolean mPostHideTapHighlightScheduled; private int mPostLastWebKitXOffset; private int mPostLastWebKitYOffset; private float mPostLastWebKitScale; // State for event tracking (click, longpress, double tap, etc..) private boolean mIsDoubleTapCandidate; private boolean mIsTapCandidate; private float mInitialDownX; private float mInitialDownY; private float mTouchSlopSquared; private float mDoubleTapSlopSquared; // Web kit state, tracks events observed by web kit. (guarded by mLock) private final DispatchEventQueue mWebKitDispatchEventQueue = new DispatchEventQueue(); private final TouchStream mWebKitTouchStream = new TouchStream(); private final WebKitCallbacks mWebKitCallbacks; private final WebKitHandler mWebKitHandler; private boolean mWebKitDispatchScheduled; private boolean mWebKitTimeoutScheduled; private long mWebKitTimeoutTime; /// M: Web kit state, tracks events prevent by web kit. private final PreventTouchStream mWebKitPreventTouchStream = new PreventTouchStream(); // UI state, tracks events observed by the UI. (guarded by mLock) private final DispatchEventQueue mUiDispatchEventQueue = new DispatchEventQueue(); private final TouchStream mUiTouchStream = new TouchStream(); private final UiCallbacks mUiCallbacks; private final UiHandler mUiHandler; private boolean mUiDispatchScheduled; // Give up on web kit handling of input events when this timeout expires. private static final long WEBKIT_TIMEOUT_MILLIS = 200; private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); private static final int LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() + TAP_TIMEOUT; private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); private static final int PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration(); /** * Event type: Indicates a touch event type. * * <p>This event is delivered together with a {@link MotionEvent} with one of the following * actions: {@link MotionEvent#ACTION_DOWN}, {@link MotionEvent#ACTION_MOVE}, {@link * MotionEvent#ACTION_UP}, {@link MotionEvent#ACTION_POINTER_DOWN}, {@link * MotionEvent#ACTION_POINTER_UP}, {@link MotionEvent#ACTION_CANCEL}. */ public static final int EVENT_TYPE_TOUCH = 0; /** * Event type: Indicates a hover event type. * * <p>This event is delivered together with a {@link MotionEvent} with one of the following * actions: {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE}, {@link * MotionEvent#ACTION_HOVER_MOVE}. */ public static final int EVENT_TYPE_HOVER = 1; /** * Event type: Indicates a scroll event type. * * <p>This event is delivered together with a {@link MotionEvent} with action {@link * MotionEvent#ACTION_SCROLL}. */ public static final int EVENT_TYPE_SCROLL = 2; /** * Event type: Indicates a long-press event type. * * <p>This event is delivered in the middle of a sequence of {@link #EVENT_TYPE_TOUCH} events. It * includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_MOVE} that indicates the * current touch coordinates of the long-press. * * <p>This event is sent when the current touch gesture has been held longer than the long-press * interval. */ public static final int EVENT_TYPE_LONG_PRESS = 3; /** * Event type: Indicates a click event type. * * <p>This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that comprise a * complete gesture ending with {@link MotionEvent#ACTION_UP}. It includes a {@link MotionEvent} * with action {@link MotionEvent#ACTION_UP} that indicates the location of the click. * * <p>This event is sent shortly after the end of a touch after the double-tap interval has * expired to indicate a click. */ public static final int EVENT_TYPE_CLICK = 4; /** * Event type: Indicates a double-tap event type. * * <p>This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that comprise a * complete gesture ending with {@link MotionEvent#ACTION_UP}. It includes a {@link MotionEvent} * with action {@link MotionEvent#ACTION_UP} that indicates the location of the double-tap. * * <p>This event is sent immediately after a sequence of two touches separated in time by no more * than the double-tap interval and separated in space by no more than the double-tap slop. */ public static final int EVENT_TYPE_DOUBLE_TAP = 5; /** Event type: Indicates that a hit test should be performed */ public static final int EVENT_TYPE_HIT_TEST = 6; /** Flag: This event is private to this queue. Do not forward it. */ public static final int FLAG_PRIVATE = 1 << 0; /** * Flag: This event is currently being processed by web kit. If a timeout occurs, make a copy of * it before forwarding the event to another queue. */ public static final int FLAG_WEBKIT_IN_PROGRESS = 1 << 1; /** Flag: A timeout occurred while waiting for web kit to process this input event. */ public static final int FLAG_WEBKIT_TIMEOUT = 1 << 2; /** * Flag: Indicates that the event was transformed for delivery to web kit. The event must be * transformed back before being delivered to the UI. */ public static final int FLAG_WEBKIT_TRANSFORMED_EVENT = 1 << 3; public WebViewInputDispatcher(UiCallbacks uiCallbacks, WebKitCallbacks webKitCallbacks) { this.mUiCallbacks = uiCallbacks; mUiHandler = new UiHandler(uiCallbacks.getUiLooper()); this.mWebKitCallbacks = webKitCallbacks; mWebKitHandler = new WebKitHandler(webKitCallbacks.getWebKitLooper()); ViewConfiguration config = ViewConfiguration.get(mUiCallbacks.getContext()); mDoubleTapSlopSquared = config.getScaledDoubleTapSlop(); mDoubleTapSlopSquared = (mDoubleTapSlopSquared * mDoubleTapSlopSquared); mTouchSlopSquared = config.getScaledTouchSlop(); mTouchSlopSquared = (mTouchSlopSquared * mTouchSlopSquared); } /** * Sets whether web kit wants to receive touch events. * * @param enable True to enable dispatching of touch events to web kit, otherwise web kit will be * skipped. */ public void setWebKitWantsTouchEvents(boolean enable) { if (DEBUG) { Log.d(TAG, "webkitWantsTouchEvents: " + enable); } synchronized (mLock) { if (mPostSendTouchEventsToWebKit != enable) { if (!enable) { enqueueWebKitCancelTouchEventIfNeededLocked(); } mPostSendTouchEventsToWebKit = enable; } } } /** * Posts a pointer event to the dispatch queue. * * @param event The event to post. * @param webKitXOffset X offset to apply to events before dispatching them to web kit. * @param webKitYOffset Y offset to apply to events before dispatching them to web kit. * @param webKitScale The scale factor to apply to translated events before dispatching them to * web kit. * @return True if the dispatcher will handle the event, false if the event is unsupported. */ public boolean postPointerEvent( MotionEvent event, int webKitXOffset, int webKitYOffset, float webKitScale) { if (event == null) { throw new IllegalArgumentException("event cannot be null"); } if (DEBUG) { Log.d(TAG, "postPointerEvent: " + event); } final int action = event.getActionMasked(); final int eventType; switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: eventType = EVENT_TYPE_TOUCH; break; case MotionEvent.ACTION_SCROLL: eventType = EVENT_TYPE_SCROLL; break; case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_EXIT: eventType = EVENT_TYPE_HOVER; break; default: return false; // currently unsupported event type } synchronized (mLock) { // Ensure that the event is consistent and should be delivered. MotionEvent eventToEnqueue = event; if (eventType == EVENT_TYPE_TOUCH) { eventToEnqueue = mPostTouchStream.update(event); if (eventToEnqueue == null) { if (DEBUG) { Log.d(TAG, "postPointerEvent: dropped event " + event); } unscheduleLongPressLocked(); unscheduleClickLocked(); hideTapCandidateLocked(); return false; } if (action == MotionEvent.ACTION_DOWN && mPostSendTouchEventsToWebKit) { if (mUiCallbacks.shouldInterceptTouchEvent(eventToEnqueue)) { mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true; } else if (mPostDoNotSendTouchEventsToWebKitUntilNextGesture) { // Recover from a previous web kit timeout. mPostDoNotSendTouchEventsToWebKitUntilNextGesture = false; } } } // Copy the event because we need to retain ownership. if (eventToEnqueue == event) { eventToEnqueue = event.copy(); } DispatchEvent d = obtainDispatchEventLocked( eventToEnqueue, eventType, 0, webKitXOffset, webKitYOffset, webKitScale); updateStateTrackersLocked(d, event); enqueueEventLocked(d); } return true; } private void scheduleLongPressLocked() { unscheduleLongPressLocked(); mPostLongPressScheduled = true; mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_LONG_PRESS, LONG_PRESS_TIMEOUT); } private void unscheduleLongPressLocked() { if (mPostLongPressScheduled) { mPostLongPressScheduled = false; mUiHandler.removeMessages(UiHandler.MSG_LONG_PRESS); } } private void postLongPress() { synchronized (mLock) { if (!mPostLongPressScheduled) { return; } mPostLongPressScheduled = false; MotionEvent event = mPostTouchStream.getLastEvent(); if (event == null) { return; } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: break; default: return; } MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); eventToEnqueue.setAction(MotionEvent.ACTION_MOVE); DispatchEvent d = obtainDispatchEventLocked( eventToEnqueue, EVENT_TYPE_LONG_PRESS, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); enqueueEventLocked(d); } } private void hideTapCandidateLocked() { unscheduleHideTapHighlightLocked(); unscheduleShowTapHighlightLocked(); mUiCallbacks.showTapHighlight(false); } private void showTapCandidateLocked() { unscheduleHideTapHighlightLocked(); unscheduleShowTapHighlightLocked(); mUiCallbacks.showTapHighlight(true); } private void scheduleShowTapHighlightLocked() { unscheduleShowTapHighlightLocked(); mPostShowTapHighlightScheduled = true; mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_SHOW_TAP_HIGHLIGHT, TAP_TIMEOUT); } private void unscheduleShowTapHighlightLocked() { if (mPostShowTapHighlightScheduled) { mPostShowTapHighlightScheduled = false; mUiHandler.removeMessages(UiHandler.MSG_SHOW_TAP_HIGHLIGHT); } } private void scheduleHideTapHighlightLocked() { unscheduleHideTapHighlightLocked(); mPostHideTapHighlightScheduled = true; mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_HIDE_TAP_HIGHLIGHT, PRESSED_STATE_DURATION); } private void unscheduleHideTapHighlightLocked() { if (mPostHideTapHighlightScheduled) { mPostHideTapHighlightScheduled = false; mUiHandler.removeMessages(UiHandler.MSG_HIDE_TAP_HIGHLIGHT); } } private void postShowTapHighlight(boolean show) { synchronized (mLock) { if (show) { if (!mPostShowTapHighlightScheduled) { return; } mPostShowTapHighlightScheduled = false; } else { if (!mPostHideTapHighlightScheduled) { return; } mPostHideTapHighlightScheduled = false; } mUiCallbacks.showTapHighlight(show); } } private void scheduleClickLocked() { unscheduleClickLocked(); mPostClickScheduled = true; mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_CLICK, DOUBLE_TAP_TIMEOUT); } private void unscheduleClickLocked() { if (mPostClickScheduled) { mPostClickScheduled = false; mUiHandler.removeMessages(UiHandler.MSG_CLICK); } } private void postClick() { synchronized (mLock) { if (!mPostClickScheduled) { return; } mPostClickScheduled = false; MotionEvent event = mPostTouchStream.getLastEvent(); if (event == null || event.getAction() != MotionEvent.ACTION_UP) { return; } showTapCandidateLocked(); MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); DispatchEvent d = obtainDispatchEventLocked( eventToEnqueue, EVENT_TYPE_CLICK, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); enqueueEventLocked(d); } } private void checkForDoubleTapOnDownLocked(MotionEvent event) { mIsDoubleTapCandidate = false; if (!mPostClickScheduled) { return; } int deltaX = (int) mInitialDownX - (int) event.getX(); int deltaY = (int) mInitialDownY - (int) event.getY(); if ((deltaX * deltaX + deltaY * deltaY) < mDoubleTapSlopSquared) { unscheduleClickLocked(); mIsDoubleTapCandidate = true; } } private boolean isClickCandidateLocked(MotionEvent event) { if (event == null || event.getActionMasked() != MotionEvent.ACTION_UP || !mIsTapCandidate) { return false; } long downDuration = event.getEventTime() - event.getDownTime(); return downDuration < LONG_PRESS_TIMEOUT; } private void enqueueDoubleTapLocked(MotionEvent event) { MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); DispatchEvent d = obtainDispatchEventLocked( eventToEnqueue, EVENT_TYPE_DOUBLE_TAP, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); enqueueEventLocked(d); } private void enqueueHitTestLocked(MotionEvent event) { mUiCallbacks.clearPreviousHitTest(); MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); DispatchEvent d = obtainDispatchEventLocked( eventToEnqueue, EVENT_TYPE_HIT_TEST, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); enqueueEventLocked(d); } private void checkForSlopLocked(MotionEvent event) { if (!mIsTapCandidate) { return; } int deltaX = (int) mInitialDownX - (int) event.getX(); int deltaY = (int) mInitialDownY - (int) event.getY(); if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquared) { unscheduleLongPressLocked(); mIsTapCandidate = false; hideTapCandidateLocked(); } } private void updateStateTrackersLocked(DispatchEvent d, MotionEvent event) { mPostLastWebKitXOffset = d.mWebKitXOffset; mPostLastWebKitYOffset = d.mWebKitYOffset; mPostLastWebKitScale = d.mWebKitScale; int action = event != null ? event.getAction() : MotionEvent.ACTION_CANCEL; if (d.mEventType != EVENT_TYPE_TOUCH) { return; } if (action == MotionEvent.ACTION_CANCEL || event.getPointerCount() > 1) { unscheduleLongPressLocked(); unscheduleClickLocked(); hideTapCandidateLocked(); mIsDoubleTapCandidate = false; mIsTapCandidate = false; hideTapCandidateLocked(); } else if (action == MotionEvent.ACTION_DOWN) { checkForDoubleTapOnDownLocked(event); scheduleLongPressLocked(); mIsTapCandidate = true; mInitialDownX = event.getX(); mInitialDownY = event.getY(); enqueueHitTestLocked(event); if (mIsDoubleTapCandidate) { hideTapCandidateLocked(); } else { scheduleShowTapHighlightLocked(); } } else if (action == MotionEvent.ACTION_UP) { unscheduleLongPressLocked(); if (isClickCandidateLocked(event)) { if (mIsDoubleTapCandidate) { hideTapCandidateLocked(); enqueueDoubleTapLocked(event); } else { scheduleClickLocked(); } } else { hideTapCandidateLocked(); } } else if (action == MotionEvent.ACTION_MOVE) { checkForSlopLocked(event); } } /** * Dispatches pending web kit events. Must only be called from the web kit thread. * * <p>This method may be used to flush the queue of pending input events immediately. This method * may help to reduce input dispatch latency if called before certain expensive operations such as * drawing. */ public void dispatchWebKitEvents() { dispatchWebKitEvents(false); } private void dispatchWebKitEvents(boolean calledFromHandler) { for (; ; ) { // Get the next event, but leave it in the queue so we can move it to the UI // queue if a timeout occurs. DispatchEvent d; MotionEvent event; final int eventType; int flags; synchronized (mLock) { if (!ENABLE_EVENT_BATCHING) { drainStaleWebKitEventsLocked(); } d = mWebKitDispatchEventQueue.mHead; if (d == null) { if (mWebKitDispatchScheduled) { mWebKitDispatchScheduled = false; if (!calledFromHandler) { mWebKitHandler.removeMessages(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS); } } return; } event = d.mEvent; if (event != null) { event.offsetLocation(d.mWebKitXOffset, d.mWebKitYOffset); event.scale(d.mWebKitScale); d.mFlags |= FLAG_WEBKIT_TRANSFORMED_EVENT; } eventType = d.mEventType; if (eventType == EVENT_TYPE_TOUCH) { event = mWebKitTouchStream.update(event); if (DEBUG && event == null && d.mEvent != null) { Log.d(TAG, "dispatchWebKitEvents: dropped event " + d.mEvent); } } d.mFlags |= FLAG_WEBKIT_IN_PROGRESS; flags = d.mFlags; } // Handle the event. final boolean preventDefault; if (event == null) { preventDefault = false; } else { preventDefault = dispatchWebKitEvent(event, eventType, flags); } synchronized (mLock) { /// M: update last event prevent status. @ { boolean lastPreventDefault = false; if (d.mEventType == EVENT_TYPE_TOUCH) { lastPreventDefault = mWebKitPreventTouchStream.update(event, preventDefault); } /// @ } flags = d.mFlags; d.mFlags = flags & ~FLAG_WEBKIT_IN_PROGRESS; boolean recycleEvent = event != d.mEvent; if ((flags & FLAG_WEBKIT_TIMEOUT) != 0) { // A timeout occurred! recycleDispatchEventLocked(d); } else { // Web kit finished in a timely manner. Dequeue the event. assert mWebKitDispatchEventQueue.mHead == d; mWebKitDispatchEventQueue.dequeue(); updateWebKitTimeoutLocked(); if ((flags & FLAG_PRIVATE) != 0) { // Event was intended for web kit only. All done. recycleDispatchEventLocked(d); } else if (preventDefault) { // Web kit has decided to consume the event! if (d.mEventType == EVENT_TYPE_TOUCH) { /// M: Consider two consistent events is preventDefault. @ { if (lastPreventDefault) { Log.d(TAG, "Webkit prevent current and last event, cancel ui event"); enqueueUiCancelTouchEventIfNeededLocked(); } else { Log.d(TAG, "Webkit prevent current but not last event, enqueue ui event"); /// Only one event is preventDefault, Ui also handle this touch event. enqueueUiEventUnbatchedLocked(d); } unscheduleLongPressLocked(); /// @ } } } else { // Web kit is being friendly. Pass the event to the UI. enqueueUiEventUnbatchedLocked(d); } } if (event != null && recycleEvent) { event.recycle(); } if (eventType == EVENT_TYPE_CLICK) { scheduleHideTapHighlightLocked(); } } } } // Runs on web kit thread. private boolean dispatchWebKitEvent(MotionEvent event, int eventType, int flags) { if (DEBUG) { Log.d( TAG, "dispatchWebKitEvent: event=" + event + ", eventType=" + eventType + ", flags=" + flags); } boolean preventDefault = mWebKitCallbacks.dispatchWebKitEvent(this, event, eventType, flags); if (DEBUG) { Log.d(TAG, "dispatchWebKitEvent: preventDefault=" + preventDefault); } return preventDefault; } private boolean isMoveEventLocked(DispatchEvent d) { return d.mEvent != null && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE; } private void drainStaleWebKitEventsLocked() { DispatchEvent d = mWebKitDispatchEventQueue.mHead; while (d != null && d.mNext != null && isMoveEventLocked(d) && isMoveEventLocked(d.mNext)) { DispatchEvent next = d.mNext; skipWebKitEventLocked(d); d = next; } mWebKitDispatchEventQueue.mHead = d; } // Called by WebKit when it doesn't care about the rest of the touch stream public void skipWebkitForRemainingTouchStream() { // Just treat this like a timeout handleWebKitTimeout(); } // Runs on UI thread in response to the web kit thread appearing to be unresponsive. private void handleWebKitTimeout() { synchronized (mLock) { if (!mWebKitTimeoutScheduled) { return; } mWebKitTimeoutScheduled = false; if (DEBUG) { Log.d(TAG, "handleWebKitTimeout: timeout occurred!"); } // Drain the web kit event queue. DispatchEvent d = mWebKitDispatchEventQueue.dequeueList(); // If web kit was processing an event (must be at the head of the list because // it can only do one at a time), then clone it or ignore it. if ((d.mFlags & FLAG_WEBKIT_IN_PROGRESS) != 0) { d.mFlags |= FLAG_WEBKIT_TIMEOUT; if ((d.mFlags & FLAG_PRIVATE) != 0) { d = d.mNext; // the event is private to web kit, ignore it } else { d = copyDispatchEventLocked(d); d.mFlags &= ~FLAG_WEBKIT_IN_PROGRESS; } } // Enqueue all non-private events for handling by the UI thread. while (d != null) { DispatchEvent next = d.mNext; skipWebKitEventLocked(d); d = next; } // Tell web kit to cancel all pending touches. // This also prevents us from sending web kit any more touches until the // next gesture begins. (As required to ensure touch event stream consistency.) enqueueWebKitCancelTouchEventIfNeededLocked(); } } private void skipWebKitEventLocked(DispatchEvent d) { d.mNext = null; if ((d.mFlags & FLAG_PRIVATE) != 0) { recycleDispatchEventLocked(d); } else { d.mFlags |= FLAG_WEBKIT_TIMEOUT; enqueueUiEventUnbatchedLocked(d); } } /** * Dispatches pending UI events. Must only be called from the UI thread. * * <p>This method may be used to flush the queue of pending input events immediately. This method * may help to reduce input dispatch latency if called before certain expensive operations such as * drawing. */ public void dispatchUiEvents() { dispatchUiEvents(false); } private void dispatchUiEvents(boolean calledFromHandler) { for (; ; ) { MotionEvent event; final int eventType; final int flags; synchronized (mLock) { DispatchEvent d = mUiDispatchEventQueue.dequeue(); if (d == null) { if (mUiDispatchScheduled) { mUiDispatchScheduled = false; if (!calledFromHandler) { mUiHandler.removeMessages(UiHandler.MSG_DISPATCH_UI_EVENTS); } } return; } event = d.mEvent; if (event != null && (d.mFlags & FLAG_WEBKIT_TRANSFORMED_EVENT) != 0) { event.scale(1.0f / d.mWebKitScale); event.offsetLocation(-d.mWebKitXOffset, -d.mWebKitYOffset); d.mFlags &= ~FLAG_WEBKIT_TRANSFORMED_EVENT; } eventType = d.mEventType; if (eventType == EVENT_TYPE_TOUCH) { event = mUiTouchStream.update(event); if (DEBUG && event == null && d.mEvent != null) { Log.d(TAG, "dispatchUiEvents: dropped event " + d.mEvent); } } flags = d.mFlags; if (event == d.mEvent) { d.mEvent = null; // retain ownership of event, don't recycle it yet } recycleDispatchEventLocked(d); if (eventType == EVENT_TYPE_CLICK) { scheduleHideTapHighlightLocked(); } } // Handle the event. if (event != null) { dispatchUiEvent(event, eventType, flags); event.recycle(); } } } // Runs on UI thread. private void dispatchUiEvent(MotionEvent event, int eventType, int flags) { if (DEBUG) { Log.d( TAG, "dispatchUiEvent: event=" + event + ", eventType=" + eventType + ", flags=" + flags); } mUiCallbacks.dispatchUiEvent(event, eventType, flags); } private void enqueueEventLocked(DispatchEvent d) { if (!shouldSkipWebKit(d)) { enqueueWebKitEventLocked(d); } else { enqueueUiEventLocked(d); } } private boolean shouldSkipWebKit(DispatchEvent d) { switch (d.mEventType) { case EVENT_TYPE_CLICK: case EVENT_TYPE_HOVER: case EVENT_TYPE_SCROLL: case EVENT_TYPE_HIT_TEST: return false; case EVENT_TYPE_TOUCH: // TODO: This should be cleaned up. We now have WebViewInputDispatcher // and WebViewClassic both checking for slop and doing their own // thing - they should be consolidated. And by consolidated, I mean // WebViewClassic's version should just be deleted. // The reason this is done is because webpages seem to expect // that they only get an ontouchmove if the slop has been exceeded. if (mIsTapCandidate && d.mEvent != null && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE) { return true; } return !mPostSendTouchEventsToWebKit || mPostDoNotSendTouchEventsToWebKitUntilNextGesture; } return true; } private void enqueueWebKitCancelTouchEventIfNeededLocked() { // We want to cancel touch events that were delivered to web kit. // Enqueue a null event at the end of the queue if needed. if (mWebKitTouchStream.isCancelNeeded() || !mWebKitDispatchEventQueue.isEmpty()) { DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE, 0, 0, 1.0f); enqueueWebKitEventUnbatchedLocked(d); mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true; } } private void enqueueWebKitEventLocked(DispatchEvent d) { if (batchEventLocked(d, mWebKitDispatchEventQueue.mTail)) { if (DEBUG) { Log.d(TAG, "enqueueWebKitEventLocked: batched event " + d.mEvent); } recycleDispatchEventLocked(d); } else { enqueueWebKitEventUnbatchedLocked(d); } } private void enqueueWebKitEventUnbatchedLocked(DispatchEvent d) { if (DEBUG) { Log.d(TAG, "enqueueWebKitEventUnbatchedLocked: enqueued event " + d.mEvent); } mWebKitDispatchEventQueue.enqueue(d); scheduleWebKitDispatchLocked(); updateWebKitTimeoutLocked(); } private void scheduleWebKitDispatchLocked() { if (!mWebKitDispatchScheduled) { mWebKitHandler.sendEmptyMessage(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS); mWebKitDispatchScheduled = true; } } private void updateWebKitTimeoutLocked() { DispatchEvent d = mWebKitDispatchEventQueue.mHead; if (d != null && mWebKitTimeoutScheduled && mWebKitTimeoutTime == d.mTimeoutTime) { return; } if (mWebKitTimeoutScheduled) { mUiHandler.removeMessages(UiHandler.MSG_WEBKIT_TIMEOUT); mWebKitTimeoutScheduled = false; } if (d != null) { mUiHandler.sendEmptyMessageAtTime(UiHandler.MSG_WEBKIT_TIMEOUT, d.mTimeoutTime); mWebKitTimeoutScheduled = true; mWebKitTimeoutTime = d.mTimeoutTime; } } private void enqueueUiCancelTouchEventIfNeededLocked() { // We want to cancel touch events that were delivered to the UI. // Enqueue a null event at the end of the queue if needed. if (mUiTouchStream.isCancelNeeded() || !mUiDispatchEventQueue.isEmpty()) { DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE, 0, 0, 1.0f); enqueueUiEventUnbatchedLocked(d); } } private void enqueueUiEventLocked(DispatchEvent d) { if (batchEventLocked(d, mUiDispatchEventQueue.mTail)) { if (DEBUG) { Log.d(TAG, "enqueueUiEventLocked: batched event " + d.mEvent); } recycleDispatchEventLocked(d); } else { enqueueUiEventUnbatchedLocked(d); } } private void enqueueUiEventUnbatchedLocked(DispatchEvent d) { if (DEBUG) { Log.d(TAG, "enqueueUiEventUnbatchedLocked: enqueued event " + d.mEvent); } mUiDispatchEventQueue.enqueue(d); scheduleUiDispatchLocked(); } private void scheduleUiDispatchLocked() { if (!mUiDispatchScheduled) { mUiHandler.sendEmptyMessage(UiHandler.MSG_DISPATCH_UI_EVENTS); mUiDispatchScheduled = true; } } private boolean batchEventLocked(DispatchEvent in, DispatchEvent tail) { if (!ENABLE_EVENT_BATCHING) { return false; } if (tail != null && tail.mEvent != null && in.mEvent != null && in.mEventType == tail.mEventType && in.mFlags == tail.mFlags && in.mWebKitXOffset == tail.mWebKitXOffset && in.mWebKitYOffset == tail.mWebKitYOffset && in.mWebKitScale == tail.mWebKitScale) { return tail.mEvent.addBatch(in.mEvent); } return false; } private DispatchEvent obtainDispatchEventLocked( MotionEvent event, int eventType, int flags, int webKitXOffset, int webKitYOffset, float webKitScale) { DispatchEvent d = obtainUninitializedDispatchEventLocked(); d.mEvent = event; d.mEventType = eventType; d.mFlags = flags; d.mTimeoutTime = SystemClock.uptimeMillis() + WEBKIT_TIMEOUT_MILLIS; d.mWebKitXOffset = webKitXOffset; d.mWebKitYOffset = webKitYOffset; d.mWebKitScale = webKitScale; if (DEBUG) { Log.d(TAG, "Timeout time: " + (d.mTimeoutTime - SystemClock.uptimeMillis())); } return d; } private DispatchEvent copyDispatchEventLocked(DispatchEvent d) { DispatchEvent copy = obtainUninitializedDispatchEventLocked(); if (d.mEvent != null) { copy.mEvent = d.mEvent.copy(); } copy.mEventType = d.mEventType; copy.mFlags = d.mFlags; copy.mTimeoutTime = d.mTimeoutTime; copy.mWebKitXOffset = d.mWebKitXOffset; copy.mWebKitYOffset = d.mWebKitYOffset; copy.mWebKitScale = d.mWebKitScale; copy.mNext = d.mNext; return copy; } private DispatchEvent obtainUninitializedDispatchEventLocked() { DispatchEvent d = mDispatchEventPool; if (d != null) { mDispatchEventPoolSize -= 1; mDispatchEventPool = d.mNext; d.mNext = null; } else { d = new DispatchEvent(); } return d; } private void recycleDispatchEventLocked(DispatchEvent d) { if (d.mEvent != null) { d.mEvent.recycle(); d.mEvent = null; } if (mDispatchEventPoolSize < MAX_DISPATCH_EVENT_POOL_SIZE) { mDispatchEventPoolSize += 1; d.mNext = mDispatchEventPool; mDispatchEventPool = d; } } /* Implemented by {@link WebViewClassic} to perform operations on the UI thread. */ public static interface UiCallbacks { /** * Gets the UI thread's looper. * * @return The looper. */ public Looper getUiLooper(); /** * Gets the UI's context * * @return The context */ public Context getContext(); /** * Dispatches an event to the UI. * * @param event The event. * @param eventType The event type. * @param flags The event's dispatch flags. */ public void dispatchUiEvent(MotionEvent event, int eventType, int flags); /** * Asks the UI thread whether this touch event stream should be intercepted based on the touch * down event. * * @param event The touch down event. * @return true if the UI stream wants the touch stream without going through webkit or false * otherwise. */ public boolean shouldInterceptTouchEvent(MotionEvent event); /** * Inform's the UI that it should show the tap highlight * * @param show True if it should show the highlight, false if it should hide it */ public void showTapHighlight(boolean show); /** * Called when we are sending a new EVENT_TYPE_HIT_TEST to WebKit, so previous hit tests should * be cleared as they are obsolete. */ public void clearPreviousHitTest(); } /* Implemented by {@link WebViewCore} to perform operations on the web kit thread. */ public static interface WebKitCallbacks { /** * Gets the web kit thread's looper. * * @return The looper. */ public Looper getWebKitLooper(); /** * Dispatches an event to web kit. * * @param dispatcher The WebViewInputDispatcher sending the event * @param event The event. * @param eventType The event type. * @param flags The event's dispatch flags. * @return True if web kit wants to prevent default event handling. */ public boolean dispatchWebKitEvent( WebViewInputDispatcher dispatcher, MotionEvent event, int eventType, int flags); } // Runs on UI thread. private final class UiHandler extends Handler { public static final int MSG_DISPATCH_UI_EVENTS = 1; public static final int MSG_WEBKIT_TIMEOUT = 2; public static final int MSG_LONG_PRESS = 3; public static final int MSG_CLICK = 4; public static final int MSG_SHOW_TAP_HIGHLIGHT = 5; public static final int MSG_HIDE_TAP_HIGHLIGHT = 6; public UiHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DISPATCH_UI_EVENTS: dispatchUiEvents(true); break; case MSG_WEBKIT_TIMEOUT: handleWebKitTimeout(); break; case MSG_LONG_PRESS: postLongPress(); break; case MSG_CLICK: postClick(); break; case MSG_SHOW_TAP_HIGHLIGHT: postShowTapHighlight(true); break; case MSG_HIDE_TAP_HIGHLIGHT: postShowTapHighlight(false); break; default: throw new IllegalStateException("Unknown message type: " + msg.what); } } } // Runs on web kit thread. private final class WebKitHandler extends Handler { public static final int MSG_DISPATCH_WEBKIT_EVENTS = 1; public WebKitHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DISPATCH_WEBKIT_EVENTS: dispatchWebKitEvents(true); break; default: throw new IllegalStateException("Unknown message type: " + msg.what); } } } private static final class DispatchEvent { public DispatchEvent mNext; public MotionEvent mEvent; public int mEventType; public int mFlags; public long mTimeoutTime; public int mWebKitXOffset; public int mWebKitYOffset; public float mWebKitScale; } private static final class DispatchEventQueue { public DispatchEvent mHead; public DispatchEvent mTail; public boolean isEmpty() { return mHead != null; } public void enqueue(DispatchEvent d) { if (mHead == null) { mHead = d; mTail = d; } else { mTail.mNext = d; mTail = d; } } public DispatchEvent dequeue() { DispatchEvent d = mHead; if (d != null) { DispatchEvent next = d.mNext; if (next == null) { mHead = null; mTail = null; } else { mHead = next; d.mNext = null; } } return d; } public DispatchEvent dequeueList() { DispatchEvent d = mHead; if (d != null) { mHead = null; mTail = null; } return d; } } /** * Keeps track of a stream of touch events so that we can discard touch events that would make the * stream inconsistent. */ private static final class TouchStream { private MotionEvent mLastEvent; /** * Gets the last touch event that was delivered. * * @return The last touch event, or null if none. */ public MotionEvent getLastEvent() { return mLastEvent; } /** * Updates the touch event stream. * * @param event The event that we intend to send, or null to cancel the touch event stream. * @return The event that we should actually send, or null if no event should be sent because * the proposed event would make the stream inconsistent. */ public MotionEvent update(MotionEvent event) { if (event == null) { if (isCancelNeeded()) { event = mLastEvent; if (event != null) { event.setAction(MotionEvent.ACTION_CANCEL); mLastEvent = null; } } return event; } switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: if (mLastEvent == null || mLastEvent.getAction() == MotionEvent.ACTION_UP) { return null; } updateLastEvent(event); return event; case MotionEvent.ACTION_DOWN: updateLastEvent(event); return event; case MotionEvent.ACTION_CANCEL: if (mLastEvent == null) { return null; } updateLastEvent(null); return event; default: return null; } } /** * Returns true if there is a gesture in progress that may need to be canceled. * * @return True if cancel is needed. */ public boolean isCancelNeeded() { return mLastEvent != null && mLastEvent.getAction() != MotionEvent.ACTION_UP; } private void updateLastEvent(MotionEvent event) { if (mLastEvent != null) { mLastEvent.recycle(); } mLastEvent = event != null ? MotionEvent.obtainNoHistory(event) : null; } } /** * M: Keeps track of a stream of touch events so that we can prevent Ui to handle events that * previous event is also prevent. */ private static final class PreventTouchStream { private boolean mLastPreventDefault; public boolean getLastPreventDefault() { return mLastPreventDefault; } /** * Update the prevent touch stream. * * @param event The event that webkit had handled, or null to drop it. * @param preventDefault Prevent Ui to handle this event or not. * @return true If last event if prevented or false if not.. */ public boolean update(MotionEvent event, boolean preventDefault) { // Don't prevent null event. if (event == null) { Log.e(TAG, "PreventTouchStream has null event."); return false; } if (DEBUG) { Log.d(TAG, "event action=" + event.getActionMasked() + " preventDefault=" + preventDefault); } boolean prevent = mLastPreventDefault; mLastPreventDefault = preventDefault; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: return prevent; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mLastPreventDefault = false; return prevent; default: return preventDefault; } } } }
@Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); boolean handled = false; switch (action) { case MotionEvent.ACTION_DOWN: { View v = getCurrentView(); if (v != null) { if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mTouchMode = TOUCH_MODE_DOWN_IN_CURRENT_VIEW; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } } break; } case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_POINTER_UP: break; case MotionEvent.ACTION_UP: { if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) { final View v = getCurrentView(); final ViewAndMetaData viewData = getMetaDataForChild(v); if (v != null) { if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForTap); } showTapFeedback(v); postDelayed(new Runnable() { public void run() { hideTapFeedback(v); post(new Runnable() { public void run() { if (viewData != null) { performItemClick(v, viewData.adapterPosition, viewData.itemId); } else { performItemClick(v, 0, 0); } } }); } }, ViewConfiguration.getPressedStateDuration()); handled = true; } } } mTouchMode = TOUCH_MODE_NONE; break; } case MotionEvent.ACTION_CANCEL: { View v = getCurrentView(); if (v != null) { hideTapFeedback(v); } mTouchMode = TOUCH_MODE_NONE; } } return handled; }