Move Hotspot and RippleOld to submodule

This commit is contained in:
seven332 committed 2015-06-17 15:23:19 +08:00
1 parent 23bc6870e0
commit 352b2c1834
24 files changed
+26 -1743

No files matched your search

+6
View File
@@ -4,3 +4,9 @@
[submodule "vectorold"]
path = vectorold
url = https://github.com/seven332/VectorOld
[submodule "hotspot"]
path = hotspot
url = https://github.com/seven332/Hotspot
[submodule "rippleold"]
path = rippleold
url = https://github.com/seven332/RippleOld
+2
View File
@@ -34,4 +34,6 @@ dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':unifile')
compile project(':vectorold')
compile project(':hotspot')
compile project(':rippleold')
}
-8
View File
@@ -20,14 +20,6 @@
-keepattributes SourceFile,LineNumberTable
# For ObjectAnimator
-keepclassmembers class com.hippo.effect.ripple.Ripple {
void set*(***);
*** get*();
}
-keepclassmembers class com.hippo.effect.ripple.RippleBackground {
void set*(***);
*** get*();
}
-keepclassmembers public class * extends com.hippo.vectorold.drawable.VectorDrawable.VGroup {
void set*(***);
*** get*();
@@ -1,426 +0,0 @@
/*
* Copyright (C) 2013 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.hippo.effect.ripple;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.os.Build;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.hippo.util.MathUtils;
/**
* Draws a Material ripple.
*/
class Ripple {
@SuppressWarnings("unused")
private static final String TAG = Ripple.class.getSimpleName();
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator DECEL_INTERPOLATOR = new LogInterpolator();
private static final float GLOBAL_SPEED = 1.0f;
private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
private static final long RIPPLE_ENTER_DELAY = 80;
private final RippleOwner mOwner;
/** Bounds used for computing max radius. */
private final Rect mBounds;
/** Full-opacity color for drawing this ripple. */
private int mColorOpaque;
/** Maximum ripple radius. */
private float mOuterRadius;
/** Screen density used to adjust pixel-based velocities. */
private float mDensity;
private float mStartingX;
private float mStartingY;
private float mClampedStartingX;
private float mClampedStartingY;
// Software animators.
private ObjectAnimator mAnimRadius;
private ObjectAnimator mAnimOpacity;
private ObjectAnimator mAnimX;
private ObjectAnimator mAnimY;
// Software rendering properties.
private float mOpacity = 1;
private float mOuterX;
private float mOuterY;
// Values used to tween between the start and end positions.
private float mTweenRadius = 0;
private float mTweenX = 0;
private float mTweenY = 0;
/** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius;
/** Whether we were canceled externally and should avoid self-removal. */
private boolean mCanceled;
/**
* Creates a new ripple.
*/
public Ripple(RippleOwner owner, Rect bounds, float startingX, float startingY) {
mOwner = owner;
mBounds = bounds;
mStartingX = startingX;
mStartingY = startingY;
}
public void setup(int maxRadius, int color, float density) {
mColorOpaque = color | 0xFF000000;
mOpacity = Color.alpha(color) / 2 / 255.0f;
if (maxRadius != RippleOwner.RADIUS_AUTO) {
mHasMaxRadius = true;
mOuterRadius = maxRadius;
} else {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
mOuterX = 0;
mOuterY = 0;
mDensity = density;
clampStartingPosition();
}
private void clampStartingPosition() {
final float cX = mBounds.exactCenterX();
final float cY = mBounds.exactCenterY();
final float dX = mStartingX - cX;
final float dY = mStartingY - cY;
final float r = mOuterRadius;
if (dX * dX + dY * dY > r * r) {
// Point is outside the circle, clamp to the circumference.
final double angle = Math.atan2(dY, dX);
mClampedStartingX = cX + (float) (Math.cos(angle) * r);
mClampedStartingY = cY + (float) (Math.sin(angle) * r);
} else {
mClampedStartingX = mStartingX;
mClampedStartingY = mStartingY;
}
}
public void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
clampStartingPosition();
}
}
public void setOpacity(float a) {
mOpacity = a;
invalidateSelf();
}
public float getOpacity() {
return mOpacity;
}
public void setRadiusGravity(float r) {
mTweenRadius = r;
invalidateSelf();
}
public float getRadiusGravity() {
return mTweenRadius;
}
public void setXGravity(float x) {
mTweenX = x;
invalidateSelf();
}
public float getXGravity() {
return mTweenX;
}
public void setYGravity(float y) {
mTweenY = y;
invalidateSelf();
}
public float getYGravity() {
return mTweenY;
}
/**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
return drawSoftware(c, p);
}
private boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
p.setColor(mColorOpaque);
final int alpha = MathUtils.clamp((int) (255 * mOpacity + 0.5f), 0, 255);
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
if (alpha > 0 && radius > 0) {
final float x = MathUtils.lerp(
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
final float y = MathUtils.lerp(
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
p.setAlpha(alpha);
p.setStyle(Style.FILL);
c.drawCircle(x, y, radius, p);
hasContent = true;
}
return hasContent;
}
/**
* Returns the maximum bounds of the ripple relative to the ripple center.
*/
public void getBounds(Rect bounds) {
final int outerX = (int) mOuterX;
final int outerY = (int) mOuterY;
final int r = (int) mOuterRadius + 1;
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
}
/**
* Specifies the starting position relative to the drawable bounds. No-op if
* the ripple has already entered.
*/
public void move(float x, float y) {
mStartingX = x;
mStartingY = y;
clampStartingPosition();
}
/**
* Starts the enter animation.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public void enter() {
cancel();
final int radiusDuration = (int)
(1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radius.setDuration(radiusDuration);
radius.setInterpolator(LINEAR_INTERPOLATOR);
radius.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
cX.setDuration(radiusDuration);
cX.setInterpolator(LINEAR_INTERPOLATOR);
cX.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
cY.setDuration(radiusDuration);
cY.setInterpolator(LINEAR_INTERPOLATOR);
cY.setStartDelay(RIPPLE_ENTER_DELAY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
radius.setAutoCancel(true);
cX.setAutoCancel(true);
cY.setAutoCancel(true);
}
mAnimRadius = radius;
mAnimX = cX;
mAnimY = cY;
// Enter animations always run on the UI thread, since it's unlikely
// that anything interesting is happening until the user lifts their
// finger.
radius.start();
cX.start();
cY.start();
}
/**
* Starts the exit animation.
*/
public void exit() {
cancel();
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
final float remaining;
if (mAnimRadius != null && mAnimRadius.isRunning()) {
remaining = mOuterRadius - radius;
} else {
remaining = mOuterRadius;
}
final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
+ WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
final int opacityDuration = (int) (1000 * 1 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
exitSoftware(radiusDuration, opacityDuration);
}
/**
* Jump all animations to their end state. The caller is responsible for
* removing the ripple from the list of animating ripples.
*/
public void jump() {
mCanceled = true;
endSoftwareAnimations();
mCanceled = false;
}
private void endSoftwareAnimations() {
if (mAnimRadius != null) {
mAnimRadius.end();
mAnimRadius = null;
}
if (mAnimOpacity != null) {
mAnimOpacity.end();
mAnimOpacity = null;
}
if (mAnimX != null) {
mAnimX.end();
mAnimX = null;
}
if (mAnimY != null) {
mAnimY.end();
mAnimY = null;
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void exitSoftware(int radiusDuration, int opacityDuration) {
final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radiusAnim.setDuration(radiusDuration);
radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
xAnim.setDuration(radiusDuration);
xAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
yAnim.setDuration(radiusDuration);
yAnim.setInterpolator(DECEL_INTERPOLATOR);
final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
opacityAnim.setDuration(opacityDuration);
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
opacityAnim.addListener(mAnimationListener);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
radiusAnim.setAutoCancel(true);
xAnim.setAutoCancel(true);
yAnim.setAutoCancel(true);
opacityAnim.setAutoCancel(true);
}
mAnimRadius = radiusAnim;
mAnimOpacity = opacityAnim;
mAnimX = xAnim;
mAnimY = yAnim;
radiusAnim.start();
opacityAnim.start();
xAnim.start();
yAnim.start();
}
/**
* Cancels all animations. The caller is responsible for removing
* the ripple from the list of animating ripples.
*/
public void cancel() {
mCanceled = true;
cancelSoftwareAnimations();
mCanceled = false;
}
private void cancelSoftwareAnimations() {
if (mAnimRadius != null) {
mAnimRadius.cancel();
mAnimRadius = null;
}
if (mAnimOpacity != null) {
mAnimOpacity.cancel();
mAnimOpacity = null;
}
if (mAnimX != null) {
mAnimX.cancel();
mAnimX = null;
}
if (mAnimY != null) {
mAnimY.cancel();
mAnimY = null;
}
}
private void removeSelf() {
// The owner will invalidate itself.
if (!mCanceled) {
mOwner.removeRipple(this);
}
}
private void invalidateSelf() {
mOwner.invalidateSelf();
}
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeSelf();
}
};
/**
* Interpolator with a smooth log deceleration
*/
private static final class LogInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
return 1 - (float) Math.pow(400, -input * 1.4);
}
}
}
@@ -1,289 +0,0 @@
/*
* Copyright (C) 2013 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.hippo.effect.ripple;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.os.Build;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.hippo.util.MathUtils;
/**
* Draws a Material ripple.
*/
class RippleBackground {
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final float GLOBAL_SPEED = 1.0f;
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * GLOBAL_SPEED;
private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
private final RippleOwner mOwner;
/** Bounds used for computing max radius. */
private final Rect mBounds;
/** Full-opacity color for drawing this ripple. */
private int mColorOpaque;
/** Maximum alpha value for drawing this ripple. */
private int mColorAlpha;
/** Maximum ripple radius. */
private float mOuterRadius;
/** Screen density used to adjust pixel-based velocities. */
private float mDensity;
// Software animators.
private ObjectAnimator mAnimOuterOpacity;
// Software rendering properties.
private float mOuterOpacity = 0;
private float mOuterX;
private float mOuterY;
/** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius;
/**
* Creates a new ripple.
*/
public RippleBackground(RippleOwner owner, Rect bounds) {
mOwner = owner;
mBounds = bounds;
}
public void setup(int maxRadius, int color, float density) {
mColorOpaque = color | 0xFF000000;
mColorAlpha = Color.alpha(color) / 2;
if (maxRadius != RippleOwner.RADIUS_AUTO) {
mHasMaxRadius = true;
mOuterRadius = maxRadius;
} else {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
mOuterX = 0;
mOuterY = 0;
mDensity = density;
}
public void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
final float halfWidth = mBounds.width() / 2.0f;
final float halfHeight = mBounds.height() / 2.0f;
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
}
}
public void setOuterOpacity(float a) {
mOuterOpacity = a;
invalidateSelf();
}
public float getOuterOpacity() {
return mOuterOpacity;
}
/**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
return drawSoftware(c, p);
}
public boolean shouldDraw() {
final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
return outerAlpha > 0 && mOuterRadius > 0;
}
private boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
p.setColor(mColorOpaque);
final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
if (outerAlpha > 0 && mOuterRadius > 0) {
p.setAlpha(outerAlpha);
p.setStyle(Style.FILL);
c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
hasContent = true;
}
return hasContent;
}
/**
* Returns the maximum bounds of the ripple relative to the ripple center.
*/
public void getBounds(Rect bounds) {
final int outerX = (int) mOuterX;
final int outerY = (int) mOuterY;
final int r = (int) mOuterRadius + 1;
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
}
/**
* Starts the enter animation.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public void enter() {
cancel();
final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY);
final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
outer.setDuration(outerDuration);
outer.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outer.setAutoCancel(true);
}
mAnimOuterOpacity = outer;
// Enter animations always run on the UI thread, since it's unlikely
// that anything interesting is happening until the user lifts their
// finger.
outer.start();
}
/**
* Starts the exit animation.
*/
public void exit() {
cancel();
// Scale the outer max opacity and opacity velocity based
// on the size of the outer radius.
final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
final float outerSizeInfluence = MathUtils.clamp(
(mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
/ (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
// Determine at what time the inner and outer opacity intersect.
// inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
// outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
/ (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
final int inflectionOpacity = (int) (mColorAlpha * (mOuterOpacity
+ inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
}
/**
* Jump all animations to their end state. The caller is responsible for
* removing the ripple from the list of animating ripples.
*/
public void jump() {
endSoftwareAnimations();
}
private void endSoftwareAnimations() {
if (mAnimOuterOpacity != null) {
mAnimOuterOpacity.end();
mAnimOuterOpacity = null;
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
final ObjectAnimator outerOpacityAnim;
if (inflectionDuration > 0) {
// Outer opacity continues to increase for a bit.
outerOpacityAnim = ObjectAnimator.ofFloat(this,
"outerOpacity", inflectionOpacity / 255.0f);
outerOpacityAnim.setDuration(inflectionDuration);
outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerOpacityAnim.setAutoCancel(true);
}
// Chain the outer opacity exit animation.
final int outerDuration = opacityDuration - inflectionDuration;
if (outerDuration > 0) {
outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
RippleBackground.this, "outerOpacity", 0);
outerFadeOutAnim.setDuration(outerDuration);
outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerFadeOutAnim.setAutoCancel(true);
}
mAnimOuterOpacity = outerFadeOutAnim;
outerFadeOutAnim.start();
}
@Override
public void onAnimationCancel(Animator animation) {
animation.removeListener(this);
}
});
}
} else {
outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
outerOpacityAnim.setDuration(opacityDuration);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
outerOpacityAnim.setAutoCancel(true);
}
}
mAnimOuterOpacity = outerOpacityAnim;
outerOpacityAnim.start();
}
/**
* Cancel all animations. The caller is responsible for removing
* the ripple from the list of animating ripples.
*/
public void cancel() {
cancelSoftwareAnimations();
}
private void cancelSoftwareAnimations() {
if (mAnimOuterOpacity != null) {
mAnimOuterOpacity.cancel();
mAnimOuterOpacity = null;
}
}
private void invalidateSelf() {
mOwner.invalidateSelf();
}
}
@@ -1,580 +0,0 @@
/*
* Copyright (C) 2013 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.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>&lt!-- A red ripple masked against an opaque rectangle. --/>
* &ltripple android:color="#ffff0000">
* &ltitem android:id="@android:id/mask"
* android:drawable="@android:color/white" />
* &ltripple /></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>&lt!-- A blue ripple drawn atop a black rectangle. --/>
* &ltripple android:color="#ff00ff00">
* &ltitem android:drawable="@android:color/black" />
* &ltripple />
*
* &lt!-- A red ripple drawn atop a drawable resource. --/>
* &ltripple android:color="#ff00ff00">
* &ltitem android:drawable="@drawable/my_drawable" />
* &ltripple /></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>&lt!-- An unbounded green ripple. --/>
* &ltripple 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;
}
}
@@ -1,30 +0,0 @@
/*
* 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
* 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.hippo.effect.ripple;
public interface RippleOwner {
/**
* Constant for automatically determining the maximum ripple radius.
*/
int RADIUS_AUTO = -1;
void removeRipple(Ripple ripple);
void invalidateSelf();
}
@@ -1,74 +0,0 @@
/*
* 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
* 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.hippo.effect.ripple;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import com.hippo.ehviewer.R;
import com.hippo.widget.HotspotTouchHelper;
public final class RippleSalon {
public static void addRipple(View c, boolean dark) {
Resources resources = c.getContext().getResources();
ColorStateList color = ColorStateList.valueOf(
resources.getColor(dark ? R.color.ripple_material_dark : R.color.ripple_material_light));
addRipple(c, color);
}
public static void addRipple(View v, ColorStateList color) {
addRipple(v, color, v.getBackground());
}
@SuppressWarnings("deprecation")
public static void addRipple(View v, ColorStateList color, Drawable content) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
RippleDrawable rippleDrawable = new RippleDrawable(color, content);
v.setOnTouchListener(new HotspotTouchHelper(rippleDrawable));
v.setBackgroundDrawable(rippleDrawable);
} else {
android.graphics.drawable.RippleDrawable rippleDrawable =
new android.graphics.drawable.RippleDrawable(color, content, new ColorDrawable(Color.BLACK));
v.setBackground(rippleDrawable);
}
}
public static Drawable generateRippleDrawable(Context context, boolean dark) {
ColorStateList color = ColorStateList.valueOf(
context.getResources().getColor(dark ? R.color.ripple_material_dark : R.color.ripple_material_light));
return generateRippleDrawable(color);
}
public static Drawable generateRippleDrawable(ColorStateList color) {
return generateRippleDrawable(color, null);
}
public static Drawable generateRippleDrawable(ColorStateList color, Drawable content) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new RippleDrawable(color, content);
} else {
return new android.graphics.drawable.RippleDrawable(color, content, new ColorDrawable(Color.BLACK));
}
}
}
@@ -24,11 +24,11 @@ import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.Constants;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.util.Config;
import com.hippo.rippleold.RippleSalon;
import com.hippo.scene.Scene;
import com.hippo.util.Messenger;
@@ -29,10 +29,10 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.Constants;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.util.Config;
import com.hippo.rippleold.RippleSalon;
import com.hippo.util.FrescoUtils;
import com.hippo.util.Messenger;
import com.hippo.widget.DrawerListView;
@@ -35,9 +35,9 @@ import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.data.ListUrlBuilder;
import com.hippo.rippleold.RippleSalon;
import com.hippo.scene.SimpleDialog;
import com.hippo.util.UiUtils;
import com.hippo.util.ViewUtils;
@@ -31,8 +31,8 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.util.ViewUtils;
public class SelectSearchImageLayout extends LinearLayout implements View.OnClickListener{
@@ -26,8 +26,8 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.util.ViewUtils;
import com.hippo.widget.SimpleImageView;
@@ -22,9 +22,9 @@ import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.hippo.hotspot.HotspotTouchHelper;
import com.hippo.hotspot.Hotspotable;
import com.hippo.util.ViewUtils;
import com.hippo.widget.HotspotTouchHelper;
import com.hippo.widget.Hotspotable;
public class SceneDialogView extends SceneView implements View.OnClickListener, Hotspotable {
@@ -33,8 +33,8 @@ import android.widget.ScrollView;
import android.widget.Space;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.util.ViewUtils;
public class SimpleDialog extends SceneDialog implements View.OnClickListener,
@@ -26,8 +26,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.scene.AppbarScene;
import com.hippo.scene.SimpleCurtain;
import com.hippo.scene.StageActivity;
@@ -73,7 +73,7 @@ public class PreferenceHeaderScene extends AppbarScene {
}
}
});
mRecyclerView.setSelector(RippleSalon.generateRippleDrawable(mActivity, false));
mRecyclerView.setSelector(RippleSalon.generateRippleDrawable(false));
}
@Override
@@ -22,8 +22,8 @@ import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import com.hippo.effect.ripple.RippleSalon;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.scene.AppbarScene;
import com.hippo.util.ViewUtils;
import com.hippo.widget.recyclerview.EasyRecyclerView;
@@ -55,7 +55,7 @@ public class PreferenceScene extends AppbarScene {
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getStageActivity()));
mRecyclerView.setOnItemClickListener(new PreferenceItemClickListener());
mRecyclerView.setSelector(RippleSalon.generateRippleDrawable(getStageActivity(), false));
mRecyclerView.setSelector(RippleSalon.generateRippleDrawable(false));
mRecyclerView.setOnDrawSelectorListener(new PreferencesDrawSelectorListener());
mRecyclerView.setClipToPadding(false);
}
@@ -19,7 +19,7 @@ package com.hippo.util;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.hippo.widget.Hotspotable;
import com.hippo.hotspot.Hotspotable;
public class DrawableUtils {
@@ -33,6 +33,8 @@ import android.widget.TextView;
import com.hippo.animation.SimpleAnimatorListener;
import com.hippo.ehviewer.R;
import com.hippo.hotspot.HotspotTouchHelper;
import com.hippo.hotspot.Hotspotable;
import com.hippo.util.AnimationUtils;
import com.hippo.util.MathUtils;
@@ -1,300 +0,0 @@
/*
* 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
* 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.hippo.widget;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewConfiguration;
public class HotspotTouchHelper implements View.OnTouchListener {
private final Hotspotable mOwner;
private boolean mPrePressed = false;
private boolean mHasPerformedLongPress = false;
private CheckForLongPress mPendingCheckForLongPress = null;
private CheckForTap mPendingCheckForTap = null;
private PerformClick mPerformClick = null;
private UnsetPressedState mUnsetPressedState = null;
/**
* Cache the touch slop from the context that created the view.
*/
private int mTouchSlop = -1;
public HotspotTouchHelper(Hotspotable owner){
mOwner = owner;
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onTouch(View v, MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN ||
event.getAction() == MotionEvent.ACTION_MOVE)
mOwner.setHotspot(x, y);
if (!v.isEnabled()) {
if (event.getAction() == MotionEvent.ACTION_UP && v.isPressed()) {
v.setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (v.isClickable() || v.isLongClickable());
}
TouchDelegate touchDelegate = v.getTouchDelegate();
if (touchDelegate != null) {
if (touchDelegate.onTouchEvent(event)) {
return true;
}
}
if (v.isClickable() || v.isLongClickable()) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if (v.isPressed() || mPrePressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (v.isFocusable() && v.isFocusableInTouchMode() && !v.isFocused()) {
focusTaken = v.requestFocus();
}
if (mPrePressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(v, true, x, y);
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback(v);
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick(v);
}
if (!v.post(mPerformClick)) {
v.performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState(v);
}
if (mPrePressed) {
v.postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!v.post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback(v);
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(v, event)) {
break;
}
mPrePressed = true;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap(v);
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
v.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
v.setPressed(false);
removeTapCallback(v);
removeLongPressCallback(v);
break;
case MotionEvent.ACTION_MOVE:
mOwner.setHotspot(x, y);
if (mTouchSlop == -1)
mTouchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop();
// Be lenient about moving outside of buttons
if (!pointInView(v, x, y, mTouchSlop)) {
// Outside button
removeTapCallback(v);
if (v.isPressed()) {
// Remove any future long press/tap checks
removeLongPressCallback(v);
v.setPressed(false);
}
}
break;
}
return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setPressed(View v, boolean pressed, float x, float y){
v.setPressed(pressed);
mOwner.setHotspot(x, y);
}
/**
* Utility method to determine whether the given point, in local coordinates,
* is inside the view, where the area of the view is expanded by the slop factor.
* This method is called while processing touch-move events to determine if the event
* is still within the view.
*/
public boolean pointInView(View v, float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((v.getRight() - v.getLeft()) + slop) &&
localY < ((v.getBottom() - v.getTop()) + slop);
}
/**
* Performs button-related actions during a touch down event.
*
* @param event The event.
* @return True if the down was consumed.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected boolean performButtonActionOnTouchDown(View v, MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (v.showContextMenu()) {
return true;
}
}
}
return false;
}
/**
* Remove the longpress detection timer.
*/
private void removeLongPressCallback(View v) {
if (mPendingCheckForLongPress != null) {
v.removeCallbacks(mPendingCheckForLongPress);
}
}
/**
* Remove the tap detection timer.
*/
private void removeTapCallback(View v) {
if (mPendingCheckForTap != null) {
mPrePressed = false;
v.removeCallbacks(mPendingCheckForTap);
}
}
private void checkForLongClick(View v, int delayOffset) {
if (v.isLongClickable()) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress(v);
}
v.postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
private final class CheckForLongPress implements Runnable {
private final View mView;
public CheckForLongPress(View v) {
mView = v;
}
@Override
public void run() {
if (mView.isPressed() && (mView.getParent() != null)) {
if (mView.performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
}
private final class CheckForTap implements Runnable {
public float x;
public float y;
private final View mView;
public CheckForTap(View v) {
mView = v;
}
@Override
public void run() {
mPrePressed = false;
setPressed(mView, true, x, y);
checkForLongClick(mView, ViewConfiguration.getTapTimeout());
}
}
private final class PerformClick implements Runnable {
private final View mView;
public PerformClick(View v) {
mView = v;
}
@Override
public void run() {
mView.performClick();
}
}
private final class UnsetPressedState implements Runnable {
private final View mView;
public UnsetPressedState(View v) {
mView = v;
}
@Override
public void run() {
mView.setPressed(false);
}
}
}
@@ -1,22 +0,0 @@
/*
* 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
* 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.hippo.widget;
public interface Hotspotable {
void setHotspot(float x, float y);
}
Submodule
+1
Submodule hotspot added at 1c2b4d6160.
Submodule
+1
Submodule rippleold added at 0acfd74abd.
+1 -1
View File
@@ -1 +1 @@
include ':app', ':unifile', ':vectorold'
include ':app', ':unifile', ':vectorold', ':hotspot', ':rippleold'