Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1fdf6034 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Expand the animation from the user's last touch point"

parents 949db7f2 91df3f9e
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -260,6 +260,7 @@ public class Editor {
    private PositionListener mPositionListener;

    private float mLastDownPositionX, mLastDownPositionY;
    private float mLastUpPositionX, mLastUpPositionY;
    private float mContextMenuAnchorX, mContextMenuAnchorY;
    Callback mCustomSelectionActionModeCallback;
    Callback mCustomInsertionActionModeCallback;
@@ -1130,6 +1131,14 @@ public class Editor {
        return handled;
    }

    float getLastUpPositionX() {
        return mLastUpPositionX;
    }

    float getLastUpPositionY() {
        return mLastUpPositionY;
    }

    private long getLastTouchOffsets() {
        SelectionModifierCursorController selectionController = getSelectionController();
        final int minOffset = selectionController.getMinTouchOffset();
@@ -1371,6 +1380,11 @@ public class Editor {
            mShowSuggestionRunnable = null;
        }

        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
            mLastUpPositionX = event.getX();
            mLastUpPositionY = event.getY();
        }

        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mLastDownPositionX = event.getX();
            mLastDownPositionY = event.getY();
+43 −7
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.AsyncTask;
import android.os.LocaleList;
@@ -27,13 +28,13 @@ import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextUtils;
import android.util.Pair;
import android.view.ActionMode;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextSelection;
import android.widget.Editor.SelectionModifierCursorController;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
@@ -45,8 +46,10 @@ import java.util.function.Supplier;
/**
 * Helper class for starting selection action mode
 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
 * @hide
 */
@UiThread
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
final class SelectionActionModeHelper {

    /**
@@ -224,15 +227,15 @@ final class SelectionActionModeHelper {
            rectangle.bottom += textView.getPaddingTop();
        }

        final RectF firstRectangle = selectionRectangles.get(0);
        final PointF touchPoint = new PointF(
                mEditor.getLastUpPositionX(),
                mEditor.getLastUpPositionY());

        // TODO use the original touch point instead of the hardcoded point generated here
        final Pair<Float, Float> halfPoint = new Pair<>(
                firstRectangle.centerX(),
                firstRectangle.centerY());
        final PointF animationStartPoint =
                movePointInsideNearestRectangle(touchPoint, selectionRectangles);

        mSmartSelectSprite.startAnimation(
                halfPoint,
                animationStartPoint,
                selectionRectangles,
                onAnimationEndCallback);
    }
@@ -248,6 +251,39 @@ final class SelectionActionModeHelper {
        return result;
    }

    /** @hide */
    @VisibleForTesting
    public static PointF movePointInsideNearestRectangle(final PointF point,
            final List<RectF> rectangles) {
        float bestX = -1;
        float bestY = -1;
        double bestDistance = Double.MAX_VALUE;

        for (final RectF rectangle : rectangles) {
            final float candidateY = rectangle.centerY();
            final float candidateX;

            if (point.x > rectangle.right) {
                candidateX = rectangle.right;
            } else if (point.x < rectangle.left) {
                candidateX = rectangle.left;
            } else {
                candidateX = point.x;
            }

            final double candidateDistance = Math.pow(point.x - candidateX, 2)
                    + Math.pow(point.y - candidateY, 2);

            if (candidateDistance < bestDistance) {
                bestX = candidateX;
                bestY = candidateY;
                bestDistance = candidateDistance;
            }
        }

        return new PointF(bestX, bestY);
    }

    private void invalidateActionMode(@Nullable SelectionResult result) {
        cancelSmartSelectAnimation();
        mTextClassification = result != null ? result.mClassification : null;
+35 −20
Original line number Diff line number Diff line
@@ -30,11 +30,11 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
import android.util.Pair;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewOverlay;
@@ -82,16 +82,16 @@ final class SmartSelectSprite {

        private final float[] mLineCoordinates;

        private PolygonShape(final List<Pair<Float, Float>> points) {
        private PolygonShape(final List<PointF> points) {
            mLineCoordinates = new float[points.size() * POINTS_PER_LINE];

            int index = 0;
            Pair<Float, Float> currentPoint = points.get(0);
            for (final Pair<Float, Float> nextPoint : points) {
                mLineCoordinates[index] = currentPoint.first;
                mLineCoordinates[index + 1] = currentPoint.second;
                mLineCoordinates[index + 2] = nextPoint.first;
                mLineCoordinates[index + 3] = nextPoint.second;
            PointF currentPoint = points.get(0);
            for (final PointF nextPoint : points) {
                mLineCoordinates[index] = currentPoint.x;
                mLineCoordinates[index + 1] = currentPoint.y;
                mLineCoordinates[index + 2] = nextPoint.x;
                mLineCoordinates[index + 3] = nextPoint.y;

                index += POINTS_PER_LINE;
                currentPoint = nextPoint;
@@ -342,9 +342,9 @@ final class SmartSelectSprite {
            final List<RectF> rectangles,
            final int color) {
        final List<Drawable> drawables = new LinkedList<>();
        final Set<List<Pair<Float, Float>>> mergedPaths = calculateMergedPolygonPoints(rectangles);
        final Set<List<PointF>> mergedPaths = calculateMergedPolygonPoints(rectangles);

        for (List<Pair<Float, Float>> path : mergedPaths) {
        for (List<PointF> path : mergedPaths) {
            // Add the starting point to the end of the polygon so that it ends up closed.
            path.add(path.get(0));

@@ -361,7 +361,7 @@ final class SmartSelectSprite {
        return drawables;
    }

    private static Set<List<Pair<Float, Float>>> calculateMergedPolygonPoints(
    private static Set<List<PointF>> calculateMergedPolygonPoints(
            List<RectF> rectangles) {
        final Set<List<RectF>> partitions = new HashSet<>();
        final LinkedList<RectF> listOfRects = new LinkedList<>(rectangles);
@@ -389,20 +389,20 @@ final class SmartSelectSprite {
            partitions.add(partition);
        }

        final Set<List<Pair<Float, Float>>> result = new HashSet<>();
        final Set<List<PointF>> result = new HashSet<>();
        for (List<RectF> partition : partitions) {
            final List<Pair<Float, Float>> points = new LinkedList<>();
            final List<PointF> points = new LinkedList<>();

            final Stack<RectF> rects = new Stack<>();
            for (RectF rect : partition) {
                points.add(new Pair<>(rect.right, rect.top));
                points.add(new Pair<>(rect.right, rect.bottom));
                points.add(new PointF(rect.right, rect.top));
                points.add(new PointF(rect.right, rect.bottom));
                rects.add(rect);
            }
            while (!rects.isEmpty()) {
                final RectF rect = rects.pop();
                points.add(new Pair<>(rect.left, rect.bottom));
                points.add(new Pair<>(rect.left, rect.top));
                points.add(new PointF(rect.left, rect.bottom));
                points.add(new PointF(rect.left, rect.top));
            }

            result.add(points);
@@ -426,7 +426,7 @@ final class SmartSelectSprite {
     * @see #cancelAnimation()
     */
    public void startAnimation(
            final Pair<Float, Float> start,
            final PointF start,
            final List<RectF> destinationRectangles,
            final Runnable onAnimationEnd) throws IllegalArgumentException {
        cancelAnimation();
@@ -439,7 +439,7 @@ final class SmartSelectSprite {

        final RectF centerRectangle = destinationRectangles
                .stream()
                .filter((r) -> r.contains(start.first, start.second))
                .filter((r) -> contains(r, start))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(
                        "Center point is not inside any of the rectangles!"));
@@ -452,7 +452,7 @@ final class SmartSelectSprite {
            startingOffset += rectangle.width();
        }

        startingOffset += start.first - centerRectangle.left;
        startingOffset += start.x - centerRectangle.left;

        final float centerRectangleHalfHeight = centerRectangle.height() / 2;
        final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
@@ -632,6 +632,21 @@ final class SmartSelectSprite {
        return result;
    }

    /**
     * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
     * the right boundary of the rectangle.
     *
     * @param rectangle the rectangle inside which the point should be to be considered "contained"
     * @param point     the point which will be tested
     * @return whether the point is inside the rectangle (or on it's right boundary)
     */
    private static boolean contains(final RectF rectangle, final PointF point) {
        final float x = point.x;
        final float y = point.y;
        return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top
                && y <= rectangle.bottom;
    }

    private void addToOverlay(final Drawable drawable) {
        mView.getOverlay().add(drawable);
        mExistingAnimationDrawables.add(drawable);
+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import static org.junit.Assert.assertEquals;

import android.graphics.PointF;
import android.graphics.RectF;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;
import java.util.List;

@RunWith(JUnit4.class)
public final class SelectionActionModeHelperTest {

    /*
     * The test rectangle set is composed of three 1x1 rectangles as illustrated below.
     *
     * (0, 0) ____________ (100001, 0)
     *        |█        █|
     *        |_█________|
     * (0, 2)              (100001, 2)
     */
    private final List<RectF> mRectFList = Arrays.asList(
            new RectF(0, 0, 1, 1),
            new RectF(100000, 0, 100001, 1),
            new RectF(1, 1, 2, 2));

    @Test
    public void testMovePointInsideNearestRectangle_pointIsInsideRectangle() {
        testMovePointInsideNearestRectangle(
                0.1f /* pointX */,
                0.1f /* pointY */,
                0.1f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    @Test
    public void testMovePointInsideNearestRectangle_pointIsAboveRectangle() {
        testMovePointInsideNearestRectangle(
                0.1f /* pointX */,
                -1.0f /* pointY */,
                0.1f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    @Test
    public void testMovePointInsideNearestRectangle_pointIsLeftOfRectangle() {
        testMovePointInsideNearestRectangle(
                -1.0f /* pointX */,
                0.4f /* pointY */,
                0.0f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    @Test
    public void testMovePointInsideNearestRectangle_pointIsRightOfRectangle() {
        testMovePointInsideNearestRectangle(
                1.1f /* pointX */,
                0.0f /* pointY */,
                1.0f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    @Test
    public void testMovePointInsideNearestRectangle_pointIsBelowRectangle() {
        testMovePointInsideNearestRectangle(
                0.1f /* pointX */,
                1.1f /* pointY */,
                0.1f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    @Test
    public void testMovePointInsideNearestRectangle_pointIsToRightOfTheRightmostRectangle() {
        testMovePointInsideNearestRectangle(
                200000.0f /* pointX */,
                0.1f /* pointY */,
                100001.0f /* expectedPointX */,
                0.5f /* expectedPointY */);
    }

    private void testMovePointInsideNearestRectangle(final float pointX, final float pointY,
            final float expectedPointX,
            final float expectedPointY) {
        final PointF point = new PointF(pointX, pointY);
        final PointF adjustedPoint =
                SelectionActionModeHelper.movePointInsideNearestRectangle(point,
                        mRectFList);

        assertEquals(expectedPointX, adjustedPoint.x, 0.0f);
        assertEquals(expectedPointY, adjustedPoint.y, 0.0f);
    }

}