diff --git a/app/src/main/java/com/hippo/util/MathUtils.java b/app/src/main/java/com/hippo/util/MathUtils.java index b2cdff714..17320cfa1 100644 --- a/app/src/main/java/com/hippo/util/MathUtils.java +++ b/app/src/main/java/com/hippo/util/MathUtils.java @@ -278,6 +278,31 @@ public final class MathUtils { return (a >= standard && b >= standard) || (a <= standard && b <= standard); } + /** + * Get coverage radius of a area + * + * @param w + * @param h + * @param x + * @param y + * @return + */ + public static float coverageRadius(float w, float h, float x, float y) { + float x2; + float y2; + if (x > w / 2) { + x2 = 0; + } else { + x2 = w; + } + if (y > h / 2) { + y2 = 0; + } else { + y2 = h; + } + return dist(x, y, x2, y2); + } + public static int random(int howbig) { return (int) (sRandom.nextFloat() * howbig); } diff --git a/app/src/main/java/com/hippo/widget/CheckTextView.java b/app/src/main/java/com/hippo/widget/CheckTextView.java index de8758855..5425356e4 100644 --- a/app/src/main/java/com/hippo/widget/CheckTextView.java +++ b/app/src/main/java/com/hippo/widget/CheckTextView.java @@ -17,20 +17,29 @@ package com.hippo.widget; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Rect; +import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; -public class CheckTextView extends TextView implements OnClickListener{ +import com.hippo.util.MathUtils; +import com.nineoldandroids.animation.ObjectAnimator; - private static int MASK = 0x61000000; +public class CheckTextView extends TextView implements OnClickListener, Hotspotable { - private Rect mRect = new Rect(); + private static final int MASK = 0x8a000000; + private static final long ANIMATION_DURATION = 150; private boolean mChecked = false; + private float mX; + private float mY; + private float mRadius = 0f; + private float mMaxRadius; + + private Paint mPaint; + public CheckTextView(Context context) { super(context); init(); @@ -46,27 +55,54 @@ public class CheckTextView extends TextView implements OnClickListener{ } private void init() { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mPaint.setColor(MASK); + + setOnTouchListener(new HotspotTouchHelper(this)); setOnClickListener(this); } + @Override + public void setHotspot(float x, float y) { + mX = x; + mY = y; + mMaxRadius = MathUtils.coverageRadius(getWidth(), getHeight(), x, y); + } + + 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)); + } + + public float getRadius() { + return mRadius; + } + @Override public void onClick(View v) { mChecked = !mChecked; - invalidate(); - } - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - mRect.set(0, 0, right - left, bottom - top); + ObjectAnimator radiusAnim; + if (mChecked) { + radiusAnim = ObjectAnimator.ofFloat(this, "radius", + 0f, mMaxRadius); + } else { + radiusAnim = ObjectAnimator.ofFloat(this, "radius", + mMaxRadius, 0f); + } + radiusAnim.setAutoCancel(true); + radiusAnim.setDuration(ANIMATION_DURATION); // TODO decide duration according to mMaxRadius + radiusAnim.start(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mChecked) { - canvas.drawColor(MASK); + + float radius = mRadius; + if (radius > 0f) { + canvas.drawCircle(mX, mY, radius, mPaint); } } @@ -78,4 +114,5 @@ public class CheckTextView extends TextView implements OnClickListener{ public boolean isChecked() { return mChecked; } + } diff --git a/app/src/main/java/com/hippo/widget/HotspotTouchHelper.java b/app/src/main/java/com/hippo/widget/HotspotTouchHelper.java new file mode 100644 index 000000000..df71f32e8 --- /dev/null +++ b/app/src/main/java/com/hippo/widget/HotspotTouchHelper.java @@ -0,0 +1,299 @@ +/* + * 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.widget; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.MotionEvent; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewConfiguration; + +public class HotspotTouchHelper implements View.OnTouchListener { + + private final Hotspotable mOwner; + + private boolean mPrePressed = false; + private boolean mHasPerformedLongPress = false; + + private CheckForLongPress mPendingCheckForLongPress = null; + private CheckForTap mPendingCheckForTap = null; + private PerformClick mPerformClick = null; + private UnsetPressedState mUnsetPressedState = null; + + /** + * Cache the touch slop from the context that created the view. + */ + private int mTouchSlop = -1; + + public HotspotTouchHelper(Hotspotable owner){ + mOwner = owner; + } + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public boolean onTouch(View v, MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + + if (event.getAction() == MotionEvent.ACTION_DOWN || + event.getAction() == MotionEvent.ACTION_MOVE) + mOwner.setHotspot(x, y); + + if (!v.isEnabled()) { + if (event.getAction() == MotionEvent.ACTION_UP && v.isPressed()) { + v.setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (v.isClickable() || v.isLongClickable()); + } + + TouchDelegate touchDelegate = v.getTouchDelegate(); + if (touchDelegate != null) { + if (touchDelegate.onTouchEvent(event)) { + return true; + } + } + + if (v.isClickable() || v.isLongClickable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (v.isPressed() || mPrePressed) { + // take focus if we don't have it already and we should in + // touch mode. + boolean focusTaken = false; + if (v.isFocusable() && v.isFocusableInTouchMode() && !v.isFocused()) { + focusTaken = v.requestFocus(); + } + + if (mPrePressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(v, true, x, y); + } + + if (!mHasPerformedLongPress) { + // This is a tap, so remove the longpress check + removeLongPressCallback(v); + + // Only perform take click actions if we were in the pressed state + if (!focusTaken) { + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(v); + } + if (!v.post(mPerformClick)) { + v.performClick(); + } + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(v); + } + + if (mPrePressed) { + v.postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!v.post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(v); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + if (performButtonActionOnTouchDown(v, event)) { + break; + } + + mPrePressed = true; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(v); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + v.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + break; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + removeTapCallback(v); + removeLongPressCallback(v); + break; + + case MotionEvent.ACTION_MOVE: + mOwner.setHotspot(x, y); + + if (mTouchSlop == -1) + mTouchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + + // Be lenient about moving outside of buttons + if (!pointInView(v, x, y, mTouchSlop)) { + // Outside button + removeTapCallback(v); + if (v.isPressed()) { + // Remove any future long press/tap checks + removeLongPressCallback(v); + + v.setPressed(false); + } + } + break; + } + + return true; + } + + return false; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setPressed(View v, boolean pressed, float x, float y){ + v.setPressed(pressed); + mOwner.setHotspot(x, y); + } + + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public boolean pointInView(View v, float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < ((v.getRight() - v.getLeft()) + slop) && + localY < ((v.getBottom() - v.getTop()) + slop); + } + + /** + * Performs button-related actions during a touch down event. + * + * @param event The event. + * @return True if the down was consumed. + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected boolean performButtonActionOnTouchDown(View v, MotionEvent event) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { + if (v.showContextMenu()) { + return true; + } + } + } + return false; + } + + /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback(View v) { + if (mPendingCheckForLongPress != null) { + v.removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback(View v) { + if (mPendingCheckForTap != null) { + mPrePressed = false; + v.removeCallbacks(mPendingCheckForTap); + } + } + + private void checkForLongClick(View v, int delayOffset) { + if (v.isLongClickable()) { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(v); + } + v.postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); + } + } + + private final class CheckForLongPress implements Runnable { + + private final View mView; + + public CheckForLongPress(View v) { + mView = v; + } + + @Override + public void run() { + if (mView.isPressed() && (mView.getParent() != null)) { + if (mView.performLongClick()) { + mHasPerformedLongPress = true; + } + } + } + } + + private final class CheckForTap implements Runnable { + public float x; + public float y; + private final View mView; + + public CheckForTap(View v) { + mView = v; + } + + @Override + public void run() { + mPrePressed = false; + setPressed(mView, true, x, y); + checkForLongClick(mView, ViewConfiguration.getTapTimeout()); + } + } + + private final class PerformClick implements Runnable { + private final View mView; + + public PerformClick(View v) { + mView = v; + } + + @Override + public void run() { + mView.performClick(); + } + } + + private final class UnsetPressedState implements Runnable { + private final View mView; + + public UnsetPressedState(View v) { + mView = v; + } + + @Override + public void run() { + mView.setPressed(false); + } + } +} diff --git a/app/src/main/java/com/hippo/widget/Hotspotable.java b/app/src/main/java/com/hippo/widget/Hotspotable.java new file mode 100644 index 000000000..9bb6f9a38 --- /dev/null +++ b/app/src/main/java/com/hippo/widget/Hotspotable.java @@ -0,0 +1,21 @@ +/* + * 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.widget; + +public interface Hotspotable { + + public void setHotspot(float x, float y); +}