diff --git a/packages/SystemUI/res/layout/long_screenshot.xml b/packages/SystemUI/res/layout/long_screenshot.xml
index 8f3345f9d85cc..e2f3e2a306e38 100644
--- a/packages/SystemUI/res/layout/long_screenshot.xml
+++ b/packages/SystemUI/res/layout/long_screenshot.xml
@@ -74,7 +74,7 @@
android:id="@+id/preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="24dp"
+ android:layout_marginBottom="42dp"
android:layout_marginHorizontal="48dp"
android:adjustViewBounds="true"
app:layout_constrainedHeight="true"
@@ -91,19 +91,33 @@
android:id="@+id/crop_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="24dp"
+ android:layout_marginBottom="42dp"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:handleThickness="3dp"
+ app:handleThickness="@dimen/screenshot_crop_handle_thickness"
app:handleColor="@*android:color/accent_device_default"
- app:scrimColor="#9444"
+ app:scrimColor="@color/screenshot_crop_scrim"
tools:background="?android:colorBackground"
tools:minHeight="100dp"
tools:minWidth="100dp" />
+
+
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 6c55fb62e6388..8166e35d5b6a7 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -178,6 +178,14 @@
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index a7cf3e91dbbae..a39c47553e876 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -197,6 +197,9 @@
@color/GM2_grey_500
#40000000
+
+ #9444
+
#F8F9FA
#F1F3F4
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d92f4ea653904..1f179f4d06215 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -345,6 +345,7 @@
16dp
14sp
80dp
+ 3dp
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
index 8e182b415488a..c8afd0b6cfe93 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
@@ -35,7 +35,7 @@ import com.android.systemui.R;
* cropped out.
*/
public class CropView extends View {
- private enum CropBoundary {
+ public enum CropBoundary {
NONE, TOP, BOTTOM
}
@@ -48,8 +48,14 @@ public class CropView extends View {
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;
+
private CropBoundary mCurrentDraggingBoundary = CropBoundary.NONE;
- private float mLastY;
+ private float mStartingY; // y coordinate of ACTION_DOWN
+ private CropInteractionListener mCropInteractionListener;
public CropView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
@@ -73,54 +79,84 @@ public class CropView extends View {
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
- drawShade(canvas, 0, mTopCrop);
- drawShade(canvas, mBottomCrop, 1f);
- drawHandle(canvas, mTopCrop);
- drawHandle(canvas, mBottomCrop);
+ float top = mTopCrop + mTopDelta;
+ float bottom = mBottomCrop + mBottomDelta;
+ drawShade(canvas, 0, top);
+ drawShade(canvas, bottom, 1f);
+ drawHandle(canvas, top);
+ drawHandle(canvas, bottom);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int topPx = fractionToPixels(mTopCrop);
int bottomPx = fractionToPixels(mBottomCrop);
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx);
- if (mCurrentDraggingBoundary != CropBoundary.NONE) {
- mLastY = event.getY();
- }
- return true;
- }
- if (event.getAction() == MotionEvent.ACTION_MOVE
- && mCurrentDraggingBoundary != CropBoundary.NONE) {
- float delta = event.getY() - mLastY;
- if (mCurrentDraggingBoundary == CropBoundary.TOP) {
- mTopCrop = pixelsToFraction((int) MathUtils.constrain(topPx + delta, 0,
- bottomPx - 2 * mCropTouchMargin));
- } else { // Bottom
- mBottomCrop = pixelsToFraction((int) MathUtils.constrain(bottomPx + delta,
- topPx + 2 * mCropTouchMargin, getMeasuredHeight()));
- }
- mLastY = event.getY();
- invalidate();
- return true;
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx);
+ if (mCurrentDraggingBoundary != CropBoundary.NONE) {
+ mStartingY = event.getY();
+ updateListener(event);
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ if (mCurrentDraggingBoundary != CropBoundary.NONE) {
+ float delta = event.getY() - mStartingY;
+ if (mCurrentDraggingBoundary == CropBoundary.TOP) {
+ mTopDelta = pixelsToFraction((int) MathUtils.constrain(delta, -topPx,
+ bottomPx - 2 * mCropTouchMargin - topPx));
+ } else { // Bottom
+ mBottomDelta = pixelsToFraction((int) MathUtils.constrain(delta,
+ topPx + 2 * mCropTouchMargin - bottomPx,
+ getMeasuredHeight() - bottomPx));
+ }
+ updateListener(event);
+ invalidate();
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (mCurrentDraggingBoundary != CropBoundary.NONE) {
+ // Commit the delta to the stored crop values.
+ mTopCrop += mTopDelta;
+ mBottomCrop += mBottomDelta;
+ mTopDelta = 0;
+ mBottomDelta = 0;
+ updateListener(event);
+ }
}
return super.onTouchEvent(event);
}
/**
- * @return value [0,1] representing the position of the top crop boundary.
+ * @return value [0,1] representing the position of the top crop boundary. Does not reflect
+ * changes from any in-progress touch input.
*/
public float getTopBoundary() {
return mTopCrop;
}
/**
- * @return value [0,1] representing the position of the bottom crop boundary.
+ * @return value [0,1] representing the position of the bottom crop boundary. Does not reflect
+ * changes from any in-progress touch input.
*/
public float getBottomBoundary() {
return mBottomCrop;
}
+ public void setCropInteractionListener(CropInteractionListener listener) {
+ mCropInteractionListener = listener;
+ }
+
+ private void updateListener(MotionEvent event) {
+ if (mCropInteractionListener != null) {
+ float boundaryPosition = (mCurrentDraggingBoundary == CropBoundary.TOP)
+ ? mTopCrop + mTopDelta : mBottomCrop + mBottomDelta;
+ mCropInteractionListener.onCropMotionEvent(event, mCurrentDraggingBoundary,
+ boundaryPosition, fractionToPixels(boundaryPosition));
+ }
+ }
+
private void drawShade(Canvas canvas, float fracStart, float fracEnd) {
canvas.drawRect(0, fractionToPixels(fracStart), getMeasuredWidth(),
fractionToPixels(fracEnd), mShadePaint);
@@ -148,4 +184,17 @@ public class CropView extends View {
}
return CropBoundary.NONE;
}
+
+ /**
+ * Listen for crop motion events and state.
+ */
+ public interface CropInteractionListener {
+ /**
+ * Called whenever CropView has a MotionEvent that can impact the position of the crop
+ * boundaries.
+ */
+ void onCropMotionEvent(MotionEvent event, CropBoundary boundary, float boundaryPosition,
+ int boundaryPositionPx);
+
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java
new file mode 100644
index 0000000000000..f88715164bc78
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2021 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 com.android.systemui.screenshot;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.R;
+
+/**
+ * MagnifierView shows a full-res cropped circular display of a given ImageTileSet, contents and
+ * positioning dereived from events from a CropView to which it listens.
+ *
+ * Not meant to be a general-purpose magnifier!
+ */
+public class MagnifierView extends View implements CropView.CropInteractionListener {
+ private Drawable mDrawable;
+
+ private final Paint mShadePaint;
+ private final Paint mHandlePaint;
+
+ private Path mOuterCircle;
+ private Path mInnerCircle;
+
+ private Path mCheckerboard;
+ private Paint mCheckerboardPaint;
+ private final float mBorderPx;
+ private final int mBorderColor;
+ private float mCheckerboardBoxSize = 40;
+
+ private float mLastCropPosition;
+ private CropView.CropBoundary mCropBoundary;
+
+ public MagnifierView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MagnifierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray t = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.MagnifierView, 0, 0);
+ mShadePaint = new Paint();
+ mShadePaint.setColor(t.getColor(R.styleable.MagnifierView_scrimColor, Color.TRANSPARENT));
+ mHandlePaint = new Paint();
+ mHandlePaint.setColor(t.getColor(R.styleable.MagnifierView_handleColor, Color.BLACK));
+ mHandlePaint.setStrokeWidth(
+ t.getDimensionPixelSize(R.styleable.MagnifierView_handleThickness, 20));
+ mBorderPx = t.getDimensionPixelSize(R.styleable.MagnifierView_borderThickness, 0);
+ mBorderColor = t.getColor(R.styleable.MagnifierView_borderColor, Color.WHITE);
+ t.recycle();
+ mCheckerboardPaint = new Paint();
+ mCheckerboardPaint.setColor(Color.GRAY);
+ }
+
+ public void setImageTileset(ImageTileSet tiles) {
+ if (tiles != null) {
+ mDrawable = tiles.getDrawable();
+ mDrawable.setBounds(0, 0, tiles.getWidth(), tiles.getHeight());
+ } else {
+ mDrawable = null;
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ int radius = getWidth() / 2;
+ mOuterCircle = new Path();
+ mOuterCircle.addCircle(radius, radius, radius, Path.Direction.CW);
+ mInnerCircle = new Path();
+ mInnerCircle.addCircle(radius, radius, radius - mBorderPx, Path.Direction.CW);
+ mCheckerboard = generateCheckerboard();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // TODO: just draw a circle at the end instead of clipping like this?
+ canvas.clipPath(mOuterCircle);
+ canvas.drawColor(mBorderColor);
+ canvas.clipPath(mInnerCircle);
+
+ // Draw a checkerboard pattern for out of bounds.
+ canvas.drawPath(mCheckerboard, mCheckerboardPaint);
+
+ if (mDrawable != null) {
+ 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,
+ -mDrawable.getBounds().height() * mLastCropPosition + getHeight() / 2);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ Rect scrimRect = new Rect(0, 0, getWidth(), getHeight() / 2);
+ if (mCropBoundary == CropView.CropBoundary.BOTTOM) {
+ scrimRect.offset(0, getHeight() / 2);
+ }
+ canvas.drawRect(scrimRect, mShadePaint);
+
+ canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mHandlePaint);
+ }
+
+ @Override
+ public void onCropMotionEvent(MotionEvent event, CropView.CropBoundary boundary,
+ float cropPosition, int cropPositionPx) {
+ mCropBoundary = boundary;
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mLastCropPosition = cropPosition;
+ setTranslationY(cropPositionPx - getHeight() / 2);
+ setPivotX(getWidth() / 2);
+ setPivotY(getHeight() / 2);
+ setScaleX(0.2f);
+ setScaleY(0.2f);
+ setAlpha(0f);
+ setTranslationX((getParentWidth() - getWidth()) / 2);
+ setVisibility(View.VISIBLE);
+ animate().alpha(1f).translationX(0).scaleX(1f).scaleY(1f).start();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mLastCropPosition = cropPosition;
+ setTranslationY(cropPositionPx - getHeight() / 2);
+ invalidate();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ animate().alpha(0).translationX((getParentWidth() - getWidth()) / 2).scaleX(0.2f)
+ .scaleY(0.2f).withEndAction(() -> setVisibility(View.INVISIBLE)).start();
+ break;
+ }
+ }
+
+ private Path generateCheckerboard() {
+ Path path = new Path();
+ int checkerWidth = (int) Math.ceil(getWidth() / mCheckerboardBoxSize);
+ int checkerHeight = (int) Math.ceil(getHeight() / mCheckerboardBoxSize);
+
+ for (int row = 0; row < checkerHeight; row++) {
+ // Alternate starting on the first and second column;
+ int colStart = (row % 2 == 0) ? 0 : 1;
+ for (int col = colStart; col < checkerWidth; col += 2) {
+ path.addRect(col * mCheckerboardBoxSize,
+ row * mCheckerboardBoxSize,
+ (col + 1) * mCheckerboardBoxSize,
+ (row + 1) * mCheckerboardBoxSize,
+ Path.Direction.CW);
+ }
+ }
+ return path;
+ }
+
+ private int getParentWidth() {
+ return ((View) getParent()).getWidth();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 18c379a4650ff..25438a6f57bae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -81,6 +81,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
private View mEdit;
private View mShare;
private CropView mCropView;
+ private MagnifierView mMagnifierView;
public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
Executor bgExecutor, ImageExporter exporter, UiEventLogger uiEventLogger) {
@@ -120,13 +121,14 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
mEdit = findViewById(R.id.edit);
mShare = findViewById(R.id.share);
mCropView = findViewById(R.id.crop_view);
+ mMagnifierView = findViewById(R.id.magnifier);
+ mCropView.setCropInteractionListener(mMagnifierView);
mSave.setOnClickListener(this::onClicked);
mCancel.setOnClickListener(this::onClicked);
mEdit.setOnClickListener(this::onClicked);
mShare.setOnClickListener(this::onClicked);
- //mPreview.setImageDrawable(mImageTileSet.getDrawable());
mConnection.start(this::startCapture);
}
@@ -164,6 +166,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
private void doFinish() {
mPreview.setImageDrawable(null);
+ mMagnifierView.setImageTileset(null);
mImageTileSet.clear();
mCallback.onFinish();
mWindow.getDecorView().getViewTreeObserver()
@@ -273,6 +276,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
session.end(mCallback::onFinish);
} else {
mPreview.setImageDrawable(mImageTileSet.getDrawable());
+ mMagnifierView.setImageTileset(mImageTileSet);
}
}
}