Update
This commit is contained in:
1 parent
d3bfd987b0
commit
c3b5313ddc
22 files changed
+1817
-203
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.effect.ripple;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.hippo.widget.Hotspotable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Drawable that shows a ripple effect in response to state changes. The
|
||||
* anchoring position of the ripple for a given state may be specified by
|
||||
* calling {@link #setHotspot(float, float)} with the corresponding state
|
||||
* attribute identifier.
|
||||
* <p>
|
||||
* A touch feedback drawable may contain multiple child layers, including a
|
||||
* special mask layer that is not drawn to the screen. A single layer may be set
|
||||
* as the mask by specifying its android:id value as {@link android.R.id#mask}.
|
||||
* <pre>
|
||||
* <code><!-- A red ripple masked against an opaque rectangle. --/>
|
||||
* <ripple android:color="#ffff0000">
|
||||
* <item android:id="@android:id/mask"
|
||||
* android:drawable="@android:color/white" />
|
||||
* <ripple /></code>
|
||||
* </pre>
|
||||
* <p>
|
||||
* If a mask layer is set, the ripple effect will be masked against that layer
|
||||
* before it is drawn over the composite of the remaining child layers.
|
||||
* <p>
|
||||
* If no mask layer is set, the ripple effect is masked against the composite
|
||||
* of the child layers.
|
||||
* <pre>
|
||||
* <code><!-- A blue ripple drawn atop a black rectangle. --/>
|
||||
* <ripple android:color="#ff00ff00">
|
||||
* <item android:drawable="@android:color/black" />
|
||||
* <ripple />
|
||||
*
|
||||
* <!-- A red ripple drawn atop a drawable resource. --/>
|
||||
* <ripple android:color="#ff00ff00">
|
||||
* <item android:drawable="@drawable/my_drawable" />
|
||||
* <ripple /></code>
|
||||
* </pre>
|
||||
* <p>
|
||||
* If no child layers or mask is specified and the ripple is set as a View
|
||||
* background, the ripple will be drawn atop the first available parent
|
||||
* background within the View's hierarchy. In this case, the drawing region
|
||||
* may extend outside of the Drawable bounds.
|
||||
* <pre>
|
||||
* <code><!-- An unbounded green ripple. --/>
|
||||
* <ripple android:color="#ff0000ff" /></code>
|
||||
* </pre>
|
||||
*
|
||||
* @attr ref android.R.styleable#RippleDrawable_color
|
||||
*/
|
||||
public class 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Parcelable> 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<Parcelable> 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<SearchHolder> {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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<Parcelable> 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<Parcelable> container) {
|
||||
super.dispatchSaveInstanceState(container);
|
||||
|
||||
mSavedInstanceState = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchRestoreInstanceState(@NotNull SparseArray<Parcelable> 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<RecyclerView.ViewHolder> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user