| // Copyright 2014 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.content.browser.input; |
| |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.Drawable; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.animation.AnimationUtils; |
| import android.widget.PopupWindow; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| import org.chromium.content.browser.PositionObserver; |
| |
| import java.lang.ref.WeakReference; |
| |
| /** |
| * View that displays a selection or insertion handle for text editing. |
| * |
| * While a HandleView is logically a child of some other view, it does not exist in that View's |
| * hierarchy. |
| * |
| */ |
| @JNINamespace("content") |
| public class PopupTouchHandleDrawable extends View { |
| private Drawable mDrawable; |
| private final PopupWindow mContainer; |
| private final Context mContext; |
| private final PositionObserver.Listener mParentPositionListener; |
| |
| // The weak delegate reference allows the PopupTouchHandleDrawable to be owned by a native |
| // object that might have a different lifetime (or a cyclic lifetime) with respect to the |
| // delegate, allowing garbage collection of any Java references. |
| private final WeakReference<PopupTouchHandleDrawableDelegate> mDelegate; |
| |
| // The observer reference will only be non-null while it is attached to mParentPositionListener. |
| private PositionObserver mParentPositionObserver; |
| |
| // The position of the handle relative to the parent view. |
| private int mPositionX; |
| private int mPositionY; |
| |
| // The position of the parent relative to the application's root view. |
| private int mParentPositionX; |
| private int mParentPositionY; |
| |
| // The offset from this handles position to the "tip" of the handle. |
| private float mHotspotX; |
| private float mHotspotY; |
| |
| private float mAlpha; |
| |
| private final int[] mTempScreenCoords = new int[2]; |
| |
| static final int LEFT = 0; |
| static final int CENTER = 1; |
| static final int RIGHT = 2; |
| private int mOrientation = -1; |
| |
| // Length of the delay before fading in after the last page movement. |
| private static final int FADE_IN_DELAY_MS = 300; |
| private static final int FADE_IN_DURATION_MS = 200; |
| private Runnable mDeferredHandleFadeInRunnable; |
| private long mFadeStartTime; |
| private boolean mVisible; |
| private boolean mTemporarilyHidden; |
| |
| // Deferred runnable to avoid invalidating outside of frame dispatch, |
| // in turn avoiding issues with sync barrier insertion. |
| private Runnable mInvalidationRunnable; |
| private boolean mHasPendingInvalidate; |
| |
| /** |
| * Provides additional interaction behaviors necessary for handle |
| * manipulation and interaction. |
| */ |
| public interface PopupTouchHandleDrawableDelegate { |
| /** |
| * @return The parent View of the PopupWindow. |
| */ |
| View getParent(); |
| |
| /** |
| * @return A position observer for the parent View, used to keep the |
| * absolutely positioned PopupWindow in-sync with the parent. |
| */ |
| PositionObserver getParentPositionObserver(); |
| |
| /** |
| * Should route MotionEvents to the appropriate logic layer for |
| * performing handle manipulation. |
| */ |
| boolean onTouchHandleEvent(MotionEvent ev); |
| |
| /** |
| * @return Whether the associated content is actively scrolling. |
| */ |
| boolean isScrollInProgress(); |
| } |
| |
| public PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate) { |
| super(delegate.getParent().getContext()); |
| mContext = delegate.getParent().getContext(); |
| mDelegate = new WeakReference<PopupTouchHandleDrawableDelegate>(delegate); |
| mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle); |
| mContainer.setSplitTouchEnabled(true); |
| mContainer.setClippingEnabled(false); |
| mContainer.setAnimationStyle(0); |
| mAlpha = 1.f; |
| mVisible = getVisibility() == VISIBLE; |
| mParentPositionListener = new PositionObserver.Listener() { |
| @Override |
| public void onPositionChanged(int x, int y) { |
| updateParentPosition(x, y); |
| } |
| }; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); |
| if (delegate == null) { |
| // If the delegate is gone, we should immediately dispose of the popup. |
| hide(); |
| return false; |
| } |
| |
| // Convert from PopupWindow local coordinates to |
| // parent view local coordinates prior to forwarding. |
| delegate.getParent().getLocationOnScreen(mTempScreenCoords); |
| final float offsetX = event.getRawX() - event.getX() - mTempScreenCoords[0]; |
| final float offsetY = event.getRawY() - event.getY() - mTempScreenCoords[1]; |
| final MotionEvent offsetEvent = MotionEvent.obtainNoHistory(event); |
| offsetEvent.offsetLocation(offsetX, offsetY); |
| final boolean handled = delegate.onTouchHandleEvent(offsetEvent); |
| offsetEvent.recycle(); |
| return handled; |
| } |
| |
| private void setOrientation(int orientation) { |
| assert orientation >= LEFT && orientation <= RIGHT; |
| if (mOrientation == orientation) return; |
| |
| final boolean hadValidOrientation = mOrientation != -1; |
| mOrientation = orientation; |
| |
| final int oldAdjustedPositionX = getAdjustedPositionX(); |
| final int oldAdjustedPositionY = getAdjustedPositionY(); |
| |
| switch (orientation) { |
| case LEFT: { |
| mDrawable = HandleViewResources.getLeftHandleDrawable(mContext); |
| mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f; |
| break; |
| } |
| |
| case RIGHT: { |
| mDrawable = HandleViewResources.getRightHandleDrawable(mContext); |
| mHotspotX = mDrawable.getIntrinsicWidth() / 4f; |
| break; |
| } |
| |
| case CENTER: |
| default: { |
| mDrawable = HandleViewResources.getCenterHandleDrawable(mContext); |
| mHotspotX = mDrawable.getIntrinsicWidth() / 2f; |
| break; |
| } |
| } |
| mHotspotY = 0; |
| |
| // Force handle repositioning to accommodate the new orientation's hotspot. |
| if (hadValidOrientation) setFocus(oldAdjustedPositionX, oldAdjustedPositionY); |
| mDrawable.setAlpha((int) (255 * mAlpha)); |
| scheduleInvalidate(); |
| } |
| |
| private void updateParentPosition(int parentPositionX, int parentPositionY) { |
| if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return; |
| mParentPositionX = parentPositionX; |
| mParentPositionY = parentPositionY; |
| temporarilyHide(); |
| } |
| |
| private int getContainerPositionX() { |
| return mParentPositionX + mPositionX; |
| } |
| |
| private int getContainerPositionY() { |
| return mParentPositionY + mPositionY; |
| } |
| |
| private void updatePosition() { |
| mContainer.update(getContainerPositionX(), getContainerPositionY(), |
| getRight() - getLeft(), getBottom() - getTop()); |
| } |
| |
| private void updateVisibility() { |
| boolean visible = mVisible && !mTemporarilyHidden; |
| setVisibility(visible ? VISIBLE : INVISIBLE); |
| } |
| |
| private void updateAlpha() { |
| if (mAlpha == 1.f) return; |
| long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis(); |
| mAlpha = Math.min(1.f, (float) (currentTimeMillis - mFadeStartTime) / FADE_IN_DURATION_MS); |
| mDrawable.setAlpha((int) (255 * mAlpha)); |
| scheduleInvalidate(); |
| } |
| |
| private void temporarilyHide() { |
| mTemporarilyHidden = true; |
| updateVisibility(); |
| rescheduleFadeIn(); |
| } |
| |
| private void doInvalidate() { |
| updatePosition(); |
| updateVisibility(); |
| invalidate(); |
| } |
| |
| private void scheduleInvalidate() { |
| if (mInvalidationRunnable == null) { |
| mInvalidationRunnable = new Runnable() { |
| @Override |
| public void run() { |
| mHasPendingInvalidate = false; |
| doInvalidate(); |
| } |
| }; |
| } |
| |
| if (mHasPendingInvalidate) return; |
| mHasPendingInvalidate = true; |
| ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable); |
| } |
| |
| private void rescheduleFadeIn() { |
| if (mDeferredHandleFadeInRunnable == null) { |
| mDeferredHandleFadeInRunnable = new Runnable() { |
| @Override |
| public void run() { |
| if (isScrollInProgress()) { |
| rescheduleFadeIn(); |
| return; |
| } |
| mTemporarilyHidden = false; |
| beginFadeIn(); |
| } |
| }; |
| } |
| |
| removeCallbacks(mDeferredHandleFadeInRunnable); |
| ApiCompatibilityUtils.postOnAnimationDelayed( |
| this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS); |
| } |
| |
| private void beginFadeIn() { |
| if (getVisibility() == VISIBLE) return; |
| mAlpha = 0.f; |
| mFadeStartTime = AnimationUtils.currentAnimationTimeMillis(); |
| doInvalidate(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| if (mDrawable == null) { |
| setMeasuredDimension(0, 0); |
| return; |
| } |
| setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); |
| } |
| |
| @Override |
| protected void onDraw(Canvas c) { |
| if (mDrawable == null) return; |
| updateAlpha(); |
| mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); |
| mDrawable.draw(c); |
| } |
| |
| // Returns the x coordinate of the position that the handle appears to be pointing to relative |
| // to the handles "parent" view. |
| private int getAdjustedPositionX() { |
| return mPositionX + Math.round(mHotspotX); |
| } |
| |
| // Returns the y coordinate of the position that the handle appears to be pointing to relative |
| // to the handles "parent" view. |
| private int getAdjustedPositionY() { |
| return mPositionY + Math.round(mHotspotY); |
| } |
| |
| private boolean isScrollInProgress() { |
| final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); |
| if (delegate == null) { |
| hide(); |
| return false; |
| } |
| |
| return delegate.isScrollInProgress(); |
| } |
| |
| @CalledByNative |
| private void show() { |
| if (mContainer.isShowing()) return; |
| |
| final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); |
| if (delegate == null) { |
| hide(); |
| return; |
| } |
| |
| mParentPositionObserver = delegate.getParentPositionObserver(); |
| assert mParentPositionObserver != null; |
| |
| // While hidden, the parent position may have become stale. It must be updated before |
| // checking isPositionVisible(). |
| updateParentPosition(mParentPositionObserver.getPositionX(), |
| mParentPositionObserver.getPositionY()); |
| mParentPositionObserver.addListener(mParentPositionListener); |
| mContainer.setContentView(this); |
| mContainer.showAtLocation(delegate.getParent(), 0, |
| getContainerPositionX(), getContainerPositionY()); |
| } |
| |
| @CalledByNative |
| private void hide() { |
| mTemporarilyHidden = false; |
| mContainer.dismiss(); |
| if (mParentPositionObserver != null) { |
| mParentPositionObserver.removeListener(mParentPositionListener); |
| // Clear the strong reference to allow garbage collection. |
| mParentPositionObserver = null; |
| } |
| } |
| |
| @CalledByNative |
| private void setRightOrientation() { |
| setOrientation(RIGHT); |
| } |
| |
| @CalledByNative |
| private void setLeftOrientation() { |
| setOrientation(LEFT); |
| } |
| |
| @CalledByNative |
| private void setCenterOrientation() { |
| setOrientation(CENTER); |
| } |
| |
| @CalledByNative |
| private void setOpacity(float alpha) { |
| // Ignore opacity updates from the caller as they are not compatible |
| // with the custom fade animation. |
| } |
| |
| @CalledByNative |
| private void setFocus(float focusX, float focusY) { |
| int x = (int) focusX - Math.round(mHotspotX); |
| int y = (int) focusY - Math.round(mHotspotY); |
| if (mPositionX == x && mPositionY == y) return; |
| mPositionX = x; |
| mPositionY = y; |
| if (isScrollInProgress()) { |
| temporarilyHide(); |
| } else { |
| scheduleInvalidate(); |
| } |
| } |
| |
| @CalledByNative |
| private void setVisible(boolean visible) { |
| mVisible = visible; |
| int visibility = visible ? VISIBLE : INVISIBLE; |
| if (getVisibility() == visibility) return; |
| scheduleInvalidate(); |
| } |
| |
| @CalledByNative |
| private boolean intersectsWith(float x, float y, float width, float height) { |
| if (mDrawable == null) return false; |
| final int drawableWidth = mDrawable.getIntrinsicWidth(); |
| final int drawableHeight = mDrawable.getIntrinsicHeight(); |
| return !(x >= mPositionX + drawableWidth |
| || y >= mPositionY + drawableHeight |
| || x + width <= mPositionX |
| || y + height <= mPositionY); |
| } |
| } |