| // Copyright (c) 2012 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.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.SystemClock; |
| import android.util.TypedValue; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.view.WindowManager; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup.LayoutParams; |
| import android.widget.PopupWindow; |
| import android.widget.TextView; |
| |
| import org.chromium.content.browser.PositionObserver; |
| |
| /** |
| * 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. |
| * |
| */ |
| public class HandleView extends View { |
| private static final float FADE_DURATION = 200.f; |
| |
| private Drawable mDrawable; |
| private final PopupWindow mContainer; |
| |
| // 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 final CursorController mController; |
| private boolean mIsDragging; |
| private float mTouchToWindowOffsetX; |
| private float mTouchToWindowOffsetY; |
| |
| private int mLineOffsetY; |
| private float mDownPositionX, mDownPositionY; |
| private long mTouchTimer; |
| private boolean mIsInsertionHandle = false; |
| private float mAlpha; |
| private long mFadeStartTime; |
| |
| private View mParent; |
| private InsertionHandleController.PastePopupMenu mPastePopupWindow; |
| |
| private final int mTextSelectHandleLeftRes; |
| private final int mTextSelectHandleRightRes; |
| private final int mTextSelectHandleRes; |
| |
| private Drawable mSelectHandleLeft; |
| private Drawable mSelectHandleRight; |
| private Drawable mSelectHandleCenter; |
| |
| private final Rect mTempRect = new Rect(); |
| |
| static final int LEFT = 0; |
| static final int CENTER = 1; |
| static final int RIGHT = 2; |
| |
| private PositionObserver mParentPositionObserver; |
| private PositionObserver.Listener mParentPositionListener; |
| |
| // Number of dips to subtract from the handle's y position to give a suitable |
| // y coordinate for the corresponding text position. This is to compensate for the fact |
| // that the handle position is at the base of the line of text. |
| private static final float LINE_OFFSET_Y_DIP = 5.0f; |
| |
| private static final int[] TEXT_VIEW_HANDLE_ATTRS = { |
| android.R.attr.textSelectHandleLeft, |
| android.R.attr.textSelectHandle, |
| android.R.attr.textSelectHandleRight, |
| }; |
| |
| HandleView(CursorController controller, int pos, View parent, |
| PositionObserver parentPositionObserver) { |
| super(parent.getContext()); |
| Context context = parent.getContext(); |
| mParent = parent; |
| mController = controller; |
| mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); |
| mContainer.setSplitTouchEnabled(true); |
| mContainer.setClippingEnabled(false); |
| |
| TypedArray a = context.obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS); |
| mTextSelectHandleLeftRes = a.getResourceId(a.getIndex(LEFT), 0); |
| mTextSelectHandleRes = a.getResourceId(a.getIndex(CENTER), 0); |
| mTextSelectHandleRightRes = a.getResourceId(a.getIndex(RIGHT), 0); |
| a.recycle(); |
| |
| setOrientation(pos); |
| |
| // Convert line offset dips to pixels. |
| mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics()); |
| |
| mAlpha = 1.f; |
| |
| mParentPositionListener = new PositionObserver.Listener() { |
| @Override |
| public void onPositionChanged(int x, int y) { |
| updateParentPosition(x, y); |
| } |
| }; |
| mParentPositionObserver = parentPositionObserver; |
| } |
| |
| void setOrientation(int pos) { |
| int handleWidth; |
| switch (pos) { |
| case LEFT: { |
| if (mSelectHandleLeft == null) { |
| mSelectHandleLeft = getContext().getResources().getDrawable( |
| mTextSelectHandleLeftRes); |
| } |
| mDrawable = mSelectHandleLeft; |
| handleWidth = mDrawable.getIntrinsicWidth(); |
| mHotspotX = (handleWidth * 3) / 4f; |
| break; |
| } |
| |
| case RIGHT: { |
| if (mSelectHandleRight == null) { |
| mSelectHandleRight = getContext().getResources().getDrawable( |
| mTextSelectHandleRightRes); |
| } |
| mDrawable = mSelectHandleRight; |
| handleWidth = mDrawable.getIntrinsicWidth(); |
| mHotspotX = handleWidth / 4f; |
| break; |
| } |
| |
| case CENTER: |
| default: { |
| if (mSelectHandleCenter == null) { |
| mSelectHandleCenter = getContext().getResources().getDrawable( |
| mTextSelectHandleRes); |
| } |
| mDrawable = mSelectHandleCenter; |
| handleWidth = mDrawable.getIntrinsicWidth(); |
| mHotspotX = handleWidth / 2f; |
| mIsInsertionHandle = true; |
| break; |
| } |
| } |
| |
| mHotspotY = 0; |
| invalidate(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| setMeasuredDimension(mDrawable.getIntrinsicWidth(), |
| mDrawable.getIntrinsicHeight()); |
| } |
| |
| private void updateParentPosition(int parentPositionX, int parentPositionY) { |
| // Hide paste popup window as soon as a scroll occurs. |
| if (mPastePopupWindow != null) mPastePopupWindow.hide(); |
| |
| mTouchToWindowOffsetX += parentPositionX - mParentPositionX; |
| mTouchToWindowOffsetY += parentPositionY - mParentPositionY; |
| mParentPositionX = parentPositionX; |
| mParentPositionY = parentPositionY; |
| onPositionChanged(); |
| } |
| |
| private int getContainerPositionX() { |
| return mParentPositionX + mPositionX; |
| } |
| |
| private int getContainerPositionY() { |
| return mParentPositionY + mPositionY; |
| } |
| |
| private void onPositionChanged() { |
| mContainer.update(getContainerPositionX(), getContainerPositionY(), |
| getRight() - getLeft(), getBottom() - getTop()); |
| } |
| |
| private void showContainer() { |
| mContainer.showAtLocation(mParent, 0, getContainerPositionX(), getContainerPositionY()); |
| } |
| |
| void show() { |
| // While hidden, the parent position may have become stale. It must be updated before |
| // checking isPositionVisible(). |
| updateParentPosition(mParentPositionObserver.getPositionX(), |
| mParentPositionObserver.getPositionY()); |
| if (!isPositionVisible()) { |
| hide(); |
| return; |
| } |
| mParentPositionObserver.addListener(mParentPositionListener); |
| mContainer.setContentView(this); |
| showContainer(); |
| |
| // Hide paste view when handle is moved on screen. |
| if (mPastePopupWindow != null) { |
| mPastePopupWindow.hide(); |
| } |
| } |
| |
| void hide() { |
| mIsDragging = false; |
| mContainer.dismiss(); |
| mParentPositionObserver.removeListener(mParentPositionListener); |
| if (mPastePopupWindow != null) { |
| mPastePopupWindow.hide(); |
| } |
| } |
| |
| boolean isShowing() { |
| return mContainer.isShowing(); |
| } |
| |
| private boolean isPositionVisible() { |
| // Always show a dragging handle. |
| if (mIsDragging) { |
| return true; |
| } |
| |
| final Rect clip = mTempRect; |
| clip.left = 0; |
| clip.top = 0; |
| clip.right = mParent.getWidth(); |
| clip.bottom = mParent.getHeight(); |
| |
| final ViewParent parent = mParent.getParent(); |
| if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) { |
| return false; |
| } |
| |
| final int posX = getContainerPositionX() + (int) mHotspotX; |
| final int posY = getContainerPositionY() + (int) mHotspotY; |
| |
| return posX >= clip.left && posX <= clip.right && |
| posY >= clip.top && posY <= clip.bottom; |
| } |
| |
| // x and y are in physical pixels. |
| void moveTo(int x, int y) { |
| int previousPositionX = mPositionX; |
| int previousPositionY = mPositionY; |
| |
| mPositionX = x; |
| mPositionY = y; |
| if (isPositionVisible()) { |
| if (mContainer.isShowing()) { |
| onPositionChanged(); |
| // Hide paste popup window as soon as the handle is dragged. |
| if (mPastePopupWindow != null && |
| (previousPositionX != mPositionX || previousPositionY != mPositionY)) { |
| mPastePopupWindow.hide(); |
| } |
| } else { |
| show(); |
| } |
| |
| if (mIsDragging) { |
| // Hide paste popup window as soon as the handle is dragged. |
| if (mPastePopupWindow != null) { |
| mPastePopupWindow.hide(); |
| } |
| } |
| } else { |
| hide(); |
| } |
| } |
| |
| @Override |
| protected void onDraw(Canvas c) { |
| updateAlpha(); |
| mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); |
| mDrawable.draw(c); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| switch (ev.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: { |
| mDownPositionX = ev.getRawX(); |
| mDownPositionY = ev.getRawY(); |
| mTouchToWindowOffsetX = mDownPositionX - mPositionX; |
| mTouchToWindowOffsetY = mDownPositionY - mPositionY; |
| mIsDragging = true; |
| mController.beforeStartUpdatingPosition(this); |
| mTouchTimer = SystemClock.uptimeMillis(); |
| break; |
| } |
| |
| case MotionEvent.ACTION_MOVE: { |
| updatePosition(ev.getRawX(), ev.getRawY()); |
| break; |
| } |
| |
| case MotionEvent.ACTION_UP: |
| if (mIsInsertionHandle) { |
| long delay = SystemClock.uptimeMillis() - mTouchTimer; |
| if (delay < ViewConfiguration.getTapTimeout()) { |
| if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { |
| // Tapping on the handle dismisses the displayed paste view, |
| mPastePopupWindow.hide(); |
| } else { |
| showPastePopupWindow(); |
| } |
| } |
| } |
| mIsDragging = false; |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| mIsDragging = false; |
| break; |
| |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| boolean isDragging() { |
| return mIsDragging; |
| } |
| |
| /** |
| * @return Returns the x position of the handle |
| */ |
| int getPositionX() { |
| return mPositionX; |
| } |
| |
| /** |
| * @return Returns the y position of the handle |
| */ |
| int getPositionY() { |
| return mPositionY; |
| } |
| |
| private void updatePosition(float rawX, float rawY) { |
| final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; |
| final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY - mLineOffsetY; |
| |
| mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); |
| } |
| |
| // x and y are in physical pixels. |
| void positionAt(int x, int y) { |
| moveTo(x - Math.round(mHotspotX), y - Math.round(mHotspotY)); |
| } |
| |
| // Returns the x coordinate of the position that the handle appears to be pointing to relative |
| // to the handles "parent" view. |
| 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. |
| int getAdjustedPositionY() { |
| return mPositionY + Math.round(mHotspotY); |
| } |
| |
| // Returns the x coordinate of the postion that the handle appears to be pointing to relative to |
| // the root view of the application. |
| int getRootViewRelativePositionX() { |
| return getContainerPositionX() + Math.round(mHotspotX); |
| } |
| |
| // Returns the y coordinate of the postion that the handle appears to be pointing to relative to |
| // the root view of the application. |
| int getRootViewRelativePositionY() { |
| return getContainerPositionY() + Math.round(mHotspotY); |
| } |
| |
| // Returns a suitable y coordinate for the text position corresponding to the handle. |
| // As the handle points to a position on the base of the line of text, this method |
| // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number) |
| // than getAdjustedPositionY. |
| int getLineAdjustedPositionY() { |
| return (int) (mPositionY + mHotspotY - mLineOffsetY); |
| } |
| |
| Drawable getDrawable() { |
| return mDrawable; |
| } |
| |
| private void updateAlpha() { |
| if (mAlpha == 1.f) return; |
| mAlpha = Math.min(1.f, (System.currentTimeMillis() - mFadeStartTime) / FADE_DURATION); |
| mDrawable.setAlpha((int) (255 * mAlpha)); |
| invalidate(); |
| } |
| |
| /** |
| * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in. |
| */ |
| void beginFadeIn() { |
| if (getVisibility() == VISIBLE) return; |
| mAlpha = 0.f; |
| mFadeStartTime = System.currentTimeMillis(); |
| setVisibility(VISIBLE); |
| } |
| |
| void showPastePopupWindow() { |
| InsertionHandleController ihc = (InsertionHandleController) mController; |
| if (mIsInsertionHandle && ihc.canPaste()) { |
| if (mPastePopupWindow == null) { |
| // Lazy initialization: create when actually shown only. |
| mPastePopupWindow = ihc.new PastePopupMenu(); |
| } |
| mPastePopupWindow.show(); |
| } |
| } |
| } |