Merge "Update long screenshot crop per mocks" into sc-dev

This commit is contained in:
Matt Casey
2021-03-22 15:50:19 +00:00
committed by Android (Google) Code Review
4 changed files with 174 additions and 117 deletions

View File

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

View File

@@ -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 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;
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 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;
@@ -338,7 +395,7 @@ public class CropView extends View {
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
switch(virtualViewId) {
switch (virtualViewId) {
case TOP_HANDLE_ID:
event.setContentDescription(
getResources().getString(R.string.screenshot_top_boundary));
@@ -353,16 +410,16 @@ public class CropView extends View {
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId,
AccessibilityNodeInfo node) {
switch(virtualViewId) {
switch (virtualViewId) {
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

View File

@@ -128,7 +128,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);
@@ -206,7 +206,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
@@ -363,10 +363,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.");
@@ -404,7 +402,7 @@ public class LongScreenshotActivity extends Activity {
}
}
private void updateCropLocation() {
private void updateImageDimensions() {
Drawable drawable = mPreview.getDrawable();
if (drawable == null) {
return;
@@ -419,9 +417,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));
}
}
}

View File

@@ -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()) {