travisc ab2fec6da4 Move lib/src/ to lib/java/, and lib/jvmtests/javatests/ to lib/javatests/.
Bazel is happier if Java/Java test roots are named 'java' and 'javatests', and
this will mean that once we create a BUILD file for
android/support/design/{widget,internal}/ we'll no longer need a custom package
specified in our build (which tends to cause build problems that manifest quite
weirdly). This commit doesn't attempt to refactor the build at all yet, and is
just a pure move.

PiperOrigin-RevId: 178060739
2018-01-11 10:50:18 -05:00

357 lines
11 KiB
Java

/*
* Copyright 2017 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 android.support.design.circularreveal;
import static android.support.design.math.MathUtils.DEFAULT_EPSILON;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.Rect;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.design.circularreveal.CircularRevealWidget.RevealInfo;
import android.support.design.math.MathUtils;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Helper class to implement circular reveal functionality.
*
* <p>A {@link CircularRevealWidget} subclass will call the corresponding method in this helper,
* which contains the actual implementations for circular reveal. This helper communicates back to
* the widget via the {@link #delegate}.
*/
public class CircularRevealHelper {
private static final boolean DEBUG = false;
/**
* Delegate interface to be implemented by the {@link CircularRevealWidget} that owns this helper.
*/
interface Delegate {
/**
* Calls {@link View#draw(Canvas) super#draw(Canvas)}.
*
* <p>The delegate should override {@link View#draw(Canvas)} to call the corresponding method in
* {@link CircularRevealHelper} if the helper is non-null.
*/
void actualDraw(Canvas canvas);
/**
* Calls {@link View#isOpaque() super#isOpaque()}.
*
* <p>The delegate should override {@link View#isOpaque()} to call the corresponding method in
* {@link CircularRevealHelper} if the helper is non-null.
*/
boolean actualIsOpaque();
}
/**
* Specify that this view should use a {@link BitmapShader} to create the circular reveal effect.
* BitmapShader is supported in all APIs, but has the downside that it can only animate a static
* {@link Bitmap}.
*/
public static final int BITMAP_SHADER = 0;
/**
* Specify that this view should use {@link Canvas#clipPath(Path)} to create the circular reveal
* effect. clipPath() is only hardware accelerated on {@link VERSION_CODES#JELLY_BEAN_MR2} and
* above.
*/
public static final int CLIP_PATH = 1;
/**
* Specify that this view should use {@link
* android.view.ViewAnimationUtils#createCircularReveal(View, int, int, float, float)} to create
* the circular reveal effect. This is only supported on {@link VERSION_CODES#LOLLIPOP} and above.
*/
public static final int REVEAL_ANIMATOR = 2;
/** Which strategy this view should use to create the circular reveal effect. */
@IntDef({CLIP_PATH, BITMAP_SHADER, REVEAL_ANIMATOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Strategy {}
@Strategy public static final int STRATEGY;
private final Delegate delegate;
private final View view;
private final Path revealPath;
private final Paint revealPaint;
private final Paint scrimPaint;
/**
* The circular reveal representation which affects how the current frame will be drawn.
*
* <p>A non-null RevealInfo is {@link RevealInfo#isInvalid() invalid} if a circular reveal is
* active and the circular reveal does not cover all four corners of this view. When the circular
* reveal is such that it covers the entire view, {@link RevealInfo#radius} should be set to
* {@link RevealInfo#INVALID_RADIUS}, making the RevealInfo invalid. An invalid RevealInfo is a
* optimization that allows {@link #draw(Canvas)} to use the fastest code path.
*/
@Nullable private RevealInfo revealInfo;
/** An icon to be drawn on top of the widget's contents and after the scrim color. */
@Nullable private Drawable overlayDrawable;
private Paint debugPaint;
private boolean buildingCircularRevealCache;
private boolean hasCircularRevealCache;
static {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
STRATEGY = REVEAL_ANIMATOR;
} else if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
STRATEGY = CLIP_PATH;
} else {
STRATEGY = BITMAP_SHADER;
}
}
public CircularRevealHelper(Delegate delegate) {
this.delegate = delegate;
this.view = (View) delegate;
this.view.setWillNotDraw(false);
revealPath = new Path();
revealPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
scrimPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
scrimPaint.setColor(Color.TRANSPARENT);
if (DEBUG) {
debugPaint = new Paint();
debugPaint.setStyle(Style.STROKE);
}
}
public void buildCircularRevealCache() {
if (STRATEGY == BITMAP_SHADER) {
buildingCircularRevealCache = true;
hasCircularRevealCache = false;
view.buildDrawingCache();
Bitmap bitmap = view.getDrawingCache();
if (bitmap == null && view.getWidth() != 0 && view.getHeight() != 0) {
bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
}
if (bitmap != null) {
revealPaint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
}
buildingCircularRevealCache = false;
hasCircularRevealCache = true;
}
}
public void destroyCircularRevealCache() {
if (STRATEGY == BITMAP_SHADER) {
hasCircularRevealCache = false;
view.destroyDrawingCache();
revealPaint.setShader(null);
view.invalidate();
}
}
/**
* Sets the reveal info, ensuring that a reveal circle with a large enough radius that covers the
* entire View has its {@link RevealInfo#radius} set to {@link RevealInfo#INVALID_RADIUS}.
*/
public void setRevealInfo(@Nullable RevealInfo revealInfo) {
if (revealInfo == null) {
this.revealInfo = null;
} else {
if (this.revealInfo == null) {
this.revealInfo = new RevealInfo(revealInfo);
} else {
this.revealInfo.set(revealInfo);
}
// Check if the reveal circle radius covers all four corners.
if (MathUtils.geq(
revealInfo.radius, getDistanceToFurthestCorner(revealInfo), DEFAULT_EPSILON)) {
this.revealInfo.radius = RevealInfo.INVALID_RADIUS;
}
}
invalidateRevealInfo();
}
@Nullable
public RevealInfo getRevealInfo() {
if (revealInfo == null) {
return null;
}
RevealInfo revealInfo = new RevealInfo(this.revealInfo);
if (revealInfo.isInvalid()) {
revealInfo.radius = getDistanceToFurthestCorner(revealInfo);
}
return revealInfo;
}
public void setCircularRevealScrimColor(@ColorInt int color) {
scrimPaint.setColor(color);
view.invalidate();
}
@ColorInt
public int getCircularRevealScrimColor() {
return scrimPaint.getColor();
}
@Nullable
public Drawable getCircularRevealOverlayDrawable() {
return overlayDrawable;
}
public void setCircularRevealOverlayDrawable(@Nullable Drawable drawable) {
overlayDrawable = drawable;
view.invalidate();
}
private void invalidateRevealInfo() {
if (STRATEGY == CLIP_PATH) {
revealPath.rewind();
if (revealInfo != null) {
revealPath.addCircle(
revealInfo.centerX, revealInfo.centerY, revealInfo.radius, Direction.CW);
}
}
view.invalidate();
}
private float getDistanceToFurthestCorner(RevealInfo revealInfo) {
return MathUtils.distanceToFurthestCorner(
revealInfo.centerX, revealInfo.centerY, 0, 0, view.getWidth(), view.getHeight());
}
public void draw(Canvas canvas) {
if (DEBUG) {
drawDebugMode(canvas);
return;
}
if (shouldDrawCircularReveal()) {
switch (STRATEGY) {
case REVEAL_ANIMATOR:
delegate.actualDraw(canvas);
if (shouldDrawScrim()) {
canvas.drawRect(0, 0, view.getWidth(), view.getHeight(), scrimPaint);
}
break;
case CLIP_PATH:
int count = canvas.save();
canvas.clipPath(revealPath);
delegate.actualDraw(canvas);
if (shouldDrawScrim()) {
canvas.drawRect(0, 0, view.getWidth(), view.getHeight(), scrimPaint);
}
canvas.restoreToCount(count);
break;
case BITMAP_SHADER:
canvas.drawCircle(revealInfo.centerX, revealInfo.centerY, revealInfo.radius, revealPaint);
if (shouldDrawScrim()) {
canvas.drawCircle(
revealInfo.centerX, revealInfo.centerY, revealInfo.radius, scrimPaint);
}
break;
default:
throw new IllegalStateException("Unsupported strategy " + STRATEGY);
}
} else {
delegate.actualDraw(canvas);
if (shouldDrawScrim()) {
canvas.drawRect(0, 0, view.getWidth(), view.getHeight(), scrimPaint);
}
}
drawOverlayDrawable(canvas);
}
private void drawOverlayDrawable(Canvas canvas) {
if (shouldDrawOverlayDrawable()) {
Rect bounds = overlayDrawable.getBounds();
float translationX = revealInfo.centerX - bounds.width() / 2f;
float translationY = revealInfo.centerY - bounds.height() / 2f;
canvas.translate(translationX, translationY);
overlayDrawable.draw(canvas);
canvas.translate(-translationX, -translationY);
}
}
public boolean isOpaque() {
return delegate.actualIsOpaque() && !shouldDrawCircularReveal();
}
private boolean shouldDrawCircularReveal() {
boolean invalidRevealInfo = revealInfo == null || revealInfo.isInvalid();
if (STRATEGY == BITMAP_SHADER) {
return !invalidRevealInfo && hasCircularRevealCache;
} else {
return !invalidRevealInfo;
}
}
private boolean shouldDrawScrim() {
return !buildingCircularRevealCache && Color.alpha(scrimPaint.getColor()) != 0;
}
private boolean shouldDrawOverlayDrawable() {
return !buildingCircularRevealCache && overlayDrawable != null && revealInfo != null;
}
private void drawDebugMode(Canvas canvas) {
delegate.actualDraw(canvas);
if (shouldDrawScrim()) {
canvas.drawCircle(revealInfo.centerX, revealInfo.centerY, revealInfo.radius, scrimPaint);
}
// Instead of using a circular mask, draw a circle representing that mask instead.
if (shouldDrawCircularReveal()) {
drawDebugCircle(canvas, Color.BLACK, 10f);
drawDebugCircle(canvas, Color.RED, 5f);
}
drawOverlayDrawable(canvas);
}
private void drawDebugCircle(Canvas canvas, int color, float width) {
debugPaint.setColor(color);
debugPaint.setStrokeWidth(width);
canvas.drawCircle(
revealInfo.centerX, revealInfo.centerY, revealInfo.radius - width / 2, debugPaint);
}
}