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

Commit 4062430f authored by Matt Casey's avatar Matt Casey
Browse files

Update long screenshot crop per mocks

- Can crop on both axes (for alignment with markup)
- Updated handle drawing
- Vertical dragging is functionally the same, but horizontal dragging is
allowed if vertical hasn't triggered (intentionally giving priority to
vertical since that's the important function here).
- Lots of refactoring in CropView to allow for this without filling it
with switch statements.
- Only showing magnifier for vertical cropping (for now).
- Scrim the long screenshot image but nothing beyond that.
- Lessen scrim opacity

Bug: 180967468
Test: Manual testing
Change-Id: I85593414ab5aca12b365da34529d2033226b1cfb
parent 177d8e36
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -202,7 +202,7 @@
    <color name="global_screenshot_background_protection_start">#40000000</color> <!-- 25% black -->

    <!-- Long screenshot UI -->
    <color name="screenshot_crop_scrim">#9444</color>
    <color name="screenshot_crop_scrim">#6444</color>

    <!-- GM2 colors -->
    <color name="GM2_grey_50">#F8F9FA</color>
+161 −107
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.systemui.screenshot;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
@@ -25,6 +23,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -32,6 +31,7 @@ import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
import android.util.MathUtils;
import android.util.Range;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
@@ -49,29 +49,30 @@ import com.android.systemui.R;
 */
public class CropView extends View {
    private static final String TAG = "CropView";

    public enum CropBoundary {
        NONE, TOP, BOTTOM
        NONE, TOP, BOTTOM, LEFT, RIGHT
    }

    private final float mCropTouchMargin;
    private final Paint mShadePaint;
    private final Paint mHandlePaint;

    // Top and bottom crops are stored as floats [0, 1], representing the top and bottom of the
    // view, respectively.
    private float mTopCrop = 0f;
    private float mBottomCrop = 1f;

    // When the user is dragging a handle, these variables store the distance between the top/bottom
    // crop values and
    private float mTopDelta = 0f;
    private float mBottomDelta = 0f;
    // Crop rect with each element represented as [0,1] along its proper axis.
    private RectF mCrop = new RectF(0, 0, 1, 1);

    private int mExtraTopPadding;
    private int mExtraBottomPadding;
    private int mImageWidth;

    private CropBoundary mCurrentDraggingBoundary = CropBoundary.NONE;
    // The starting value of mCurrentDraggingBoundary's crop, used to compute touch deltas.
    private float mMovementStartValue;
    private float mStartingY;  // y coordinate of ACTION_DOWN
    private float mStartingX;
    // The allowable values for the current boundary being dragged
    private Range<Float> mMotionRange;

    private CropInteractionListener mCropInteractionListener;

