diff --git a/app/src/main/java/com/hippo/effect/ripple/Ripple.java b/app/src/main/java/com/hippo/effect/ripple/Ripple.java new file mode 100644 index 000000000..8ee4bfda8 --- /dev/null +++ b/app/src/main/java/com/hippo/effect/ripple/Ripple.java @@ -0,0 +1,416 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import com.hippo.util.MathUtils; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ObjectAnimator; + +/** + * 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. + */ + 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.setAutoCancel(true); + radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); + radius.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); + cX.setAutoCancel(true); + cX.setDuration(radiusDuration); + cX.setInterpolator(LINEAR_INTERPOLATOR); + cX.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); + cY.setAutoCancel(true); + cY.setDuration(radiusDuration); + cY.setInterpolator(LINEAR_INTERPOLATOR); + cY.setStartDelay(RIPPLE_ENTER_DELAY); + + 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; + } + } + + private void exitSoftware(int radiusDuration, int opacityDuration) { + final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1); + radiusAnim.setAutoCancel(true); + radiusAnim.setDuration(radiusDuration); + radiusAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); + xAnim.setAutoCancel(true); + xAnim.setDuration(radiusDuration); + xAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); + yAnim.setAutoCancel(true); + yAnim.setDuration(radiusDuration); + yAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0); + opacityAnim.setAutoCancel(true); + opacityAnim.setDuration(opacityDuration); + opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + opacityAnim.addListener(mAnimationListener); + + 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); + } + } +} diff --git a/app/src/main/java/com/hippo/effect/ripple/RippleOldBackground.java b/app/src/main/java/com/hippo/effect/ripple/RippleOldBackground.java new file mode 100644 index 000000000..6e9a3923c --- /dev/null +++ b/app/src/main/java/com/hippo/effect/ripple/RippleOldBackground.java @@ -0,0 +1,276 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import com.hippo.util.MathUtils; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ObjectAnimator; + +/** + * Draws a Material ripple. + */ +class RippleOldBackground { + 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 RippleOldBackground(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. + */ + 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.setAutoCancel(true); + outer.setDuration(outerDuration); + outer.setInterpolator(LINEAR_INTERPOLATOR); + + 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; + } + } + + 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.setAutoCancel(true); + outerOpacityAnim.setDuration(inflectionDuration); + outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + + // 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( + RippleOldBackground.this, "outerOpacity", 0); + outerFadeOutAnim.setAutoCancel(true); + outerFadeOutAnim.setDuration(outerDuration); + outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); + + mAnimOuterOpacity = outerFadeOutAnim; + + outerFadeOutAnim.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + animation.removeListener(this); + } + }); + } + } else { + outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); + outerOpacityAnim.setAutoCancel(true); + outerOpacityAnim.setDuration(opacityDuration); + } + + 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(); + } +} diff --git a/app/src/main/java/com/hippo/effect/ripple/RippleOldDrawable.java b/app/src/main/java/com/hippo/effect/ripple/RippleOldDrawable.java new file mode 100644 index 000000000..e62b856b9 --- /dev/null +++ b/app/src/main/java/com/hippo/effect/ripple/RippleOldDrawable.java @@ -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. + *
+ * 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}. + *
+ * <!-- A red ripple masked against an opaque rectangle. --/>
+ * <ripple android:color="#ffff0000">
+ * <item android:id="@android:id/mask"
+ * android:drawable="@android:color/white" />
+ * <ripple />
+ *
+ * + * 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. + *
+ * If no mask layer is set, the ripple effect is masked against the composite + * of the child layers. + *
+ * <!-- 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 />
+ *
+ * + * 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. + *
+ * <!-- An unbounded green ripple. --/>
+ * <ripple android:color="#ff0000ff" />
+ *
+ *
+ * @attr ref android.R.styleable#RippleDrawable_color
+ */
+public class RippleOldDrawable extends Drawable implements RippleOwner, Hotspotable {
+ @SuppressWarnings("unused")
+ private static final String TAG = RippleOldDrawable.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 RippleOldBackground 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;
+
+ RippleOldDrawable(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 RippleOldBackground(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;
+ }
+}
diff --git a/app/src/main/java/com/hippo/effect/ripple/RippleOwner.java b/app/src/main/java/com/hippo/effect/ripple/RippleOwner.java
new file mode 100644
index 000000000..19535d3c7
--- /dev/null
+++ b/app/src/main/java/com/hippo/effect/ripple/RippleOwner.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+
+public interface RippleOwner {
+ /**
+ * Constant for automatically determining the maximum ripple radius.
+ */
+ public static final int RADIUS_AUTO = -1;
+
+ public void removeRipple(Ripple ripple);
+
+ public void invalidateSelf();
+
+}
diff --git a/app/src/main/java/com/hippo/effect/ripple/RippleSalon.java b/app/src/main/java/com/hippo/effect/ripple/RippleSalon.java
new file mode 100644
index 000000000..066ce4f31
--- /dev/null
+++ b/app/src/main/java/com/hippo/effect/ripple/RippleSalon.java
@@ -0,0 +1,63 @@
+/*
+ * 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.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.view.View;
+
+import com.hippo.ehviewer.R;
+import com.hippo.widget.HotspotTouchHelper;
+
+
+public final class RippleSalon {
+
+ private static final boolean USE_OLD_RIPPLE =
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
+
+ private static final Drawable MASK = new ColorDrawable(Color.BLACK);
+
+ 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")
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static void addRipple(View v, ColorStateList color, Drawable content) {
+ if (USE_OLD_RIPPLE) {
+ RippleOldDrawable rippleOldDrawable = new RippleOldDrawable(color, content);
+ v.setOnTouchListener(new HotspotTouchHelper(rippleOldDrawable));
+ v.setBackgroundDrawable(rippleOldDrawable);
+ } else {
+ RippleDrawable rippleDrawable = new RippleDrawable(color, content, MASK);
+ v.setBackgroundDrawable(rippleDrawable);
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java
index c66679a91..1253c2a13 100644
--- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java
+++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/SearchFragment.java
@@ -19,64 +19,23 @@ import android.app.Activity;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.FrameLayout;
-import android.widget.TextView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.data.GalleryListUrlBuilder;
-import com.hippo.ehviewer.widget.AdvanceSearchTable;
-import com.hippo.ehviewer.widget.CategoryTable;
-import com.hippo.util.UiUtils;
-import com.hippo.util.ViewUtils;
-import com.hippo.widget.PrefixEditText;
-public class SearchFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
+public class SearchFragment extends Fragment {
private static final String VIEW_STATE_TAG = "android:view_state";
- private static final int SEARCH_TYPE_NORMAL = 0;
- private static final int SEARCH_TYPE_TAG = 1;
- private static final int SEARCH_TYPE_IMAGE = 2;
-
- private static final int TYPE_NORMAL = 0;
- private static final int TYPE_NORMAL_ADVANCE = 1;
- private static final int TYPE_TAG = 2;
- private static final int TYPE_IMAGE = 3;
-
- private static final int[] SEARCH_ITEM_COUNT_ARRAY = {
- 2, 1, 1
- };
-
private Activity mActivity;
- private int mSearchType = SEARCH_TYPE_NORMAL;
- private boolean mEnableAdvance;
-
private OnSearchListener mOnSearchListener;
private View mRootView;
- private RecyclerView mContainer;
- private View mNormalView;
- private CategoryTable mTableCategory;
- private CheckBox mCheckSpecifyAuthor;
- private PrefixEditText mTextSearch;
- private CheckBox mCheckEnableAdvance;
- private View mAdvanceView;
- private AdvanceSearchTable mTableAdvanceSearch;
-
- private LinearLayoutManager mLayoutManager;
- private SearchAdapter mAdapter;
-
- SparseArray