blob: 78737329750a8c4e07e39558053b9f078acf0db4 [file] [log] [blame]
/*
* 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();
}
}