| /* |
| * 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.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.annotation.NonNull; |
| 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.View; |
| import android.view.ViewPropertyAnimator; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.graphics.ColorUtils; |
| import com.android.systemui.R; |
| |
| /** |
| * MagnifierView shows a full-res cropped circular display of a given ImageTileSet, contents and |
| * positioning derived 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 float mLastCenter = 0.5f; |
| private CropView.CropBoundary mCropBoundary; |
| |
| private ViewPropertyAnimator mTranslationAnimator; |
| private final Animator.AnimatorListener mTranslationAnimatorListener = |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| mTranslationAnimator = null; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mTranslationAnimator = null; |
| } |
| }; |
| |
| 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(); |
| int alpha = t.getInteger(R.styleable.MagnifierView_scrimAlpha, 255); |
| int scrimColor = t.getColor(R.styleable.MagnifierView_scrimColor, Color.TRANSPARENT); |
| mShadePaint.setColor(ColorUtils.setAlphaComponent(scrimColor, alpha)); |
| 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); |
| } |
| |
| /** |
| * Set the drawable to be displayed by the magnifier. |
| */ |
| public void setDrawable(@NonNull Drawable drawable, int width, int height) { |
| mDrawable = drawable; |
| mDrawable.setBounds(0, 0, width, height); |
| 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() * mLastCenter + 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 onCropDragStarted(CropView.CropBoundary boundary, float boundaryPosition, |
| int boundaryPositionPx, float horizontalCenter, float x) { |
| mCropBoundary = boundary; |
| mLastCenter = horizontalCenter; |
| boolean touchOnRight = x > getParentWidth() / 2; |
| float translateXTarget = touchOnRight ? 0 : getParentWidth() - getWidth(); |
| mLastCropPosition = boundaryPosition; |
| setTranslationY(boundaryPositionPx - getHeight() / 2); |
| setPivotX(getWidth() / 2); |
| setPivotY(getHeight() / 2); |
| setScaleX(0.2f); |
| setScaleY(0.2f); |
| setAlpha(0f); |
| setTranslationX((getParentWidth() - getWidth()) / 2); |
| setVisibility(View.VISIBLE); |
| mTranslationAnimator = |
| animate().alpha(1f).translationX(translateXTarget).scaleX(1f).scaleY(1f); |
| mTranslationAnimator.setListener(mTranslationAnimatorListener); |
| mTranslationAnimator.start(); |
| } |
| |
| @Override |
| public void onCropDragMoved(CropView.CropBoundary boundary, float boundaryPosition, |
| int boundaryPositionPx, float horizontalCenter, float x) { |
| boolean touchOnRight = x > getParentWidth() / 2; |
| float translateXTarget = touchOnRight ? 0 : getParentWidth() - getWidth(); |
| // The touch is near the middle if it's within 10% of the center point. |
| // We don't want to animate horizontally if the touch is near the middle. |
| boolean nearMiddle = Math.abs(x - getParentWidth() / 2) |
| < getParentWidth() / 10f; |
| boolean viewOnLeft = getTranslationX() < (getParentWidth() - getWidth()) / 2; |
| if (!nearMiddle && viewOnLeft != touchOnRight && mTranslationAnimator == null) { |
| mTranslationAnimator = animate().translationX(translateXTarget); |
| mTranslationAnimator.setListener(mTranslationAnimatorListener); |
| mTranslationAnimator.start(); |
| } |
| mLastCropPosition = boundaryPosition; |
| setTranslationY(boundaryPositionPx - getHeight() / 2); |
| invalidate(); |
| } |
| |
| @Override |
| public void onCropDragComplete() { |
| animate().alpha(0).translationX((getParentWidth() - getWidth()) / 2).scaleX(0.2f) |
| .scaleY(0.2f).withEndAction(() -> setVisibility(View.INVISIBLE)).start(); |
| } |
| |
| 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(); |
| } |
| } |