Rename RippleOldDrawable to RippleDrawable
This commit is contained in:
1 parent
111cee1299
commit
795682f31f
2 files changed
+10
-10
No files matched your search
@@ -0,0 +1,579 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* 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
|
||||
* 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.hippo.effect.ripple;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.hippo.widget.Hotspotable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Drawable that shows a ripple effect in response to state changes. The
|
||||
* anchoring position of the ripple for a given state may be specified by
|
||||
* calling {@link #setHotspot(float, float)} with the corresponding state
|
||||
* attribute identifier.
|
||||
* <p>
|
||||
* A touch feedback drawable may contain multiple child layers, including a
|
||||
* special mask layer that is not drawn to the screen. A single layer may be set
|
||||
* as the mask by specifying its android:id value as {@link android.R.id#mask}.
|
||||
* <pre>
|
||||
* <code><!-- A red ripple masked against an opaque rectangle. --/>
|
||||
* <ripple android:color="#ffff0000">
|
||||
* <item android:id="@android:id/mask"
|
||||
* android:drawable="@android:color/white" />
|
||||
* <ripple /></code>
|
||||
* </pre>
|
||||
* <p>
|
||||
* If a mask layer is set, the ripple effect will be masked against that layer
|
||||
* before it is drawn over the composite of the remaining child layers.
|
||||
* <p>
|
||||
* If no mask layer is set, the ripple effect is masked against the composite
|
||||
* of the child layers.
|
||||
* <pre>
|
||||
* <code><!-- A blue ripple drawn atop a black rectangle. --/>
|
||||
* <ripple android:color="#ff00ff00">
|
||||
* <item android:drawable="@android:color/black" />
|
||||
* <ripple />
|
||||
*
|
||||
* <!-- A red ripple drawn atop a drawable resource. --/>
|
||||
* <ripple android:color="#ff00ff00">
|
||||
* <item android:drawable="@drawable/my_drawable" />
|
||||
* <ripple /></code>
|
||||
* </pre>
|
||||
* <p>
|
||||
* If no child layers or mask is specified and the ripple is set as a View
|
||||
* background, the ripple will be drawn atop the first available parent
|
||||
* background within the View's hierarchy. In this case, the drawing region
|
||||
* may extend outside of the Drawable bounds.
|
||||
* <pre>
|
||||
* <code><!-- An unbounded green ripple. --/>
|
||||
* <ripple android:color="#ff0000ff" /></code>
|
||||
* </pre>
|
||||
*
|
||||
* @attr ref android.R.styleable#RippleDrawable_color
|
||||
*/
|
||||
public class RippleDrawable extends Drawable implements RippleOwner, Hotspotable {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = RippleDrawable.class.getSimpleName();
|
||||
|
||||
/** The maximum number of ripples supported. */
|
||||
private static final int MAX_RIPPLES = 10;
|
||||
|
||||
/** Current ripple effect bounds, used to constrain ripple effects. */
|
||||
private final Rect mHotspotBounds = new Rect();
|
||||
|
||||
/** The current background. May be actively animating or pending entry. */
|
||||
private RippleBackground mBackground;
|
||||
|
||||
/** Whether we expect to draw a background when visible. */
|
||||
private boolean mBackgroundActive;
|
||||
|
||||
/** The current ripple. May be actively animating or pending entry. */
|
||||
private Ripple mRipple;
|
||||
|
||||
/** Whether we expect to draw a ripple when visible. */
|
||||
private boolean mRippleActive;
|
||||
|
||||
// Hotspot coordinates that are awaiting activation.
|
||||
private float mPendingX;
|
||||
private float mPendingY;
|
||||
private boolean mHasPending;
|
||||
|
||||
/**
|
||||
* Lazily-created array of actively animating ripples. Inactive ripples are
|
||||
* pruned during draw(). The locations of these will not change.
|
||||
*/
|
||||
private Ripple[] mExitingRipples;
|
||||
private int mExitingRipplesCount = 0;
|
||||
|
||||
/** Paint used to control appearance of ripples. */
|
||||
private Paint mRipplePaint;
|
||||
|
||||
/** Target density of the display into which ripples are drawn. */
|
||||
private final float mDensity = 1.0f;
|
||||
|
||||
/** Whether bounds are being overridden. */
|
||||
private boolean mOverrideBounds;
|
||||
|
||||
/**
|
||||
* Whether the next draw MUST draw something to canvas. Used to work around
|
||||
* a bug in hardware invalidation following a render thread-accelerated
|
||||
* animation.
|
||||
*/
|
||||
private boolean mNeedsDraw;
|
||||
|
||||
private ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
|
||||
|
||||
private int mMaxRadius = RADIUS_AUTO;
|
||||
|
||||
private final Drawable mContent;
|
||||
|
||||
RippleDrawable(ColorStateList color, Drawable content) {
|
||||
setColor(color);
|
||||
|
||||
mContent = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public void jumpToCurrentState() {
|
||||
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
|
||||
super.jumpToCurrentState();
|
||||
}
|
||||
|
||||
boolean needsDraw = false;
|
||||
|
||||
if (mRipple != null) {
|
||||
mRipple.jump();
|
||||
}
|
||||
|
||||
if (mBackground != null) {
|
||||
mBackground.jump();
|
||||
}
|
||||
|
||||
needsDraw |= cancelExitingRipples();
|
||||
|
||||
mNeedsDraw = needsDraw;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
private boolean cancelExitingRipples() {
|
||||
final int count = mExitingRipplesCount;
|
||||
final Ripple[] ripples = mExitingRipples;
|
||||
for (int i = 0; i < count; i++) {
|
||||
ripples[i].cancel();
|
||||
}
|
||||
|
||||
if (ripples != null) {
|
||||
Arrays.fill(ripples, 0, count, null);
|
||||
}
|
||||
mExitingRipplesCount = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mColor.withAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlpha() {
|
||||
return Color.alpha(mColor.getDefaultColor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
//TODO Paint.setColorFilter(filter) ?
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
// Worst-case scenario.
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] stateSet) {
|
||||
boolean changed = false;
|
||||
if (mContent != null)
|
||||
changed |= mContent.setState(stateSet);
|
||||
|
||||
boolean enabled = false;
|
||||
boolean pressed = false;
|
||||
boolean focused = false;
|
||||
|
||||
for (int state : stateSet) {
|
||||
if (state == android.R.attr.state_enabled) {
|
||||
enabled = true;
|
||||
}
|
||||
if (state == android.R.attr.state_focused) {
|
||||
focused = true;
|
||||
}
|
||||
if (state == android.R.attr.state_pressed) {
|
||||
pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
setRippleActive(enabled && pressed);
|
||||
setBackgroundActive(focused || (enabled && pressed));
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private void setRippleActive(boolean active) {
|
||||
if (mRippleActive != active) {
|
||||
mRippleActive = active;
|
||||
if (active) {
|
||||
tryRippleEnter();
|
||||
} else {
|
||||
tryRippleExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackgroundActive(boolean active) {
|
||||
if (mBackgroundActive != active) {
|
||||
mBackgroundActive = active;
|
||||
if (active) {
|
||||
tryBackgroundEnter();
|
||||
} else {
|
||||
tryBackgroundExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
|
||||
if (!mOverrideBounds) {
|
||||
mHotspotBounds.set(bounds);
|
||||
onHotspotBoundsChanged();
|
||||
}
|
||||
|
||||
if (mContent != null)
|
||||
mContent.setBounds(bounds);
|
||||
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
final boolean changed = super.setVisible(visible, restart);
|
||||
|
||||
if (!visible) {
|
||||
clearHotspots();
|
||||
} else if (changed) {
|
||||
// If we just became visible, ensure the background and ripple
|
||||
// visibilities are consistent with their internal states.
|
||||
if (mRippleActive) {
|
||||
tryRippleEnter();
|
||||
}
|
||||
|
||||
if (mBackgroundActive) {
|
||||
tryBackgroundEnter();
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setColor(ColorStateList color) {
|
||||
int alpha = Color.alpha(color.getDefaultColor());
|
||||
mColor = color.withAlpha((int) (alpha * (1 - 0.001f * alpha)));
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHotspot(float x, float y) {
|
||||
if (mRipple == null || mBackground == null) {
|
||||
mPendingX = x;
|
||||
mPendingY = y;
|
||||
mHasPending = true;
|
||||
}
|
||||
|
||||
if (mRipple != null) {
|
||||
mRipple.move(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an active hotspot at the specified location.
|
||||
*/
|
||||
private void tryBackgroundEnter() {
|
||||
if (mBackground == null) {
|
||||
mBackground = new RippleBackground(this, mHotspotBounds);
|
||||
}
|
||||
|
||||
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
|
||||
mBackground.setup(mMaxRadius, color, mDensity);
|
||||
mBackground.enter();
|
||||
}
|
||||
|
||||
private void tryBackgroundExit() {
|
||||
if (mBackground != null) {
|
||||
// Don't null out the background, we need it to draw!
|
||||
mBackground.exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to start an enter animation for the active hotspot. Fails if
|
||||
* there are too many animating ripples.
|
||||
*/
|
||||
private void tryRippleEnter() {
|
||||
if (mExitingRipplesCount >= MAX_RIPPLES) {
|
||||
// This should never happen unless the user is tapping like a maniac
|
||||
// or there is a bug that's preventing ripples from being removed.
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRipple == null) {
|
||||
final float x;
|
||||
final float y;
|
||||
if (mHasPending) {
|
||||
mHasPending = false;
|
||||
x = mPendingX;
|
||||
y = mPendingY;
|
||||
} else {
|
||||
x = mHotspotBounds.exactCenterX();
|
||||
y = mHotspotBounds.exactCenterY();
|
||||
}
|
||||
mRipple = new Ripple(this, mHotspotBounds, x, y);
|
||||
}
|
||||
|
||||
final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
|
||||
mRipple.setup(mMaxRadius, color, mDensity);
|
||||
mRipple.enter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to start an exit animation for the active hotspot. Fails if
|
||||
* there is no active hotspot.
|
||||
*/
|
||||
private void tryRippleExit() {
|
||||
if (mRipple != null) {
|
||||
if (mExitingRipples == null) {
|
||||
mExitingRipples = new Ripple[MAX_RIPPLES];
|
||||
}
|
||||
mExitingRipples[mExitingRipplesCount++] = mRipple;
|
||||
mRipple.exit();
|
||||
mRipple = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels and removes the active ripple, all exiting ripples, and the
|
||||
* background. Nothing will be drawn after this method is called.
|
||||
*/
|
||||
private void clearHotspots() {
|
||||
boolean needsDraw = false;
|
||||
|
||||
if (mRipple != null) {
|
||||
mRipple.cancel();
|
||||
mRipple = null;
|
||||
}
|
||||
|
||||
if (mBackground != null) {
|
||||
mBackground.cancel();
|
||||
mBackground = null;
|
||||
}
|
||||
|
||||
needsDraw |= cancelExitingRipples();
|
||||
|
||||
mNeedsDraw = needsDraw;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHotspotBounds(int left, int top, int right, int bottom) {
|
||||
mOverrideBounds = true;
|
||||
mHotspotBounds.set(left, top, right, bottom);
|
||||
|
||||
onHotspotBoundsChanged();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void getHotspotBounds(Rect outRect) {
|
||||
outRect.set(mHotspotBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all the animating ripples that the hotspot bounds have changed.
|
||||
*/
|
||||
private void onHotspotBoundsChanged() {
|
||||
final int count = mExitingRipplesCount;
|
||||
final Ripple[] ripples = mExitingRipples;
|
||||
for (int i = 0; i < count; i++) {
|
||||
ripples[i].onHotspotBoundsChanged();
|
||||
}
|
||||
|
||||
if (mRipple != null) {
|
||||
mRipple.onHotspotBoundsChanged();
|
||||
}
|
||||
|
||||
if (mBackground != null) {
|
||||
mBackground.onHotspotBoundsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
final boolean drawNonMaskContent = mContent != null;
|
||||
final Rect bounds = getDirtyBounds();
|
||||
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
|
||||
canvas.clipRect(bounds);
|
||||
|
||||
// If we have content, draw it into a layer first.
|
||||
if (drawNonMaskContent) {
|
||||
drawContentLayer(canvas, bounds);
|
||||
}
|
||||
|
||||
// If we have a background and a non-opaque mask, draw the masking layer.
|
||||
drawBackgroundLayer(canvas, bounds);
|
||||
|
||||
// If we have ripples and a non-opaque mask, draw the masking layer.
|
||||
drawRippleLayer(canvas, bounds);
|
||||
|
||||
// If we failed to draw anything and we just canceled animations, at
|
||||
// least draw a color so that hardware invalidation works correctly.
|
||||
if (mNeedsDraw) {
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
// Request another draw so we can avoid adding a transparent layer
|
||||
// during the next display list refresh.
|
||||
invalidateSelf();
|
||||
}
|
||||
mNeedsDraw = false;
|
||||
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a ripple from the exiting ripple list.
|
||||
*
|
||||
* @param ripple the ripple to remove
|
||||
*/
|
||||
@Override
|
||||
public void removeRipple(Ripple ripple) {
|
||||
// Ripple ripple ripple ripple. Ripple ripple.
|
||||
final Ripple[] ripples = mExitingRipples;
|
||||
final int count = mExitingRipplesCount;
|
||||
final int index = getRippleIndex(ripple);
|
||||
if (index >= 0) {
|
||||
System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
|
||||
ripples[count - 1] = null;
|
||||
mExitingRipplesCount--;
|
||||
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private int getRippleIndex(Ripple ripple) {
|
||||
final Ripple[] ripples = mExitingRipples;
|
||||
final int count = mExitingRipplesCount;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (ripples[i] == ripple) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void drawContentLayer(Canvas canvas, Rect bounds) {
|
||||
mContent.draw(canvas);
|
||||
}
|
||||
|
||||
private void drawBackgroundLayer(
|
||||
Canvas canvas, Rect bounds) {
|
||||
if (mBackground != null && mBackground.shouldDraw()) {
|
||||
final float x = mHotspotBounds.exactCenterX();
|
||||
final float y = mHotspotBounds.exactCenterY();
|
||||
canvas.translate(x, y);
|
||||
mBackground.draw(canvas, getRipplePaint());
|
||||
canvas.translate(-x, -y);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawRippleLayer(Canvas canvas, Rect bounds) {
|
||||
int restoreTranslate = -1;
|
||||
|
||||
// Translate the canvas to the current hotspot bounds.
|
||||
restoreTranslate = canvas.save();
|
||||
canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
|
||||
|
||||
// Draw ripples and update the animating ripples array.
|
||||
final int count = mExitingRipplesCount;
|
||||
final Ripple[] ripples = mExitingRipples;
|
||||
for (int i = 0; i <= count; i++) {
|
||||
final Ripple ripple;
|
||||
if (i < count) {
|
||||
ripple = ripples[i];
|
||||
} else if (mRipple != null) {
|
||||
ripple = mRipple;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
ripple.draw(canvas, getRipplePaint());
|
||||
}
|
||||
|
||||
// Always restore the translation.
|
||||
if (restoreTranslate >= 0) {
|
||||
canvas.restoreToCount(restoreTranslate);
|
||||
}
|
||||
}
|
||||
|
||||
private Paint getRipplePaint() {
|
||||
if (mRipplePaint == null) {
|
||||
mRipplePaint = new Paint();
|
||||
mRipplePaint.setAntiAlias(true);
|
||||
}
|
||||
return mRipplePaint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rect getDirtyBounds() {
|
||||
return getBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum ripple radius in pixels. The default value of
|
||||
* {@link #RADIUS_AUTO} defines the radius as the distance from the center
|
||||
* of the drawable bounds (or hotspot bounds, if specified) to a corner.
|
||||
*
|
||||
* @param maxRadius the maximum ripple radius in pixels or
|
||||
* {@link #RADIUS_AUTO} to automatically determine the maximum
|
||||
* radius based on the bounds
|
||||
* @see #getMaxRadius()
|
||||
* @see #setHotspotBounds(int, int, int, int)
|
||||
* @hide
|
||||
*/
|
||||
public void setMaxRadius(int maxRadius) {
|
||||
if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
|
||||
throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
|
||||
}
|
||||
|
||||
mMaxRadius = maxRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
|
||||
* the radius is determined automatically
|
||||
* @see #setMaxRadius(int)
|
||||
* @hide
|
||||
*/
|
||||
public int getMaxRadius() {
|
||||
return mMaxRadius;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user