Merge "Add MagnifierView to long screenshot UI." into sc-dev

This commit is contained in:
TreeHugger Robot
2021-02-06 02:12:01 +00:00
committed by Android (Google) Code Review
7 changed files with 296 additions and 33 deletions

View File

@@ -74,7 +74,7 @@
android:id="@+id/preview" android:id="@+id/preview"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp" android:layout_marginBottom="42dp"
android:layout_marginHorizontal="48dp" android:layout_marginHorizontal="48dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
@@ -91,19 +91,33 @@
android:id="@+id/crop_view" android:id="@+id/crop_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp" android:layout_marginBottom="42dp"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintTop_toBottomOf="@id/guideline" app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:handleThickness="3dp" app:handleThickness="@dimen/screenshot_crop_handle_thickness"
app:handleColor="@*android:color/accent_device_default" app:handleColor="@*android:color/accent_device_default"
app:scrimColor="#9444" app:scrimColor="@color/screenshot_crop_scrim"
tools:background="?android:colorBackground" tools:background="?android:colorBackground"
tools:minHeight="100dp" tools:minHeight="100dp"
tools:minWidth="100dp" /> tools:minWidth="100dp" />
<com.android.systemui.screenshot.MagnifierView
android:id="@+id/magnifier"
android:visibility="invisible"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintLeft_toLeftOf="parent"
app:handleThickness="@dimen/screenshot_crop_handle_thickness"
app:handleColor="@*android:color/accent_device_default"
app:scrimColor="@color/screenshot_crop_scrim"
app:borderThickness="4dp"
app:borderColor="#fff"
/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -178,6 +178,14 @@
<attr name="scrimColor" format="color" /> <attr name="scrimColor" format="color" />
</declare-styleable> </declare-styleable>
<declare-styleable name="MagnifierView">
<attr name="handleThickness" format="dimension" />
<attr name="handleColor" format="color" />
<attr name="scrimColor" format="color" />
<attr name="borderThickness" format="dimension" />
<attr name="borderColor" format="color" />
</declare-styleable>
<declare-styleable name="RoundedCornerProgressDrawable"> <declare-styleable name="RoundedCornerProgressDrawable">
<attr name="android:drawable" /> <attr name="android:drawable" />
</declare-styleable> </declare-styleable>

View File

@@ -197,6 +197,9 @@
<color name="global_screenshot_dismiss_foreground">@color/GM2_grey_500</color> <color name="global_screenshot_dismiss_foreground">@color/GM2_grey_500</color>
<color name="global_screenshot_background_protection_start">#40000000</color> <!-- 25% black --> <color name="global_screenshot_background_protection_start">#40000000</color> <!-- 25% black -->
<!-- Long screenshot UI -->
<color name="screenshot_crop_scrim">#9444</color>
<!-- GM2 colors --> <!-- GM2 colors -->
<color name="GM2_grey_50">#F8F9FA</color> <color name="GM2_grey_50">#F8F9FA</color>
<color name="GM2_grey_100">#F1F3F4</color> <color name="GM2_grey_100">#F1F3F4</color>

View File

@@ -345,6 +345,7 @@
<dimen name="screenshot_action_chip_padding_end">16dp</dimen> <dimen name="screenshot_action_chip_padding_end">16dp</dimen>
<dimen name="screenshot_action_chip_text_size">14sp</dimen> <dimen name="screenshot_action_chip_text_size">14sp</dimen>
<dimen name="screenshot_dismissal_height_delta">80dp</dimen> <dimen name="screenshot_dismissal_height_delta">80dp</dimen>
<dimen name="screenshot_crop_handle_thickness">3dp</dimen>
<!-- The width of the view containing navigation buttons --> <!-- The width of the view containing navigation buttons -->

View File

