diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java index 12a7b5343..c31dc8ca0 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java +++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java @@ -24,6 +24,7 @@ import com.hippo.conaco.Conaco; import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.gallery.GallerySpider; import com.hippo.ehviewer.gallery.ImageHandler; +import com.hippo.ehviewer.gallery.ui.TouchHelper; import com.hippo.ehviewer.network.EhHttpClient; import com.hippo.ehviewer.util.DBUtils; import com.hippo.ehviewer.util.Settings; @@ -49,6 +50,7 @@ public class EhApplication extends SceneApplication { Settings.initialize(this); DBUtils.initialize(this); + TouchHelper.initialize(this); File sayFile = AppConfig.getFileInAppDir("EhViewer.log"); if (sayFile != null) { diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java index 4d78f7513..cccc4a266 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java @@ -46,8 +46,7 @@ import java.util.ArrayList; // from main thread (like a Handler) in your GLView, you need to call // lockRendering() if the rendering thread should not run at the same time. // -// TODO Add click listener -public class GLView { +public class GLView implements TouchOwner { private static final String TAG = "GLView"; private static final boolean DEBUG_DRAW_BOUNDS = false; @@ -65,7 +64,11 @@ public class GLView { public static final int NO_ID = -1; public interface OnClickListener { - void onClick(GLView v); + boolean onClick(GLView v); + } + + public interface OnLongClickListener { + boolean onLongClick(GLView v); } protected final static GLPaint mDrawBoundsPaint; @@ -124,16 +127,15 @@ public class GLView { private int mBackgroundColor = Color.TRANSPARENT; - public void startAnimation(CanvasAnimation animation) { - GLRoot root = getGLRoot(); - if (root == null) throw new IllegalStateException(); - mAnimation = animation; - if (mAnimation != null) { - mAnimation.start(); - root.registerLaunchedAnimation(mAnimation); - } - invalidate(); - } + private TouchHelper mTouchHelper; + + private float mHotspotX; + private float mHotspotY; + + private boolean mPressed; + + private OnClickListener mOnClickListener; + private OnLongClickListener mOnLongClickListener; // Sets the visiblity of this GLView (either GLView.VISIBLE or // GLView.INVISIBLE). @@ -298,10 +300,77 @@ public class GLView { return mBounds; } + @Override + public void setHotspot(float x, float y) { + mHotspotX = x; + mHotspotY = y; + } + + public float getHotspotX() { + return mHotspotX; + } + + public float getHotspotY() { + return mHotspotY; + } + + @Override + public boolean isEnabled() { + return true; // TODO + } + + @Override + public boolean isPressed() { + return mPressed; + } + + @Override + public void setPressed(boolean pressed) { + mPressed = pressed; + } + + @Override + public boolean isClickable() { + return mOnClickListener != null; + } + + @Override + public boolean isLongClickable() { + return mOnLongClickListener != null; + } + + @Override + public boolean performClick() { + if (mOnClickListener != null) { + return mOnClickListener.onClick(this); + } else { + return false; + } + } + + @Override + public boolean performLongClick() { + if (mOnLongClickListener != null) { + return mOnLongClickListener.onLongClick(this); + } else { + return false; + } + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + public void setOnLongClickListener(OnLongClickListener listener) { + mOnLongClickListener = listener; + } + + @Override public int getWidth() { return mBounds.right - mBounds.left; } + @Override public int getHeight() { return mBounds.bottom - mBounds.top; } @@ -412,7 +481,10 @@ public class GLView { } protected boolean onTouch(MotionEvent event) { - return false; + if (mTouchHelper == null) { + mTouchHelper = new TouchHelper(this); + } + return mTouchHelper.onTouch(event); } protected boolean dispatchTouchEvent(MotionEvent event, diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchHelper.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchHelper.java new file mode 100644 index 000000000..c454dd3a9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchHelper.java @@ -0,0 +1,242 @@ +/* + * Copyright 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.SimpleHandler; + +public class TouchHelper { + + private TouchOwner 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 static int sTouchSlop = 8; + + public static void initialize(Context context) { + sTouchSlop = LayoutUtils.dp2pix(context, 8); // 8dp + } + + public TouchHelper(@NonNull TouchOwner owner) { + mOwner = owner; + } + + public boolean onTouch(MotionEvent event) { + TouchOwner owner = mOwner; + final float x = event.getX(); + final float y = event.getY(); + + if (event.getAction() == MotionEvent.ACTION_DOWN || + event.getAction() == MotionEvent.ACTION_MOVE) + owner.setHotspot(x, y); + + if (!owner.isEnabled()) { + if (event.getAction() == MotionEvent.ACTION_UP && owner.isPressed()) { + owner.setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (owner.isClickable() || owner.isLongClickable()); + } + + if (owner.isClickable() || owner.isLongClickable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (owner.isPressed() || mPrePressed) { + 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(true, x, y); + } + + if (!mHasPerformedLongPress) { + // This is a tap, so remove the longpress check + removeLongPressCallback(); + + // 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(); + } + if (!SimpleHandler.getInstance().post(mPerformClick)) { + owner.performClick(); + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + + if (mPrePressed) { + SimpleHandler.getInstance().postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!SimpleHandler.getInstance().post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + mPrePressed = true; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + SimpleHandler.getInstance().postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + break; + + case MotionEvent.ACTION_CANCEL: + owner.setPressed(false); + removeTapCallback(); + removeLongPressCallback(); + break; + + case MotionEvent.ACTION_MOVE: + owner.setHotspot(x, y); + + // Be lenient about moving outside of buttons + if (!pointInView(x, y, sTouchSlop)) { + // Outside button + removeTapCallback(); + if (owner.isPressed()) { + // Remove any future long press/tap checks + removeLongPressCallback(); + + owner.setPressed(false); + } + } + break; + } + + return true; + } + + return false; + } + + private void setPressed(boolean pressed, float x, float y){ + mOwner.setPressed(pressed); + if (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(float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < (mOwner.getWidth() + slop) && + localY < (mOwner.getHeight() + slop); + } + + /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback() { + if (mPendingCheckForLongPress != null) { + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback() { + if (mPendingCheckForTap != null) { + mPrePressed = false; + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForTap); + } + } + + private void checkForLongClick(int delayOffset) { + if (mOwner.isLongClickable()) { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + SimpleHandler.getInstance().postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); + } + } + + private final class CheckForLongPress implements Runnable { + + @Override + public void run() { + if (mOwner.isPressed()) { + if (mOwner.performLongClick()) { + mHasPerformedLongPress = true; + } + } + } + } + + private final class CheckForTap implements Runnable { + public float x; + public float y; + + @Override + public void run() { + mPrePressed = false; + setPressed(true, x, y); + checkForLongClick(ViewConfiguration.getTapTimeout()); + } + } + + private final class PerformClick implements Runnable { + + @Override + public void run() { + mOwner.performClick(); + } + } + + private final class UnsetPressedState implements Runnable { + + @Override + public void run() { + mOwner.setPressed(false); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchOwner.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchOwner.java new file mode 100644 index 000000000..aa82118f9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TouchOwner.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +public interface TouchOwner { + + void setHotspot(float x, float y); + + boolean isEnabled(); + + boolean isPressed(); + + void setPressed(boolean pressed); + + boolean isClickable(); + + boolean isLongClickable(); + + boolean performClick(); + + boolean performLongClick(); + + int getWidth(); + + int getHeight(); +}