root/content/public/android/java/src/org/chromium/content/browser/input/HandleView.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. setOrientation
  2. onMeasure
  3. updateParentPosition
  4. getContainerPositionX
  5. getContainerPositionY
  6. onPositionChanged
  7. showContainer
  8. show
  9. hide
  10. isShowing
  11. isPositionVisible
  12. moveTo
  13. onDraw
  14. onTouchEvent
  15. isDragging
  16. getPositionX
  17. getPositionY
  18. updatePosition
  19. positionAt
  20. getAdjustedPositionX
  21. getAdjustedPositionY
  22. getRootViewRelativePositionX
  23. getRootViewRelativePositionY
  24. getLineAdjustedPositionY
  25. getDrawable
  26. updateAlpha
  27. beginFadeIn
  28. showPastePopupWindow

// 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.view.animation.AnimationUtils;
import android.widget.PopupWindow;

import com.google.common.annotations.VisibleForTesting;

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();
    }

    @VisibleForTesting
    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;

        boolean result = posX >= clip.left && posX <= clip.right &&
                posY >= clip.top && posY <= clip.bottom;

        final Rect clippingRect = mController.getVisibleClippingRectangle();
        if (result && clippingRect != null) {
            // We need to clip against the visible areas as supplied by Blink,
            // e.g. textaread and text input elements.
            return clippingRect.contains(getAdjustedPositionX(), getAdjustedPositionY());
        }

        return result;
    }

    // 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,
                (AnimationUtils.currentAnimationTimeMillis() - 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 = AnimationUtils.currentAnimationTimeMillis();
        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();
        }
    }
}

/* [<][>][^][v][top][bottom][index][help] */