Loading core/java/android/widget/SelectionActionModeHelper.java +42 −19 Original line number Diff line number Diff line Loading @@ -43,9 +43,11 @@ import com.android.internal.util.Preconditions; import java.text.BreakIterator; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; Loading Loading @@ -226,7 +228,7 @@ public final class SelectionActionModeHelper { return; } final List<RectF> selectionRectangles = final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles = convertSelectionToRectangles(layout, result.mStart, result.mEnd); final PointF touchPoint = new PointF( Loading @@ -234,7 +236,8 @@ public final class SelectionActionModeHelper { mEditor.getLastUpPositionY()); final PointF animationStartPoint = movePointInsideNearestRectangle(touchPoint, selectionRectangles); movePointInsideNearestRectangle(touchPoint, selectionRectangles, SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle); mSmartSelectSprite.startAnimation( animationStartPoint, Loading @@ -242,39 +245,58 @@ public final class SelectionActionModeHelper { onAnimationEndCallback); } private List<RectF> convertSelectionToRectangles(final Layout layout, final int start, final int end) { final List<RectF> result = new ArrayList<>(); layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(result, new RectF(left, top, right, bottom))); private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles( final Layout layout, final int start, final int end) { final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>(); final Layout.SelectionRectangleConsumer consumer = (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList( result, new RectF(left, top, right, bottom), SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r, textSelectionLayout) ); layout.getSelection(start, end, consumer); result.sort(Comparator.comparing( SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, SmartSelectSprite.RECTANGLE_COMPARATOR)); result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR); return result; } // TODO: Move public pure functions out of this class and make it package-private. /** * Merges a {@link RectF} into an existing list of rectangles. While merging, this method * makes sure that: * Merges a {@link RectF} into an existing list of any objects which contain a rectangle. * While merging, this method makes sure that: * * <ol> * <li>No rectangle is redundant (contained within a bigger rectangle)</li> * <li>Rectangles of the same height and vertical position that intersect get merged</li> * </ol> * * @param list the list of rectangles to merge the new rectangle in * @param list the list of rectangles (or other rectangle containers) to merge the new * rectangle into * @param candidate the {@link RectF} to merge into the list * @param extractor a function that can extract a {@link RectF} from an element of the given * list * @param packer a function that can wrap the resulting {@link RectF} into an element that * the list contains * @hide */ @VisibleForTesting public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) { public static <T> void mergeRectangleIntoList(final List<T> list, final RectF candidate, final Function<T, RectF> extractor, final Function<RectF, T> packer) { if (candidate.isEmpty()) { return; } final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { final RectF existingRectangle = list.get(index); final RectF existingRectangle = extractor.apply(list.get(index)); if (existingRectangle.contains(candidate)) { return; } Loading @@ -297,26 +319,27 @@ public final class SelectionActionModeHelper { } for (int index = elementCount - 1; index >= 0; --index) { if (list.get(index).isEmpty()) { final RectF rectangle = extractor.apply(list.get(index)); if (rectangle.isEmpty()) { list.remove(index); } } list.add(candidate); list.add(packer.apply(candidate)); } /** @hide */ @VisibleForTesting public static PointF movePointInsideNearestRectangle(final PointF point, final List<RectF> rectangles) { public static <T> PointF movePointInsideNearestRectangle(final PointF point, final List<T> list, final Function<T, RectF> extractor) { float bestX = -1; float bestY = -1; double bestDistance = Double.MAX_VALUE; final int elementCount = rectangles.size(); final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { final RectF rectangle = rectangles.get(index); final RectF rectangle = extractor.apply(list.get(index)); final float candidateY = rectangle.centerY(); final float candidateX; Loading core/java/android/widget/SmartSelectSprite.java +102 −40 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.Shape; import android.text.Layout; import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; Loading @@ -42,9 +43,9 @@ import android.view.animation.Interpolator; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; /** Loading Loading @@ -76,6 +77,26 @@ final class SmartSelectSprite { private Drawable mExistingDrawable = null; private RectangleList mExistingRectangleList = null; static final class RectangleWithTextSelectionLayout { private final RectF mRectangle; @Layout.TextSelectionLayout private final int mTextSelectionLayout; RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) { mRectangle = Preconditions.checkNotNull(rectangle); mTextSelectionLayout = textSelectionLayout; } public RectF getRectangle() { return mRectangle; } @Layout.TextSelectionLayout public int getTextSelectionLayout() { return mTextSelectionLayout; } } /** * A rounded rectangle with a configurable corner radius and the ability to expand outside of * its bounding rectangle and clip against it. Loading @@ -84,12 +105,23 @@ final class SmartSelectSprite { private static final String PROPERTY_ROUND_RATIO = "roundRatio"; /** * The direction in which the rectangle will perform its expansion. A rectangle can expand * from its left edge, its right edge or from the center (or, more precisely, the user's * touch point). For example, in left-to-right text, a selection spanning two lines with the * user's action being on the first line will have the top rectangle and expansion direction * of CENTER, while the bottom one will have an expansion direction of RIGHT. */ @Retention(SOURCE) @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) private @interface ExpansionDirection { int LEFT = 0; int CENTER = 1; int RIGHT = 2; int LEFT = -1; int CENTER = 0; int RIGHT = 1; } private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) { return expansionDirection * -1; } @Retention(SOURCE) Loading Loading @@ -119,15 +151,28 @@ final class SmartSelectSprite { /** How far offset the right edge of the rectangle is from the bounding box. */ private float mRightBoundary = 0; /** Whether the horizontal bounds are inverted (for RTL scenarios). */ private final boolean mInverted; private final float mBoundingWidth; private RoundedRectangleShape( final RectF boundingRectangle, final @ExpansionDirection int expansionDirection, final @RectangleBorderType int rectangleBorderType, final boolean inverted, final float strokeWidth) { mBoundingRectangle = new RectF(boundingRectangle); mExpansionDirection = expansionDirection; mBoundingWidth = boundingRectangle.width(); mRectangleBorderType = rectangleBorderType; mStrokeWidth = strokeWidth; mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; if (inverted) { mExpansionDirection = invert(expansionDirection); } else { mExpansionDirection = expansionDirection; } if (boundingRectangle.height() > boundingRectangle.width()) { setRoundRatio(0.0f); Loading Loading @@ -190,20 +235,28 @@ final class SmartSelectSprite { canvas.restore(); } public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { mRoundRatio = roundRatio; } public float getRoundRatio() { float getRoundRatio() { return mRoundRatio; } private void setLeftBoundary(final float leftBoundary) { mLeftBoundary = leftBoundary; private void setStartBoundary(final float startBoundary) { if (mInverted) { mRightBoundary = mBoundingWidth - startBoundary; } else { mLeftBoundary = startBoundary; } } private void setRightBoundary(final float rightBoundary) { mRightBoundary = rightBoundary; private void setEndBoundary(final float endBoundary) { if (mInverted) { mLeftBoundary = mBoundingWidth - endBoundary; } else { mRightBoundary = endBoundary; } } private float getCornerRadius() { Loading Loading @@ -247,8 +300,8 @@ final class SmartSelectSprite { private @DisplayType int mDisplayType = DisplayType.RECTANGLES; private RectangleList(final List<RoundedRectangleShape> rectangles) { mRectangles = new LinkedList<>(rectangles); mReversedRectangles = new LinkedList<>(rectangles); mRectangles = new ArrayList<>(rectangles); mReversedRectangles = new ArrayList<>(rectangles); Collections.reverse(mReversedRectangles); mOutlinePolygonPath = generateOutlinePolygonPath(rectangles); } Loading @@ -258,11 +311,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mReversedRectangles) { final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); if (leftBoundary < rectangleLeftBoundary) { rectangle.setLeftBoundary(0); rectangle.setStartBoundary(0); } else if (leftBoundary > boundarySoFar) { rectangle.setLeftBoundary(rectangle.getBoundingWidth()); rectangle.setStartBoundary(rectangle.getBoundingWidth()); } else { rectangle.setLeftBoundary( rectangle.setStartBoundary( rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); } Loading @@ -275,11 +328,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mRectangles) { final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; if (rectangleRightBoundary < rightBoundary) { rectangle.setRightBoundary(rectangle.getBoundingWidth()); rectangle.setEndBoundary(rectangle.getBoundingWidth()); } else if (boundarySoFar > rightBoundary) { rectangle.setRightBoundary(0); rectangle.setEndBoundary(0); } else { rectangle.setRightBoundary(rightBoundary - boundarySoFar); rectangle.setEndBoundary(rightBoundary - boundarySoFar); } boundarySoFar = rectangleRightBoundary; Loading Loading @@ -331,8 +384,8 @@ final class SmartSelectSprite { } /** * @param context The {@link Context} in which the animation will run * @param invalidator A {@link Runnable} which will be called every time the animation updates, * @param context the {@link Context} in which the animation will run * @param invalidator a {@link Runnable} which will be called every time the animation updates, * indicating that the view drawing the animation should invalidate itself */ SmartSelectSprite(final Context context, final Runnable invalidator) { Loading @@ -356,30 +409,36 @@ final class SmartSelectSprite { * "selection" and finally join them into a single polygon. In * order to get the correct visual behavior, these rectangles * should be sorted according to {@link #RECTANGLE_COMPARATOR}. * @param onAnimationEnd The callback which will be invoked once the whole animation * completes. * @param onAnimationEnd the callback which will be invoked once the whole animation * completes * @throws IllegalArgumentException if the given start point is not in any of the * destinationRectangles. * destinationRectangles * @see #cancelAnimation() */ // TODO nullability checks on parameters public void startAnimation( final PointF start, final List<RectF> destinationRectangles, final List<RectangleWithTextSelectionLayout> destinationRectangles, final Runnable onAnimationEnd) { cancelAnimation(); final ValueAnimator.AnimatorUpdateListener updateListener = valueAnimator -> mInvalidator.run(); final List<RoundedRectangleShape> shapes = new LinkedList<>(); final List<Animator> cornerAnimators = new LinkedList<>(); final int rectangleCount = destinationRectangles.size(); final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount); final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount); RectF centerRectangle = null; RectangleWithTextSelectionLayout centerRectangle = null; int startingOffset = 0; for (RectF rectangle : destinationRectangles) { for (int index = 0; index < rectangleCount; ++index) { final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = destinationRectangles.get(index); final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); if (contains(rectangle, start)) { centerRectangle = rectangle; centerRectangle = rectangleWithTextSelectionLayout; break; } startingOffset += rectangle.width(); Loading @@ -389,9 +448,9 @@ final class SmartSelectSprite { throw new IllegalArgumentException("Center point is not inside any of the rectangles!"); } startingOffset += start.x - centerRectangle.left; startingOffset += start.x - centerRectangle.getRectangle().left; final float centerRectangleHalfHeight = centerRectangle.height() / 2; final float centerRectangleHalfHeight = centerRectangle.getRectangle().height() / 2; final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight; final float startingOffsetRight = startingOffset + centerRectangleHalfHeight; Loading @@ -399,19 +458,21 @@ final class SmartSelectSprite { generateDirections(centerRectangle, destinationRectangles); final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = generateBorderTypes(destinationRectangles); int index = 0; generateBorderTypes(rectangleCount); for (RectF rectangle : destinationRectangles) { for (int index = 0; index < rectangleCount; ++index) { final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = destinationRectangles.get(index); final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); final RoundedRectangleShape shape = new RoundedRectangleShape( rectangle, expansionDirections[index], rectangleBorderTypes[index], rectangleWithTextSelectionLayout.getTextSelectionLayout() == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, mStrokeWidth); cornerAnimators.add(createCornerAnimator(shape, updateListener)); shapes.add(shape); index++; } final RectangleList rectangleList = new RectangleList(shapes); Loading Loading @@ -511,7 +572,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( final RectF centerRectangle, final List<RectF> rectangles) { final RectangleWithTextSelectionLayout centerRectangle, final List<RectangleWithTextSelectionLayout> rectangles) { final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; final int centerRectangleIndex = rectangles.indexOf(centerRectangle); Loading @@ -538,8 +600,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( final List<RectF> rectangles) { final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()]; final int numberOfRectangles) { final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles]; for (int i = 1; i < result.length - 1; ++i) { result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; Loading core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +5 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package android.widget; import static org.junit.Assert.assertEquals; import static java.util.function.Function.identity; import android.graphics.PointF; import android.graphics.RectF; Loading Loading @@ -105,7 +107,7 @@ public final class SelectionActionModeHelperTest { final PointF point = new PointF(pointX, pointY); final PointF adjustedPoint = SelectionActionModeHelper.movePointInsideNearestRectangle(point, mRectFList); mRectFList, identity()); assertEquals(expectedPointX, adjustedPoint.x, 0.0f); assertEquals(expectedPointY, adjustedPoint.y, 0.0f); Loading Loading @@ -254,7 +256,8 @@ public final class SelectionActionModeHelperTest { final List<RectF> result = new ArrayList<>(); final int size = inputRectangles.length; for (int index = 0; index < size; ++index) { SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index]); SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index], identity(), identity()); } assertEquals(expectedOutput, result); Loading Loading
core/java/android/widget/SelectionActionModeHelper.java +42 −19 Original line number Diff line number Diff line Loading @@ -43,9 +43,11 @@ import com.android.internal.util.Preconditions; import java.text.BreakIterator; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; Loading Loading @@ -226,7 +228,7 @@ public final class SelectionActionModeHelper { return; } final List<RectF> selectionRectangles = final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles = convertSelectionToRectangles(layout, result.mStart, result.mEnd); final PointF touchPoint = new PointF( Loading @@ -234,7 +236,8 @@ public final class SelectionActionModeHelper { mEditor.getLastUpPositionY()); final PointF animationStartPoint = movePointInsideNearestRectangle(touchPoint, selectionRectangles); movePointInsideNearestRectangle(touchPoint, selectionRectangles, SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle); mSmartSelectSprite.startAnimation( animationStartPoint, Loading @@ -242,39 +245,58 @@ public final class SelectionActionModeHelper { onAnimationEndCallback); } private List<RectF> convertSelectionToRectangles(final Layout layout, final int start, final int end) { final List<RectF> result = new ArrayList<>(); layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(result, new RectF(left, top, right, bottom))); private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles( final Layout layout, final int start, final int end) { final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>(); final Layout.SelectionRectangleConsumer consumer = (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList( result, new RectF(left, top, right, bottom), SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r, textSelectionLayout) ); layout.getSelection(start, end, consumer); result.sort(Comparator.comparing( SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, SmartSelectSprite.RECTANGLE_COMPARATOR)); result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR); return result; } // TODO: Move public pure functions out of this class and make it package-private. /** * Merges a {@link RectF} into an existing list of rectangles. While merging, this method * makes sure that: * Merges a {@link RectF} into an existing list of any objects which contain a rectangle. * While merging, this method makes sure that: * * <ol> * <li>No rectangle is redundant (contained within a bigger rectangle)</li> * <li>Rectangles of the same height and vertical position that intersect get merged</li> * </ol> * * @param list the list of rectangles to merge the new rectangle in * @param list the list of rectangles (or other rectangle containers) to merge the new * rectangle into * @param candidate the {@link RectF} to merge into the list * @param extractor a function that can extract a {@link RectF} from an element of the given * list * @param packer a function that can wrap the resulting {@link RectF} into an element that * the list contains * @hide */ @VisibleForTesting public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) { public static <T> void mergeRectangleIntoList(final List<T> list, final RectF candidate, final Function<T, RectF> extractor, final Function<RectF, T> packer) { if (candidate.isEmpty()) { return; } final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { final RectF existingRectangle = list.get(index); final RectF existingRectangle = extractor.apply(list.get(index)); if (existingRectangle.contains(candidate)) { return; } Loading @@ -297,26 +319,27 @@ public final class SelectionActionModeHelper { } for (int index = elementCount - 1; index >= 0; --index) { if (list.get(index).isEmpty()) { final RectF rectangle = extractor.apply(list.get(index)); if (rectangle.isEmpty()) { list.remove(index); } } list.add(candidate); list.add(packer.apply(candidate)); } /** @hide */ @VisibleForTesting public static PointF movePointInsideNearestRectangle(final PointF point, final List<RectF> rectangles) { public static <T> PointF movePointInsideNearestRectangle(final PointF point, final List<T> list, final Function<T, RectF> extractor) { float bestX = -1; float bestY = -1; double bestDistance = Double.MAX_VALUE; final int elementCount = rectangles.size(); final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { final RectF rectangle = rectangles.get(index); final RectF rectangle = extractor.apply(list.get(index)); final float candidateY = rectangle.centerY(); final float candidateX; Loading
core/java/android/widget/SmartSelectSprite.java +102 −40 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.Shape; import android.text.Layout; import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; Loading @@ -42,9 +43,9 @@ import android.view.animation.Interpolator; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; /** Loading Loading @@ -76,6 +77,26 @@ final class SmartSelectSprite { private Drawable mExistingDrawable = null; private RectangleList mExistingRectangleList = null; static final class RectangleWithTextSelectionLayout { private final RectF mRectangle; @Layout.TextSelectionLayout private final int mTextSelectionLayout; RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) { mRectangle = Preconditions.checkNotNull(rectangle); mTextSelectionLayout = textSelectionLayout; } public RectF getRectangle() { return mRectangle; } @Layout.TextSelectionLayout public int getTextSelectionLayout() { return mTextSelectionLayout; } } /** * A rounded rectangle with a configurable corner radius and the ability to expand outside of * its bounding rectangle and clip against it. Loading @@ -84,12 +105,23 @@ final class SmartSelectSprite { private static final String PROPERTY_ROUND_RATIO = "roundRatio"; /** * The direction in which the rectangle will perform its expansion. A rectangle can expand * from its left edge, its right edge or from the center (or, more precisely, the user's * touch point). For example, in left-to-right text, a selection spanning two lines with the * user's action being on the first line will have the top rectangle and expansion direction * of CENTER, while the bottom one will have an expansion direction of RIGHT. */ @Retention(SOURCE) @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) private @interface ExpansionDirection { int LEFT = 0; int CENTER = 1; int RIGHT = 2; int LEFT = -1; int CENTER = 0; int RIGHT = 1; } private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) { return expansionDirection * -1; } @Retention(SOURCE) Loading Loading @@ -119,15 +151,28 @@ final class SmartSelectSprite { /** How far offset the right edge of the rectangle is from the bounding box. */ private float mRightBoundary = 0; /** Whether the horizontal bounds are inverted (for RTL scenarios). */ private final boolean mInverted; private final float mBoundingWidth; private RoundedRectangleShape( final RectF boundingRectangle, final @ExpansionDirection int expansionDirection, final @RectangleBorderType int rectangleBorderType, final boolean inverted, final float strokeWidth) { mBoundingRectangle = new RectF(boundingRectangle); mExpansionDirection = expansionDirection; mBoundingWidth = boundingRectangle.width(); mRectangleBorderType = rectangleBorderType; mStrokeWidth = strokeWidth; mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; if (inverted) { mExpansionDirection = invert(expansionDirection); } else { mExpansionDirection = expansionDirection; } if (boundingRectangle.height() > boundingRectangle.width()) { setRoundRatio(0.0f); Loading Loading @@ -190,20 +235,28 @@ final class SmartSelectSprite { canvas.restore(); } public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { mRoundRatio = roundRatio; } public float getRoundRatio() { float getRoundRatio() { return mRoundRatio; } private void setLeftBoundary(final float leftBoundary) { mLeftBoundary = leftBoundary; private void setStartBoundary(final float startBoundary) { if (mInverted) { mRightBoundary = mBoundingWidth - startBoundary; } else { mLeftBoundary = startBoundary; } } private void setRightBoundary(final float rightBoundary) { mRightBoundary = rightBoundary; private void setEndBoundary(final float endBoundary) { if (mInverted) { mLeftBoundary = mBoundingWidth - endBoundary; } else { mRightBoundary = endBoundary; } } private float getCornerRadius() { Loading Loading @@ -247,8 +300,8 @@ final class SmartSelectSprite { private @DisplayType int mDisplayType = DisplayType.RECTANGLES; private RectangleList(final List<RoundedRectangleShape> rectangles) { mRectangles = new LinkedList<>(rectangles); mReversedRectangles = new LinkedList<>(rectangles); mRectangles = new ArrayList<>(rectangles); mReversedRectangles = new ArrayList<>(rectangles); Collections.reverse(mReversedRectangles); mOutlinePolygonPath = generateOutlinePolygonPath(rectangles); } Loading @@ -258,11 +311,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mReversedRectangles) { final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); if (leftBoundary < rectangleLeftBoundary) { rectangle.setLeftBoundary(0); rectangle.setStartBoundary(0); } else if (leftBoundary > boundarySoFar) { rectangle.setLeftBoundary(rectangle.getBoundingWidth()); rectangle.setStartBoundary(rectangle.getBoundingWidth()); } else { rectangle.setLeftBoundary( rectangle.setStartBoundary( rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); } Loading @@ -275,11 +328,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mRectangles) { final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; if (rectangleRightBoundary < rightBoundary) { rectangle.setRightBoundary(rectangle.getBoundingWidth()); rectangle.setEndBoundary(rectangle.getBoundingWidth()); } else if (boundarySoFar > rightBoundary) { rectangle.setRightBoundary(0); rectangle.setEndBoundary(0); } else { rectangle.setRightBoundary(rightBoundary - boundarySoFar); rectangle.setEndBoundary(rightBoundary - boundarySoFar); } boundarySoFar = rectangleRightBoundary; Loading Loading @@ -331,8 +384,8 @@ final class SmartSelectSprite { } /** * @param context The {@link Context} in which the animation will run * @param invalidator A {@link Runnable} which will be called every time the animation updates, * @param context the {@link Context} in which the animation will run * @param invalidator a {@link Runnable} which will be called every time the animation updates, * indicating that the view drawing the animation should invalidate itself */ SmartSelectSprite(final Context context, final Runnable invalidator) { Loading @@ -356,30 +409,36 @@ final class SmartSelectSprite { * "selection" and finally join them into a single polygon. In * order to get the correct visual behavior, these rectangles * should be sorted according to {@link #RECTANGLE_COMPARATOR}. * @param onAnimationEnd The callback which will be invoked once the whole animation * completes. * @param onAnimationEnd the callback which will be invoked once the whole animation * completes * @throws IllegalArgumentException if the given start point is not in any of the * destinationRectangles. * destinationRectangles * @see #cancelAnimation() */ // TODO nullability checks on parameters public void startAnimation( final PointF start, final List<RectF> destinationRectangles, final List<RectangleWithTextSelectionLayout> destinationRectangles, final Runnable onAnimationEnd) { cancelAnimation(); final ValueAnimator.AnimatorUpdateListener updateListener = valueAnimator -> mInvalidator.run(); final List<RoundedRectangleShape> shapes = new LinkedList<>(); final List<Animator> cornerAnimators = new LinkedList<>(); final int rectangleCount = destinationRectangles.size(); final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount); final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount); RectF centerRectangle = null; RectangleWithTextSelectionLayout centerRectangle = null; int startingOffset = 0; for (RectF rectangle : destinationRectangles) { for (int index = 0; index < rectangleCount; ++index) { final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = destinationRectangles.get(index); final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); if (contains(rectangle, start)) { centerRectangle = rectangle; centerRectangle = rectangleWithTextSelectionLayout; break; } startingOffset += rectangle.width(); Loading @@ -389,9 +448,9 @@ final class SmartSelectSprite { throw new IllegalArgumentException("Center point is not inside any of the rectangles!"); } startingOffset += start.x - centerRectangle.left; startingOffset += start.x - centerRectangle.getRectangle().left; final float centerRectangleHalfHeight = centerRectangle.height() / 2; final float centerRectangleHalfHeight = centerRectangle.getRectangle().height() / 2; final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight; final float startingOffsetRight = startingOffset + centerRectangleHalfHeight; Loading @@ -399,19 +458,21 @@ final class SmartSelectSprite { generateDirections(centerRectangle, destinationRectangles); final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = generateBorderTypes(destinationRectangles); int index = 0; generateBorderTypes(rectangleCount); for (RectF rectangle : destinationRectangles) { for (int index = 0; index < rectangleCount; ++index) { final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = destinationRectangles.get(index); final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); final RoundedRectangleShape shape = new RoundedRectangleShape( rectangle, expansionDirections[index], rectangleBorderTypes[index], rectangleWithTextSelectionLayout.getTextSelectionLayout() == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, mStrokeWidth); cornerAnimators.add(createCornerAnimator(shape, updateListener)); shapes.add(shape); index++; } final RectangleList rectangleList = new RectangleList(shapes); Loading Loading @@ -511,7 +572,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( final RectF centerRectangle, final List<RectF> rectangles) { final RectangleWithTextSelectionLayout centerRectangle, final List<RectangleWithTextSelectionLayout> rectangles) { final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; final int centerRectangleIndex = rectangles.indexOf(centerRectangle); Loading @@ -538,8 +600,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( final List<RectF> rectangles) { final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()]; final int numberOfRectangles) { final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles]; for (int i = 1; i < result.length - 1; ++i) { result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; Loading
core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +5 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package android.widget; import static org.junit.Assert.assertEquals; import static java.util.function.Function.identity; import android.graphics.PointF; import android.graphics.RectF; Loading Loading @@ -105,7 +107,7 @@ public final class SelectionActionModeHelperTest { final PointF point = new PointF(pointX, pointY); final PointF adjustedPoint = SelectionActionModeHelper.movePointInsideNearestRectangle(point, mRectFList); mRectFList, identity()); assertEquals(expectedPointX, adjustedPoint.x, 0.0f); assertEquals(expectedPointY, adjustedPoint.y, 0.0f); Loading Loading @@ -254,7 +256,8 @@ public final class SelectionActionModeHelperTest { final List<RectF> result = new ArrayList<>(); final int size = inputRectangles.length; for (int index = 0; index < size; ++index) { SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index]); SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index], identity(), identity()); } assertEquals(expectedOutput, result); Loading