From c3b5313ddc523c5f8e56864a8a6fea6e01ebd38b Mon Sep 17 00:00:00 2001 From: seven332 Date: Tue, 10 Feb 2015 20:24:34 +0800 Subject: [PATCH] Update --- .../java/com/hippo/effect/ripple/Ripple.java | 416 +++++++++++++ .../effect/ripple/RippleOldBackground.java | 276 +++++++++ .../effect/ripple/RippleOldDrawable.java | 579 ++++++++++++++++++ .../com/hippo/effect/ripple/RippleOwner.java | 29 + .../com/hippo/effect/ripple/RippleSalon.java | 63 ++ .../ehviewer/ui/fragment/SearchFragment.java | 198 +----- .../hippo/ehviewer/widget/SearchLayout.java | 341 +++++++++++ .../java/com/hippo/widget/CheckTextView.java | 6 +- .../main/res/drawable-hdpi/ic_fab_search.png | Bin 0 -> 427 bytes .../main/res/drawable-mdpi/ic_fab_search.png | Bin 0 -> 315 bytes .../main/res/drawable-xhdpi/ic_fab_search.png | Bin 0 -> 543 bytes .../res/drawable-xxhdpi/ic_fab_search.png | Bin 0 -> 737 bytes .../res/drawable-xxxhdpi/ic_fab_search.png | Bin 0 -> 995 bytes app/src/main/res/layout/fragment_search.xml | 10 +- app/src/main/res/layout/search_action.xml | 39 ++ app/src/main/res/layout/search_category.xml | 3 +- .../main/res/layout/widget_search_layout.xml | 34 + app/src/main/res/values-sw600dp/dimens.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 9 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 8 + 22 files changed, 1817 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/com/hippo/effect/ripple/Ripple.java create mode 100644 app/src/main/java/com/hippo/effect/ripple/RippleOldBackground.java create mode 100644 app/src/main/java/com/hippo/effect/ripple/RippleOldDrawable.java create mode 100644 app/src/main/java/com/hippo/effect/ripple/RippleOwner.java create mode 100644 app/src/main/java/com/hippo/effect/ripple/RippleSalon.java create mode 100644 app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java create mode 100644 app/src/main/res/drawable-hdpi/ic_fab_search.png create mode 100644 app/src/main/res/drawable-mdpi/ic_fab_search.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fab_search.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fab_search.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_fab_search.png create mode 100644 app/src/main/res/layout/search_action.xml create mode 100644 app/src/main/res/layout/widget_search_layout.xml 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 mSavedStates; public void setOnSearchListener(OnSearchListener listener) { mOnSearchListener = listener; @@ -87,9 +46,9 @@ public class SearchFragment extends Fragment implements CompoundButton.OnChecked super.onViewStateRestored(savedInstanceState); if (savedInstanceState != null && mRootView != null) { - mSavedStates = savedInstanceState.getSparseParcelableArray(VIEW_STATE_TAG); - if (mSavedStates != null) { - mRootView.restoreHierarchyState(mSavedStates); + SparseArray savedStates = savedInstanceState.getSparseParcelableArray(VIEW_STATE_TAG); + if (savedStates != null) { + mRootView.restoreHierarchyState(savedStates); } } } @@ -100,14 +59,6 @@ public class SearchFragment extends Fragment implements CompoundButton.OnChecked mRootView = inflater.inflate(R.layout.fragment_search, container, false); - mContainer = (RecyclerView) mRootView.findViewById(R.id.search_container); - - mLayoutManager = new LinearLayoutManager(mActivity); - mAdapter = new SearchAdapter(); - mContainer.setLayoutManager(mLayoutManager); - mContainer.setAdapter(mAdapter); - mContainer.setHasFixedSize(true); - return mRootView; } @@ -116,147 +67,6 @@ public class SearchFragment extends Fragment implements CompoundButton.OnChecked super.onDestroyView(); mRootView = null; - mSavedStates = null; - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (buttonView == mCheckSpecifyAuthor) { - mTextSearch.setPrefix(isChecked ? "uploader:" : null); - - } else if (buttonView == mCheckEnableAdvance) { - mEnableAdvance = isChecked; - if (mSearchType == SEARCH_TYPE_NORMAL) { - if (isChecked) { - mAdapter.notifyItemInserted(1); - } else { - mAdapter.notifyItemRemoved(1); - } - } - } - } - - private class SearchHolder extends RecyclerView.ViewHolder { - - public TextView title; - public FrameLayout content; - - public SearchHolder(View itemView) { - super(itemView); - - title = (TextView) itemView.findViewById(R.id.category_title); - content = (FrameLayout) itemView.findViewById(R.id.category_content); - } - } - - private class SearchAdapter extends RecyclerView.Adapter { - - LayoutInflater mInflater; - - public SearchAdapter() { - mInflater = mActivity.getLayoutInflater(); - } - - @Override - public int getItemCount() { - int count = SEARCH_ITEM_COUNT_ARRAY[mSearchType]; - if (mSearchType == SEARCH_TYPE_NORMAL && !mEnableAdvance) { - count--; - } - return count; - } - - @Override - public int getItemViewType(int position) { - // Each item is different - int type = 0; - for (int i = 0; i < mSearchType; i++) { - type += SEARCH_ITEM_COUNT_ARRAY[i]; - } - type += position; - - return type; - } - - @Override - public SearchHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = mInflater.inflate(R.layout.search_category, parent, false); - - ViewCompat.setElevation(view, UiUtils.dp2pix(4)); // TODO - - SearchHolder holder = new SearchHolder(view); - - switch (viewType) { - case TYPE_NORMAL:{ - bindNormalView(holder); - break; - } - case TYPE_NORMAL_ADVANCE: { - bindAdvanceView(holder); - break; - } - case TYPE_TAG: { - bindTagView(holder); - break; - } - case TYPE_IMAGE: { - bindImageView(holder); - break; - } - } - - if (mSavedStates != null) { - holder.itemView.restoreHierarchyState(mSavedStates); - } - - return holder; - } - - @Override - public void onBindViewHolder(SearchHolder holder, int position) { - // Empty, bind view in create view - } - - private void bindNormalView(SearchHolder holder) { - holder.title.setText(R.string.search_normal); - - if (mNormalView == null) { - mInflater.inflate(R.layout.search_normal, holder.content); - mNormalView = holder.content.getChildAt(0); - mTableCategory = (CategoryTable) mNormalView.findViewById(R.id.search_category_table); - mCheckSpecifyAuthor = (CheckBox) mNormalView.findViewById(R.id.search_specify_author); - mTextSearch = (PrefixEditText) mNormalView.findViewById(R.id.search_text); - mCheckEnableAdvance = (CheckBox) mNormalView.findViewById(R.id.search_enable_advance); - - mCheckSpecifyAuthor.setOnCheckedChangeListener(SearchFragment.this); - mCheckEnableAdvance.setOnCheckedChangeListener(SearchFragment.this); - } else { - ViewUtils.removeFromParent(mNormalView); - holder.content.addView(mNormalView); - } - - } - - private void bindAdvanceView(SearchHolder holder) { - holder.title.setText(R.string.search_advance); - - if (mAdvanceView == null) { - mInflater.inflate(R.layout.search_advance, holder.content); - mAdvanceView = holder.content.getChildAt(0); - mTableAdvanceSearch = (AdvanceSearchTable) mAdvanceView.findViewById(R.id.search_advance_search_table); - } else { - ViewUtils.removeFromParent(mAdvanceView); - holder.content.addView(mAdvanceView); - } - } - - private void bindTagView(SearchHolder holder) { - holder.title.setText(R.string.search_tag); - } - - private void bindImageView(SearchHolder holder) { - holder.title.setText(R.string.search_image); - } } public interface OnSearchListener { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java new file mode 100644 index 000000000..f2f2f8cc1 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java @@ -0,0 +1,341 @@ +/* + * 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.ehviewer.widget; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +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.effect.ripple.RippleSalon; +import com.hippo.ehviewer.R; +import com.hippo.util.UiUtils; +import com.hippo.util.ViewUtils; +import com.hippo.widget.FloatingActionButton; +import com.hippo.widget.PrefixEditText; +import com.hippo.widget.recyclerview.EasyRecyclerView; + +import org.jetbrains.annotations.NotNull; + +public class SearchLayout extends FrameLayout implements CompoundButton.OnCheckedChangeListener, + View.OnClickListener { + + private static final String STATE_KEY_SUPER = "super"; + private static final String STATE_KEY_SEARCH_TYPE = "search_type"; + private static final String STATE_KEY_ENABLE_ADVANCE = "enable_advance"; + + 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 ITEM_TYPE_NORMAL = 0; + private static final int ITEM_TYPE_NORMAL_ADVANCE = 1; + private static final int ITEM_TYPE_TAG = 2; + private static final int ITEM_TYPE_IMAGE = 3; + private static final int ITEM_TYPE_ACTION = 4; + + private static final int[] SEARCH_ITEM_COUNT_ARRAY = { + 3, 2, 2 + }; + + private static final int[][] SEARCH_ITEM_TYPE = { + {ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION}, // SEARCH_TYPE_NORMAL + {ITEM_TYPE_TAG, ITEM_TYPE_ACTION}, // SEARCH_TYPE_TAG + {ITEM_TYPE_IMAGE, ITEM_TYPE_ACTION} // SEARCH_TYPE_IMAGE + }; + + private Context mContext; + + private int mSearchType = SEARCH_TYPE_NORMAL; + private boolean mEnableAdvance = false; + + private EasyRecyclerView mSearchContainer; + private FloatingActionButton mFab; + + private View mNormalView; + private CategoryTable mTableCategory; + private CheckBox mCheckSpecifyAuthor; + private PrefixEditText mTextSearch; + private CheckBox mCheckEnableAdvance; + + private View mAdvanceView; + private AdvanceSearchTable mTableAdvanceSearch; + + private View mActionView; + private TextView mAction1; + private TextView mAction2; + + private LinearLayoutManager mLayoutManager; + private SearchAdapter mAdapter; + + private SparseArray mSavedInstanceState; + + public SearchLayout(Context context) { + super(context); + init(context); + } + + public SearchLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public SearchLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mContext = context; + + LayoutInflater.from(context).inflate(R.layout.widget_search_layout, this); + + mSearchContainer = (EasyRecyclerView) getChildAt(0); + mFab = (FloatingActionButton) getChildAt(1); + + mLayoutManager = new LinearLayoutManager(mContext); + mAdapter = new SearchAdapter(); + mSearchContainer.setLayoutManager(mLayoutManager); + mSearchContainer.setAdapter(mAdapter); + mSearchContainer.setHasFixedSize(true); + } + + @Override + protected void dispatchSaveInstanceState(@NotNull SparseArray container) { + super.dispatchSaveInstanceState(container); + + mSavedInstanceState = null; + } + + @Override + protected void dispatchRestoreInstanceState(@NotNull SparseArray container) { + super.dispatchRestoreInstanceState(container); + + mSavedInstanceState = container; + } + + @Override + public Parcelable onSaveInstanceState() { + final Bundle state = new Bundle(); + state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState()); + state.putInt(STATE_KEY_SEARCH_TYPE, mSearchType); + state.putBoolean(STATE_KEY_ENABLE_ADVANCE, mEnableAdvance); + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + final Bundle savedState = (Bundle) state; + super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER)); + mSearchType = savedState.getInt(STATE_KEY_SEARCH_TYPE); + mEnableAdvance = savedState.getBoolean(STATE_KEY_ENABLE_ADVANCE); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView == mCheckSpecifyAuthor) { + mTextSearch.setPrefix(isChecked ? "uploader:" : null); + + } else if (buttonView == mCheckEnableAdvance) { + mEnableAdvance = isChecked; + if (mSearchType == SEARCH_TYPE_NORMAL) { + if (isChecked) { + mAdapter.notifyItemInserted(1); + } else { + mAdapter.notifyItemRemoved(1); + } + } + } + } + + @Override + public void onClick(View v) { + + } + + private class SearchHolder extends RecyclerView.ViewHolder { + + public TextView title; + public FrameLayout content; + + public SearchHolder(View itemView) { + super(itemView); + + title = (TextView) itemView.findViewById(R.id.category_title); + content = (FrameLayout) itemView.findViewById(R.id.category_content); + } + } + + private class ActionHolder extends RecyclerView.ViewHolder { + + private TextView action1; + private TextView action2; + + public ActionHolder(View itemView) { + super(itemView); + + action1 = (TextView) itemView.findViewById(R.id.search_action_1); + action2 = (TextView) itemView.findViewById(R.id.search_action_2); + } + } + + private class SearchAdapter extends EasyRecyclerView.Adapter { + + LayoutInflater mInflater; + + public SearchAdapter() { + mInflater = LayoutInflater.from(mContext); + } + + @Override + public int getItemCount() { + int count = SEARCH_ITEM_COUNT_ARRAY[mSearchType]; + if (mSearchType == SEARCH_TYPE_NORMAL && !mEnableAdvance) { + count--; + } + return count; + } + + @Override + public int getItemViewType(int position) { + int type = SEARCH_ITEM_TYPE[mSearchType][position]; + if (mSearchType == SEARCH_TYPE_NORMAL && position == 1 && !mEnableAdvance) { + type = ITEM_TYPE_ACTION; + } + return type; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + RecyclerView.ViewHolder holder; + + if (viewType == ITEM_TYPE_ACTION) { + View view = mInflater.inflate(R.layout.search_action, parent, false); + holder = new ActionHolder(view); + bindActionView((ActionHolder) holder); + } else { + View view = mInflater.inflate(R.layout.search_category, parent, false); + ViewCompat.setElevation(view, UiUtils.dp2pix(4)); // TODO + holder = new SearchHolder(view); + switch (viewType) { + case ITEM_TYPE_NORMAL:{ + bindNormalView((SearchHolder) holder); + break; + } + case ITEM_TYPE_NORMAL_ADVANCE: { + bindAdvanceView((SearchHolder) holder); + break; + } + case ITEM_TYPE_TAG: { + bindTagView((SearchHolder) holder); + break; + } + case ITEM_TYPE_IMAGE: { + bindImageView((SearchHolder) holder); + break; + } + } + } + + return holder; + } + + private void bindNormalView(SearchHolder holder) { + holder.title.setText(R.string.search_normal); + + if (mNormalView == null) { + mInflater.inflate(R.layout.search_normal, holder.content); + mNormalView = holder.content.getChildAt(0); + mTableCategory = (CategoryTable) mNormalView.findViewById(R.id.search_category_table); + mCheckSpecifyAuthor = (CheckBox) mNormalView.findViewById(R.id.search_specify_author); + mTextSearch = (PrefixEditText) mNormalView.findViewById(R.id.search_text); + mCheckEnableAdvance = (CheckBox) mNormalView.findViewById(R.id.search_enable_advance); + + // Restore state + if (mSavedInstanceState != null) { + holder.content.restoreHierarchyState(mSavedInstanceState); + } + + mCheckSpecifyAuthor.setOnCheckedChangeListener(SearchLayout.this); + mCheckEnableAdvance.setOnCheckedChangeListener(SearchLayout.this); + } else { + ViewUtils.removeFromParent(mNormalView); + holder.content.removeAllViews(); + holder.content.addView(mNormalView); + } + + } + + private void bindAdvanceView(SearchHolder holder) { + holder.title.setText(R.string.search_advance); + + if (mAdvanceView == null) { + mInflater.inflate(R.layout.search_advance, holder.content); + mAdvanceView = holder.content.getChildAt(0); + mTableAdvanceSearch = (AdvanceSearchTable) mAdvanceView.findViewById(R.id.search_advance_search_table); + + // Restore state + if (mSavedInstanceState != null) { + holder.content.restoreHierarchyState(mSavedInstanceState); + } + + } else { + ViewUtils.removeFromParent(mAdvanceView); + holder.content.addView(mAdvanceView); + } + } + + private void bindTagView(SearchHolder holder) { + holder.title.setText(R.string.search_tag); + } + + private void bindImageView(SearchHolder holder) { + holder.title.setText(R.string.search_image); + } + + private void bindActionView(ActionHolder holder) { + mActionView = holder.itemView; + mAction1 = holder.action1; + mAction2 = holder.action2; + + mAction1.setText(mContext.getString(R.string.search_add)); + mAction2.setText(mContext.getString(R.string.search_mode)); + RippleSalon.addRipple(mAction1, false); + RippleSalon.addRipple(mAction2, false); + + mAction1.setOnClickListener(SearchLayout.this); + mAction2.setOnClickListener(SearchLayout.this); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + // Empty, bind view in create view + } + } +} diff --git a/app/src/main/java/com/hippo/widget/CheckTextView.java b/app/src/main/java/com/hippo/widget/CheckTextView.java index a2c40c005..8a9ad5fa6 100644 --- a/app/src/main/java/com/hippo/widget/CheckTextView.java +++ b/app/src/main/java/com/hippo/widget/CheckTextView.java @@ -29,6 +29,8 @@ import com.hippo.util.MathUtils; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ObjectAnimator; +import org.jetbrains.annotations.NotNull; + public class CheckTextView extends TextView implements OnClickListener, Hotspotable { private static final String STATE_KEY_SUPER = "super"; @@ -78,12 +80,14 @@ public class CheckTextView extends TextView implements OnClickListener, Hotspota mMaxRadius = MathUtils.coverageRadius(getWidth(), getHeight(), x, y); } + @SuppressWarnings("UnusedDeclaration") public void setRadius(float radius) { float bigger = Math.max(mRadius, radius); mRadius = radius; invalidate((int) (mX - bigger), (int) (mY - bigger), (int) (mX + bigger), (int) (mY + bigger)); } + @SuppressWarnings("UnusedDeclaration") public float getRadius() { return mRadius; } @@ -135,7 +139,7 @@ public class CheckTextView extends TextView implements OnClickListener, Hotspota } @Override - protected void onDraw(Canvas canvas) { + protected void onDraw(@NotNull Canvas canvas) { super.onDraw(canvas); if (mAimating) { diff --git a/app/src/main/res/drawable-hdpi/ic_fab_search.png b/app/src/main/res/drawable-hdpi/ic_fab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..708a9c21280629ff93323a0e280c6347b2ed45aa GIT binary patch literal 427 zcmV;c0aX5pP)wDp1ij;IH8Ibrfv{&fGcI@|0Iy;G%Tv^6 z23ss<)*Kujl~h12@{9-wv+b#t0!AP!FM;bNT3@CD)#!sf;u~G8K|9BPHo(IqygtG% zJ4qf>f%evOPjDo8D$tuarS+*mw#vu^XWz=_zumHGi9B@JEDw3CoUKis+E-JS59O$3 za>4*y%No)K4LqXI0UdQm2iWDk7mPDm^9Es1f&EYuTMI+a)Lh5djW^O0Zjlr+4_FC3 z^%26BV>c7 z8?Z@6NH>8E=m_0_Za^K$mfR>naFj>Yo@7}vmd`Oh(sh4Gky0ALLi$7mo51=bg?liU z%2osk=Z}^&|M%p0 z=a&%j8O!>$Bd)1OzErUi)@yS|?hL^zl*M}0%C#<`^I?wl8_z<(0WTCWW1o7ic?Q-z zha%8nS>IT9zzn0@@tau!TO`}`#&;Tu095ZWU>c%`MIaQ)SOl=D#T%w)^H`P&u+CEg zre#hTO`&`oFpERJpjwpbaP#C)&DaO+9H79V!`GgiYM=qm03Ny*YB2g2$2`G86QGuE z^fK2I+VYU{VXZ$=fLTazS0AIT&VaBPfl`ggOcE+=Y|7qrBDRw{Ujo&}eCJ1h{+83( zZOh4QDNK)a!CGp7MCAyHXW)9qfI>1V397v@EKr<{3OaIX6$#=&L~ZsVSJyX1JN+c4AL?RxESDuL<_54vfkCyqM&-Mbq6Lq#XOi1ic h`Gkaoe}=mN0|4!?A&0w={F(p&002ovPDHLkV1kE@?S23N literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_fab_search.png b/app/src/main/res/drawable-xxhdpi/ic_fab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..811d639d1b342c2379d19337830f27cd9889a4cd GIT binary patch literal 737 zcmV<70v`Q|P)w-s zkP+wx=>~LysuQFeq#KY-KsJC*kZeHDsw?pWYy-wV$a~T$k&@W(`Fv-eNfbd41VIo4 zK~Rt4r5bMAR?y3yY)6(V>(iC2A-j-$$8p?jnmB>U$WHBam1OxA!KCWuT&b3@w*u#* zT}~%8T~`T4kX7ySl6|Crqt`72qJD`m1b57+Z%aW>pneMeoq$=KWSZZUwDk(CtjO34?1c_F!6K^ zLjk+wyfCad#6S*HY-paJJ&w~w11|{;Xh=UE0qOQ%;Q$#j6z@$V0}qNG9*`l0C-5M> zd;-$-)r1B#6l-=K0qrzi-UM|s!#+%Sx`rSNMOzQLXg(Vt!8Vi1g;>ZEsG$zU zXE}{Z&Dx}^yQ&*qde$tmo#*JUY-)Sc zvW>U@w%Hy-TVxnt$~jLrn_%kd$&PBRki^VK?ra{(OF|r6I^Qoj_9p2yhY?E*{*XG^ zXbz}aou-PO>D+DtQs;&W%hX+|$7<4<+a2eSVvoR*o~mFow%Da^{%G;lS8Dq!dPIfr zBtQZrKmsH{0wh2Jqzys3MUN~4q#%EcSqmh<(Le$m4J5#UAP9mW2!h<{SAYQkq%@eh T^;v|K00000NkvXXu0mjff7?TD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fab_search.png b/app/src/main/res/drawable-xxxhdpi/ic_fab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..0b20ec928f9f71a651e128822ac4dccadf80f629 GIT binary patch literal 995 zcmV<9104K`P)7=}W9l&Za}mebeYaiVrmty68Ers)L|@{OLWwo$Dgrn6J6f>q57 zUy{M*;H=tOwG5Vi?D2CM{*e%fh#TXt$SBXI8HImk!)H(QdPKv0Wusa<$YK3z#y=18 zsH2|;P)*;cW?PAzsJ=ha_j{TfjBTM>p_wP>*s6KMGUl9blXiM4A9KdFK1YwXjD#vy z0d^LKuO?nIw-lm`&G;*(0X8ltOcsNFAI*e~yztB@3(FZ(ASW6&ua8DNI zt@VB(eKm=KozFhLS^SO3!1Y_iLRXq@)yKub{fH9a-DKfQP|*I-{5UI2im+5t0D9knMyGTU~4d=r~HUzKTX^4l%m`644FBiy#CrT+`VL6hIdRVWc3# zaLxq^kWGw`pi+=T0dyXIj7flnB+E-*>#-5wG|}6nd4F&zNGCukOaPPqbRh|FFbMlk zK?3|TC@%IP3DCK?=_lp?+#m&so~S(>)C?bV z?Rt1Fo1qUC^JGpLlK?sM=A9j$vpfR3Nn(RhbvQYsG2w7Vo5TvEDtZ(VHHldK@c&D? z#*`D7;`V~3jzt)6&_#N=&>BsaxL@AxlK;!k> z(4`>bZZ*I2_jqk|FZ!UI?c$X*w){H`XgF@PF^Z5Go+WTee_!V_<|^@ta1#y9%!dE= zok7fN$0@>%5cLcmzxv5&7L&DjMYu^#6&c3n05J7UPMy>|EzMNw$ z4UczogU+U8Bx5$rd4<;!HP*;$k&Yk&M1Tko0YC(Z03ZTH01yEp0EiHY2;A$u!PJkT z1-OrdcYvdV<@`lL4Qu%m0mk - - - + android:paddingBottom="@dimen/search_padding_bottom"/> diff --git a/app/src/main/res/layout/search_action.xml b/app/src/main/res/layout/search_action.xml new file mode 100644 index 000000000..10ab0ddc6 --- /dev/null +++ b/app/src/main/res/layout/search_action.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/search_category.xml b/app/src/main/res/layout/search_category.xml index c880298e5..53716401a 100644 --- a/app/src/main/res/layout/search_category.xml +++ b/app/src/main/res/layout/search_category.xml @@ -39,6 +39,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="@dimen/search_category_widget_margin_start" - android:paddingRight="@dimen/search_category_widget_margin_start"/> + android:paddingRight="@dimen/search_category_widget_margin_start" + android:paddingBottom="@dimen/search_category_widget_margin_start"/> diff --git a/app/src/main/res/layout/widget_search_layout.xml b/app/src/main/res/layout/widget_search_layout.xml new file mode 100644 index 000000000..70878eb76 --- /dev/null +++ b/app/src/main/res/layout/widget_search_layout.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index efee43af8..d548109e5 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -20,4 +20,7 @@ 16dp 24dp + + 24dp + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d505a5941..7dd3a2672 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -25,4 +25,13 @@ 启用高级选项 没关键字也行 + 添加 + 模式 + + + 2 星 + 3 星 + 4 星 + 5 星 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fabd68bbb..ac51978c1 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -35,4 +35,7 @@ 16dp 16dp + + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5cb4fcd61..56fa1236f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,9 @@ No keyword is OK + Add + Mode + 2 stars 3 stars diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2cb04f192..5f9d0ae4e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -42,4 +42,12 @@ 40dp + +