// Copyright 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.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import android.widget.PopupWindow; 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 final int mLineOffsetY; private float mDownPositionX, mDownPositionY; private long mTouchTimer; private boolean mIsInsertionHandle = false; private float mAlpha; private long mFadeStartTime; private final 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 final PositionObserver mParentPositionObserver; private final 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(); } } }