    public CropView(Context context, @Nullable AttributeSet attrs) {
@@ -86,6 +87,7 @@ public class CropView extends View {
        mShadePaint.setColor(t.getColor(R.styleable.CropView_scrimColor, Color.TRANSPARENT));
        mHandlePaint = new Paint();
        mHandlePaint.setColor(t.getColor(R.styleable.CropView_handleColor, Color.BLACK));
        mHandlePaint.setStrokeCap(Paint.Cap.ROUND);
        mHandlePaint.setStrokeWidth(
                t.getDimensionPixelSize(R.styleable.CropView_handleThickness, 20));
        t.recycle();
@@ -100,8 +102,7 @@ public class CropView extends View {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);
        ss.mTopBoundary = getTopBoundary();
        ss.mBottomBoundary = getBottomBoundary();
        ss.mCrop = mCrop;
        return ss;
    }

@@ -110,45 +111,67 @@ public class CropView extends View {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        setBoundaryTo(CropBoundary.TOP, ss.mTopBoundary);
        setBoundaryTo(CropBoundary.BOTTOM, ss.mBottomBoundary);
        mCrop = ss.mCrop;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float top = mTopCrop + mTopDelta;
        float bottom = mBottomCrop + mBottomDelta;
        drawShade(canvas, 0, top);
        drawShade(canvas, bottom, 1f);
        drawHandle(canvas, top, /* draw the handle tab down */ false);
        drawHandle(canvas, bottom, /* draw the handle tab up */ true);
        drawShade(canvas, 0, 0, 1, mCrop.top);
        drawShade(canvas, 0, mCrop.bottom, 1, 1);
        drawShade(canvas, 0, mCrop.top, mCrop.left, mCrop.bottom);
        drawShade(canvas, mCrop.right, mCrop.top, 1, mCrop.bottom);
        drawHorizontalHandle(canvas, mCrop.top, /* draw the handle tab up */ true);
        drawHorizontalHandle(canvas, mCrop.bottom, /* draw the handle tab down */ false);
        drawVerticalHandle(canvas, mCrop.left, /* left */ true);
        drawVerticalHandle(canvas, mCrop.right, /* right */ false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int topPx = fractionToPixels(mTopCrop);
        int bottomPx = fractionToPixels(mBottomCrop);
        int topPx = fractionToVerticalPixels(mCrop.top);
        int bottomPx = fractionToVerticalPixels(mCrop.bottom);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx);
                mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx,
                        fractionToHorizontalPixels(mCrop.left),
                        fractionToHorizontalPixels(mCrop.right));
                if (mCurrentDraggingBoundary != CropBoundary.NONE) {
                    mStartingY = event.getY();
                    mStartingX = event.getX();
                    mMovementStartValue = getBoundaryPosition(mCurrentDraggingBoundary);
                    updateListener(event);
                    switch (mCurrentDraggingBoundary) {
                        case TOP:
                            mMotionRange = new Range<>(0f,
                                    mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
                                            CropBoundary.BOTTOM));
                            break;
                        case BOTTOM:
                            mMotionRange = new Range<>(
                                    mCrop.top + pixelDistanceToFraction(mCropTouchMargin,
                                            CropBoundary.TOP), 1f);
                            break;
                        case LEFT:
                            mMotionRange = new Range<>(0f,
                                    mCrop.right - pixelDistanceToFraction(mCropTouchMargin,
                                            CropBoundary.RIGHT));
                            break;
                        case RIGHT:
                            mMotionRange = new Range<>(
                                    mCrop.left + pixelDistanceToFraction(mCropTouchMargin,
                                            CropBoundary.LEFT), 1f);
                            break;
                    }
                }
                return true;
            case MotionEvent.ACTION_MOVE:
                if (mCurrentDraggingBoundary != CropBoundary.NONE) {
                    float delta = event.getY() - mStartingY;
                    if (mCurrentDraggingBoundary == CropBoundary.TOP) {
                        mTopDelta = pixelDistanceToFraction((int) MathUtils.constrain(delta,
                                -topPx + mExtraTopPadding,
                                bottomPx - 2 * mCropTouchMargin - topPx));
                    } else {  // Bottom
                        mBottomDelta = pixelDistanceToFraction((int) MathUtils.constrain(delta,
                                topPx + 2 * mCropTouchMargin - bottomPx,
                                getHeight() - bottomPx - mExtraBottomPadding));
                    }
                    float deltaPx = isVertical(mCurrentDraggingBoundary) ? event.getY() - mStartingY
                            : event.getX() - mStartingX;
                    float delta = pixelDistanceToFraction((int) deltaPx, mCurrentDraggingBoundary);
                    setBoundaryPosition(mCurrentDraggingBoundary,
                            mMotionRange.clamp(mMovementStartValue + delta));
                    updateListener(event);
                    invalidate();
                    return true;
@@ -156,8 +179,6 @@ public class CropView extends View {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (mCurrentDraggingBoundary != CropBoundary.NONE) {
                    // Commit the delta to the stored crop values.
                    commitDeltas(mCurrentDraggingBoundary);
                    updateListener(event);
                }
        }
@@ -167,22 +188,46 @@ public class CropView extends View {
    /**
     * Set the given boundary to the given value without animation.
     */
    public void setBoundaryTo(CropBoundary boundary, float value) {
    public void setBoundaryPosition(CropBoundary boundary, float position) {
        switch (boundary) {
            case TOP:
                mTopCrop = value;
                mCrop.top = position;
                break;
            case BOTTOM:
                mBottomCrop = value;
                mCrop.bottom = position;
                break;
            case LEFT:
                mCrop.left = position;
                break;
            case RIGHT:
                mCrop.right = position;
                break;
            case NONE:
                Log.w(TAG, "No boundary selected for animation");
                Log.w(TAG, "No boundary selected");
                break;
        }

        invalidate();
    }

    private float getBoundaryPosition(CropBoundary boundary) {
        switch (boundary) {
            case TOP:
                return mCrop.top;
            case BOTTOM:
                return mCrop.bottom;
            case LEFT:
                return mCrop.left;
            case RIGHT:
                return mCrop.right;
        }
        return 0;
    }

    private static boolean isVertical(CropBoundary boundary) {
        return boundary == CropBoundary.TOP || boundary == CropBoundary.BOTTOM;
    }

    /**
     * Animate the given boundary to the given value.
     */
@@ -191,28 +236,13 @@ public class CropView extends View {
            Log.w(TAG, "No boundary selected for animation");
            return;
        }
        float totalDelta = (boundary == CropBoundary.TOP) ? (value - mTopCrop)
                : (value - mBottomCrop);
        float start = getBoundaryPosition(boundary);
        ValueAnimator animator = new ValueAnimator();
        animator.addUpdateListener(animation -> {
            if (boundary == CropBoundary.TOP) {
                mTopDelta = animation.getAnimatedFraction() * totalDelta;
            } else {
                mBottomDelta = animation.getAnimatedFraction() * totalDelta;
            }
            setBoundaryPosition(boundary,
                    MathUtils.lerp(start, value, animation.getAnimatedFraction()));
            invalidate();
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                commitDeltas(boundary);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                commitDeltas(boundary);
            }
        });
        animator.setFloatValues(0f, 1f);
        animator.setDuration(750);
        animator.setInterpolator(new FastOutSlowInInterpolator());
@@ -230,65 +260,79 @@ public class CropView extends View {
    }

    /**
     * @return value [0,1] representing the position of the top crop boundary. Does not reflect
     * changes from any in-progress touch input.
     * Set the pixel width of the image on the screen (on-screen dimension, not actual bitmap
     * dimension)
     */
    public float getTopBoundary() {
        return mTopCrop;
    public void setImageWidth(int width) {
        mImageWidth = width;
        invalidate();
    }

    /**
     * @return value [0,1] representing the position of the bottom crop boundary. Does not reflect
     * changes from any in-progress touch input.
     * @return RectF with values [0,1] representing the position of the boundaries along image axes.
     */
    public float getBottomBoundary() {
        return mBottomCrop;
    public Rect getCropBoundaries(int imageWidth, int imageHeight) {
        return new Rect((int) (mCrop.left * imageWidth), (int) (mCrop.top * imageHeight),
                (int) (mCrop.right * imageWidth), (int) (mCrop.bottom * imageHeight));
    }

    public void setCropInteractionListener(CropInteractionListener listener) {
        mCropInteractionListener = listener;
    }

    private void commitDeltas(CropBoundary boundary) {
        if (boundary == CropBoundary.TOP) {
            mTopCrop += mTopDelta;
            mTopDelta = 0;
        } else if (boundary == CropBoundary.BOTTOM) {
            mBottomCrop += mBottomDelta;
            mBottomDelta = 0;
        }
    }

    private void updateListener(MotionEvent event) {
        if (mCropInteractionListener != null) {
            float boundaryPosition = (mCurrentDraggingBoundary == CropBoundary.TOP)
                    ? mTopCrop + mTopDelta : mBottomCrop + mBottomDelta;
        if (mCropInteractionListener != null && (isVertical(mCurrentDraggingBoundary))) {
            float boundaryPosition = getBoundaryPosition(mCurrentDraggingBoundary);
            mCropInteractionListener.onCropMotionEvent(event, mCurrentDraggingBoundary,
                    boundaryPosition, fractionToPixels(boundaryPosition));
                    boundaryPosition, fractionToVerticalPixels(boundaryPosition),
                    (mCrop.left + mCrop.right) / 2);
        }
    }

    private void drawShade(Canvas canvas, float fracStart, float fracEnd) {
        canvas.drawRect(0, fractionToPixels(fracStart), getWidth(),
                fractionToPixels(fracEnd), mShadePaint);
    /**
     * Draw a shade to the given canvas with the given [0,1] fractional image bounds.
     */
    private void drawShade(Canvas canvas, float left, float top, float right, float bottom) {
        canvas.drawRect(fractionToHorizontalPixels(left), fractionToVerticalPixels(top),
                fractionToHorizontalPixels(right),
                fractionToVerticalPixels(bottom), mShadePaint);
    }

    private void drawHorizontalHandle(Canvas canvas, float frac, boolean handleTabUp) {
        int y = fractionToVerticalPixels(frac);
        canvas.drawLine(fractionToHorizontalPixels(mCrop.left), y,
                fractionToHorizontalPixels(mCrop.right), y, mHandlePaint);
        float radius = 8 * getResources().getDisplayMetrics().density;
        int x = (fractionToHorizontalPixels(mCrop.left) + fractionToHorizontalPixels(mCrop.right))
                / 2;
        canvas.drawArc(x - radius, y - radius, x + radius, y + radius, handleTabUp ? 180 : 0, 180,
                true, mHandlePaint);
    }

    private void drawHandle(Canvas canvas, float frac, boolean handleTabUp) {
        int y = fractionToPixels(frac);
        canvas.drawLine(0, y, getWidth(), y, mHandlePaint);
        float radius = 15 * getResources().getDisplayMetrics().density;
        float x = getWidth() * .9f;
        canvas.drawArc(x - radius, y - radius, x + radius, y + radius, handleTabUp ? 180 : 0, 180,
    private void drawVerticalHandle(Canvas canvas, float frac, boolean handleTabLeft) {
        int x = fractionToHorizontalPixels(frac);
        canvas.drawLine(x, fractionToVerticalPixels(mCrop.top), x,
                fractionToVerticalPixels(mCrop.bottom), mHandlePaint);
        float radius = 8 * getResources().getDisplayMetrics().density;
        int y = (fractionToVerticalPixels(getBoundaryPosition(CropBoundary.TOP))
                + fractionToVerticalPixels(
                getBoundaryPosition(CropBoundary.BOTTOM))) / 2;
        canvas.drawArc(x - radius, y - radius, x + radius, y + radius, handleTabLeft ? 90 : 270,
                180,
                true, mHandlePaint);
    }

    /**
     * Convert the given fraction position to pixel position within the View.
     */
    private int fractionToPixels(float frac) {
    private int fractionToVerticalPixels(float frac) {
        return (int) (mExtraTopPadding + frac * getImageHeight());
    }

    private int fractionToHorizontalPixels(float frac) {
        return (int) ((getWidth() - mImageWidth) / 2 + frac * mImageWidth);
    }

    private int getImageHeight() {
        return getHeight() - mExtraTopPadding - mExtraBottomPadding;
    }
@@ -296,17 +340,30 @@ public class CropView extends View {
    /**
     * Convert the given pixel distance to fraction of the image.
     */
    private float pixelDistanceToFraction(int px) {
        return px / (float) getImageHeight();
    private float pixelDistanceToFraction(float px, CropBoundary boundary) {
        if (isVertical(boundary)) {
            return px / getImageHeight();
        } else {
            return px / mImageWidth;
        }
    }

    private CropBoundary nearestBoundary(MotionEvent event, int topPx, int bottomPx) {
    private CropBoundary nearestBoundary(MotionEvent event, int topPx, int bottomPx, int leftPx,
            int rightPx) {
        if (Math.abs(event.getY() - topPx) < mCropTouchMargin) {
            return CropBoundary.TOP;
        }
        if (Math.abs(event.getY() - bottomPx) < mCropTouchMargin) {
            return CropBoundary.BOTTOM;
        }
        if (event.getY() > topPx || event.getY() < bottomPx) {
            if (Math.abs(event.getX() - leftPx) < mCropTouchMargin) {
                return CropBoundary.LEFT;
            }
            if (Math.abs(event.getX() - rightPx) < mCropTouchMargin) {
                return CropBoundary.RIGHT;
            }
        }
        return CropBoundary.NONE;
    }

@@ -321,10 +378,10 @@ public class CropView extends View {

        @Override
        protected int getVirtualViewAt(float x, float y) {
            if (Math.abs(y - fractionToPixels(mTopCrop)) < mCropTouchMargin) {
            if (Math.abs(y - fractionToVerticalPixels(mCrop.top)) < mCropTouchMargin) {
                return TOP_HANDLE_ID;
            }
            if (Math.abs(y - fractionToPixels(mBottomCrop)) < mCropTouchMargin) {
            if (Math.abs(y - fractionToVerticalPixels(mCrop.bottom)) < mCropTouchMargin) {
                return BOTTOM_HANDLE_ID;
            }
            return ExploreByTouchHelper.INVALID_ID;
@@ -357,12 +414,12 @@ public class CropView extends View {
                case TOP_HANDLE_ID:
                    node.setContentDescription(
                            getResources().getString(R.string.screenshot_top_boundary));
                    setNodePositions(mTopCrop, node);
                    setNodePositions(mCrop.top, node);
                    break;
                case BOTTOM_HANDLE_ID:
                    node.setContentDescription(
                            getResources().getString(R.string.screenshot_bottom_boundary));
                    setNodePositions(mBottomCrop, node);
                    setNodePositions(mCrop.bottom, node);
                    break;
            }

@@ -380,7 +437,7 @@ public class CropView extends View {
        }

        private void setNodePositions(float fraction, AccessibilityNodeInfo node) {
            int pixels = fractionToPixels(fraction);
            int pixels = fractionToVerticalPixels(fraction);
            Rect rect = new Rect(0, (int) (pixels - mCropTouchMargin),
                    getWidth(), (int) (pixels + mCropTouchMargin));
            node.setBoundsInParent(rect);
@@ -400,12 +457,11 @@ public class CropView extends View {
         * boundaries.
         */
        void onCropMotionEvent(MotionEvent event, CropBoundary boundary, float boundaryPosition,
                int boundaryPositionPx);
                int boundaryPositionPx, float horizontalCenter);
    }

    static class SavedState extends BaseSavedState {
        float mTopBoundary;
        float mBottomBoundary;
        RectF mCrop;

        /**
         * Constructor called from {@link CropView#onSaveInstanceState()}
@@ -419,15 +475,13 @@ public class CropView extends View {
         */
        private SavedState(Parcel in) {
            super(in);
            mTopBoundary = in.readFloat();
            mBottomBoundary = in.readFloat();
            mCrop = in.readParcelable(ClassLoader.getSystemClassLoader());
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(mTopBoundary);
            out.writeFloat(mBottomBoundary);
            out.writeParcelable(mCrop, 0);
        }

        public static final Parcelable.Creator<SavedState> CREATOR
+8 −7
Original line number Diff line number Diff line
@@ -131,7 +131,7 @@ public class LongScreenshotActivity extends Activity {

        mPreview.addOnLayoutChangeListener(
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
                        updateCropLocation());
                        updateImageDimensions());

        Intent intent = getIntent();
        mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE);
@@ -209,7 +209,7 @@ public class LongScreenshotActivity extends Activity {
        Log.d(TAG, "onCaptureCompleted(longScreenshot=" + longScreenshot + ")");
        mLongScreenshot = longScreenshot;
        mPreview.setImageDrawable(mLongScreenshot.getDrawable());
        updateCropLocation();
        updateImageDimensions();
        mMagnifierView.setDrawable(mLongScreenshot.getDrawable(),
                mLongScreenshot.getWidth(), mLongScreenshot.getHeight());
        // Original boundaries go from the image tile set's y=0 to y=pageSize, so
@@ -369,10 +369,8 @@ public class LongScreenshotActivity extends Activity {
            return;
        }

        Rect bounds = new Rect(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        int height = bounds.height();
        bounds.top = (int) (height * mCropView.getTopBoundary());
        bounds.bottom = (int) (height * mCropView.getBottomBoundary());
        Rect bounds = mCropView.getCropBoundaries(drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight());

        if (bounds.isEmpty()) {
            Log.w(TAG, "Crop bounds empty, skipping export.");
@@ -410,7 +408,7 @@ public class LongScreenshotActivity extends Activity {
        }
    }

    private void updateCropLocation() {
    private void updateImageDimensions() {
        Drawable drawable = mPreview.getDrawable();
        if (drawable == null) {
            return;
@@ -425,9 +423,12 @@ public class LongScreenshotActivity extends Activity {
            float imageHeight = mPreview.getHeight() * viewRatio / imageRatio;
            int extraPadding = (int) (mPreview.getHeight() - imageHeight) / 2;
            mCropView.setExtraPadding(extraPadding, extraPadding);
            mCropView.setImageWidth(mPreview.getWidth());
        } else {
            // Image is full height
            mCropView.setExtraPadding(0, 0);
            mCropView.setImageWidth((int) (mPreview.getHeight() * imageRatio));
        }

    }
}
+4 −2
Original line number Diff line number Diff line
@@ -58,6 +58,7 @@ public class MagnifierView extends View implements CropView.CropInteractionListe
    private float mCheckerboardBoxSize = 40;

    private float mLastCropPosition;
    private float mLastCenter = 0.5f;
    private CropView.CropBoundary mCropBoundary;

    private ViewPropertyAnimator mTranslationAnimator;
@@ -131,7 +132,7 @@ public class MagnifierView extends View implements CropView.CropInteractionListe
            canvas.save();
            // Translate such that the center of this view represents the center of the crop
            // boundary.
            canvas.translate(-mDrawable.getBounds().width() / 2 + getWidth() / 2,
            canvas.translate(-mDrawable.getBounds().width() * mLastCenter + getWidth() / 2,
                    -mDrawable.getBounds().height() * mLastCropPosition + getHeight() / 2);
            mDrawable.draw(canvas);
            canvas.restore();
@@ -148,8 +149,9 @@ public class MagnifierView extends View implements CropView.CropInteractionListe

    @Override
    public void onCropMotionEvent(MotionEvent event, CropView.CropBoundary boundary,
            float cropPosition, int cropPositionPx) {
            float cropPosition, int cropPositionPx, float horizontalCenter) {
        mCropBoundary = boundary;
        mLastCenter = horizontalCenter;
        boolean touchOnRight = event.getX() > getParentWidth() / 2;
        float translateXTarget = touchOnRight ? 0 : getParentWidth() - getWidth();
        switch (event.getAction()) {