@@ -35,7 +35,7 @@ import com.android.systemui.R;
* cropped out. * cropped out.
*/ */
public class CropView extends View { public class CropView extends View {
private enum CropBoundary { public enum CropBoundary {
NONE, TOP, BOTTOM NONE, TOP, BOTTOM
} }
@@ -48,8 +48,14 @@ public class CropView extends View {
private float mTopCrop = 0f; private float mTopCrop = 0f;
private float mBottomCrop = 1f; 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 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) { public CropView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0); this(context, attrs, 0);
@@ -73,54 +79,84 @@ public class CropView extends View {
@Override @Override
public void onDraw(Canvas canvas) { public void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
drawShade(canvas, 0, mTopCrop); float top = mTopCrop + mTopDelta;
drawShade(canvas, mBottomCrop, 1f); float bottom = mBottomCrop + mBottomDelta;
drawHandle(canvas, mTopCrop); drawShade(canvas, 0, top);
drawHandle(canvas, mBottomCrop); drawShade(canvas, bottom, 1f);
drawHandle(canvas, top);
drawHandle(canvas, bottom);
} }
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
int topPx = fractionToPixels(mTopCrop); int topPx = fractionToPixels(mTopCrop);
int bottomPx = fractionToPixels(mBottomCrop); int bottomPx = fractionToPixels(mBottomCrop);
if (event.getAction() == MotionEvent.ACTION_DOWN) { switch (event.getAction()) {
mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx); case MotionEvent.ACTION_DOWN:
if (mCurrentDraggingBoundary != CropBoundary.NONE) { mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx);
mLastY = event.getY(); if (mCurrentDraggingBoundary != CropBoundary.NONE) {
} mStartingY = event.getY();
return true; updateListener(event);
} }
if (event.getAction() == MotionEvent.ACTION_MOVE return true;
&& mCurrentDraggingBoundary != CropBoundary.NONE) { case MotionEvent.ACTION_MOVE:
float delta = event.getY() - mLastY; if (mCurrentDraggingBoundary != CropBoundary.NONE) {
if (mCurrentDraggingBoundary == CropBoundary.TOP) { float delta = event.getY() - mStartingY;
mTopCrop = pixelsToFraction((int) MathUtils.constrain(topPx + delta, 0, if (mCurrentDraggingBoundary == CropBoundary.TOP) {
bottomPx - 2 * mCropTouchMargin)); mTopDelta = pixelsToFraction((int) MathUtils.constrain(delta, -topPx,
} else { // Bottom bottomPx - 2 * mCropTouchMargin - topPx));
mBottomCrop = pixelsToFraction((int) MathUtils.constrain(bottomPx + delta, } else { // Bottom
topPx + 2 * mCropTouchMargin, getMeasuredHeight())); mBottomDelta = pixelsToFraction((int) MathUtils.constrain(delta,
} topPx + 2 * mCropTouchMargin - bottomPx,
mLastY = event.getY(); getMeasuredHeight() - bottomPx));
invalidate(); }
return true; 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 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() { public float getTopBoundary() {
return mTopCrop; 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() { public float getBottomBoundary() {
return mBottomCrop; 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) { private void drawShade(Canvas canvas, float fracStart, float fracEnd) {
canvas.drawRect(0, fractionToPixels(fracStart), getMeasuredWidth(), canvas.drawRect(0, fractionToPixels(fracStart), getMeasuredWidth(),
fractionToPixels(fracEnd), mShadePaint); fractionToPixels(fracEnd), mShadePaint);
@@ -148,4 +184,17 @@ public class CropView extends View {
} }
return CropBoundary.NONE; 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);
}
} }

View File

@@ -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();
}
}

View File

@@ -81,6 +81,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
private View mEdit; private View mEdit;
private View mShare; private View mShare;
private CropView mCropView; private CropView mCropView;
private MagnifierView mMagnifierView;
public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor, public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
Executor bgExecutor, ImageExporter exporter, UiEventLogger uiEventLogger) { Executor bgExecutor, ImageExporter exporter, UiEventLogger uiEventLogger) {
@@ -120,13 +121,14 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
mEdit = findViewById(R.id.edit); mEdit = findViewById(R.id.edit);
mShare = findViewById(R.id.share); mShare = findViewById(R.id.share);
mCropView = findViewById(R.id.crop_view); mCropView = findViewById(R.id.crop_view);
mMagnifierView = findViewById(R.id.magnifier);
mCropView.setCropInteractionListener(mMagnifierView);
mSave.setOnClickListener(this::onClicked); mSave.setOnClickListener(this::onClicked);
mCancel.setOnClickListener(this::onClicked); mCancel.setOnClickListener(this::onClicked);
mEdit.setOnClickListener(this::onClicked); mEdit.setOnClickListener(this::onClicked);
mShare.setOnClickListener(this::onClicked); mShare.setOnClickListener(this::onClicked);
//mPreview.setImageDrawable(mImageTileSet.getDrawable());
mConnection.start(this::startCapture); mConnection.start(this::startCapture);
} }
@@ -164,6 +166,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
private void doFinish() { private void doFinish() {
mPreview.setImageDrawable(null); mPreview.setImageDrawable(null);
mMagnifierView.setImageTileset(null);
mImageTileSet.clear(); mImageTileSet.clear();
mCallback.onFinish(); mCallback.onFinish();
mWindow.getDecorView().getViewTreeObserver() mWindow.getDecorView().getViewTreeObserver()
@@ -273,6 +276,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
session.end(mCallback::onFinish); session.end(mCallback::onFinish);
} else { } else {
mPreview.setImageDrawable(mImageTileSet.getDrawable()); mPreview.setImageDrawable(mImageTileSet.getDrawable());
mMagnifierView.setImageTileset(mImageTileSet);
} }
} }
} }