Hi i'm working on a Battleships game in Android and currently i'm trying to implement the ship positioning activity.
I have a custum view with onDraw representing the board on which you position the ships.
I want to be able to rotate ships by singletapping them and drag a ship by longclicking it. The thing is i can't just use onClick and onLongClick because i need to know where was the click on the canvas. I tried using onTouch but that didn't work. I also tried using GestureDetector but it just meseed up everything.
Do you have any suggestions on how to approach this logic?
i need to know where was the click on the canvas
You have a custom view, hence you can easily use GestureDetector.SimpleOnGestureListener. Just override the onTouchEvent() of your CustomView and use the onLongPress of the GestureDetector. I would suggest you to handle this within the CustomView itself, rather than do it in Activity or Fragment. This would keep things modularized.
You can follow the code below to get this done:
CustomView.java
public class CustomView extends View {
private GestureDetectorCompat mGestureDetector;
private LongPressGestureListener longPressGestureListener;
CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
longPressGestureListener= new LongPressGestureListener(this);
mGestureDetector = new GestureDetectorCompat(context, longPressGestureListener);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
// Handle any other event here, if not long press.
return true;
}
}
LongPressGestureListener.java
public class LongPressGestureListener extends GestureDetector.SimpleOnGestureListener {
#Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
// e will give you the location and everything else you want
// This is where you will be doing whatever you want to.
int eIndex = MotionEventCompat.getActionIndex(e);
float eX = MotionEventCompat.getX(e, eIndex);
float eY = MotionEventCompat.getY(e, eIndex);
Log.d("X:Y = " + eX + " : " + eY);
}
#Override
public boolean onDown(MotionEvent e) {
return true;
}
}
You will need to use View.OnTouchListener.
Set the touch listener to your canvas with view.setOnTouchListener(listener).
Implement your touch listener. You will need to implement the onTouch(View v, MotionEvent event) method. In this method, you will have access to the touch event, and you will be able to decide, if it is a simple click, a long press, etc, and do the appropriate action.
You can read more about it in this answer on SO.
Related
Is it possible to create custom view that both overrides function onTouch() inside this custom view implementation, and enables to set custom GestureDetector via setOnTouchListener().
I would like to Override onTouch() method to implement some drawing logic in the View related to touch gestures.
Than I would like to use such self-contained custom view to attach to it GestureDetector to detect and handle some custom gestures on this view inside Activity.
It works for me only if I have onTouch() drawing implementation, or only setOnTouchListener() to detect gestures. Maybe I could place this gesture detection inside view. But I would prefer to have this as separate loosely coupled reusable component rather than tightly coupled gesture detector.
You can do somethink like this:
public class CustomTouchView extends View {
private OnTouchListener onTouchListener;
public CustomTouchView(Context context) {
super(context);
}
#Override
public void setOnTouchListener(OnTouchListener l) {
super.setOnTouchListener(l);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (onTouchListener != null) {
return onTouchListener.onTouch(this, event);
} else {
return super.onTouchEvent(event);
}
// or implement your custom touch logic here
}
}
For practicing purpose, I am create a customized Tooltip control. To use the Tooltip control, a hosting UIControl (e.g. a Button) will be assigned to my Tooltip control, and I want my Tooltip control is be able to listen to the mouse press event on the hosting control (i.e. the Button), and show / dismiss itself accordingly.
I am having problem finding a way to listening to mouse events of the hosting control. I tried:
Set the Hosting Control's setOnTouchListener, this works, but it will override the existing OnTouchListener of the Hosting Control, thus undeserable.
Go to the Hosting Control's ViewGroup, and add a **Observer to the ViewGroup. But there is no way to observe the mouse event on the ViewGroup.
So is listening to other control's mouse event doable from a custom view, if so, what's the recommended way to implement it ?
Thanks.
I also thought of another way to do it, as followed:
Get the ViewGroup of the hosting control;
In the ViewGroup, add a transparent view to listen to the mouse event.
In the handler of mouse event of the transparent view, check whether the mouse event is happened on the Hosting Control.
If happened on the Hosting Control, respond correspondingly.
I will try this approach after I post my question, but it seems to be resource-intensive way of implementing something seemingly straightforward.
I will let you know if this approach works or not, any comment / thought is very appreciated.
Thanks ~!
Try to use this approach. I've already tried this approach with OnClickListener and it works great.
public class CustomButton extends Button {
private OnTouchListener outsideListener;
private OnTouchListener innerListener = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (outsideListener != null) {
outsideListener.onTouch(v, event);
}
//some code here ...
}
};
#Override
public void setOnTouchListener(OnTouchListener listener) {
outsideListener = listener;
}
public CustomButton(Context context) {
super(context);
super.setOnTouchListener(innerListener);
}
public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
super.setOnTouchListener(innerListener);
}
public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setOnTouchListener(innerListener);
}
}
as I mentioned in my question, I thought of a way to implement what I wanted. I am sharing my way of implementing here. But still, it seems to be resource-intensive way of solving seemingly simple problem. If you have an easier solution, or any comment, please leave a comment. Much Appreciated ~!
My custom MaterialTooltip class:
public class MaterialToolTip {
/// Implementation
}
An example of how to use my MaterialToolTip class:
public class MainActivity extends AppCompatActivity {
MaterialToolTip toolTip;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// anchorButton is the button to which ToolTip will be added to.
Button anchorButton = (Button)findViewById(R.id.button);
this.toolTip = new MaterialToolTip.Builder(this)
//.anchorView property is used to specify the view that will use this tooltip
.anchorView(anchorButton)
.maxWidth(R.dimen.simpletooltip_max_width)
.build(); // that's all the consumer needs to do, as soon as the tooltip is attached to the View, the tooltip decided when to show / dismiss by listening to View's event.
}
}
Within my MaterialToolTip class:
Create a transparent view that listens to mouse event
#SuppressLint("ViewConstructor")
public class ToolTipPressInterceptView extends View {
OnAnchorViewMouseEventListener mListener;
ToolTipPressInterceptView(Context context, View anchorView) {
super(context);
//get the anchorView's onScreenLocation
int[] output = new int[2];
anchorView.getLocationOnScreen(output);
anchorViewRect = new RectF(output[0], output[1], output[0] + anchorView.getMeasuredWidth(), output[1] + anchorView.getMeasuredHeight()) ;
//set the dimension to match parent
this.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
this.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
if (anchorViewRect.contains(event.getX(), event.getY())){
mListener.onMouseEvent(AnchorViewMouseEventType.PRESSED);
}
}
case MotionEvent.ACTION_UP:{
if (anchorViewRect.contains(event.getX(), event.getY())){
mListener.onMouseEvent(AnchorViewMouseEventType.RELEASED);
}
}
}
return false;
}
});
}
}
Get the ViewGroup of the hosting control, and add the transparent view to the ViewGroup;
private MaterialToolTip(Builder builder){
//.. Other initialisation logic
//.. add the TransparentView to the ViewGroup;
mRootView = (ViewGroup)mAnchorView.getRootView();
ToolTipPressInterceptView view = new ToolTipPressInterceptView(mContext, mAnchorView);
//mAnchorViewTouchListener is listener to Mouse Event
view.setOnTouchListener(mAnchorViewTouchListener);
mRootView.addView(view);
}
If happened on the Hosting Control, respond correspondingly.
private final View.OnTouchListener mAnchorViewTouchListener = new
View.OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event){
int action = MotionEventCompat.getActionMasked(event);
switch(action) {
case (MotionEvent.ACTION_DOWN) :
// show
show();
case (MotionEvent.ACTION_UP) :
// dismiss
dismiss();
}
return false; //return false so the other handlers is able to
}
};
I am using a ViewPager with a TouchImageView inside it and it works great, (I have used this solution in many of my Android apps).
However I have an app for which there are many other controls on the same screen so they are all inside a scrollview control.
In this scenario I see the scrollview does not play nice and I am not able to pan within the zoomed image. When I use my finger to pan upward or downward the entire page scrolls instead of the image panning.
So here is what I am trying to do....
Inside the TouchImageView I detect Zoom Begin and Zoom End and have created an interface to make a callback to my Activity onZoomBegin() and onZoomEnd() methods.
In the onZoomBegin() method I want to disable the scrollview from responding to any touch events and in onZoomEnd() I can re-enable it.
So far here are the things I have tried doing in the onZoomBegin() method for which none are working....
scrollView.setEnabled(false);
scrollView.requestDisallowInterceptTouchEvent(true);
also I have tried the answer to a similar question which was to takeover the onTouchListener like such:
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
This does stop the scrollview from scrolling but the scrollview is still intercepting the touch events cause the image still will not pan up or down.
I've tried checking nestedScrollingEnabled in the layout designer, no joy....
I just want to know is there a way to totally disable a scrollview and then re-enable it from responding to touch events?
I found this answer on another question somewhere but by the time I realized it was the solution to my problem (answer to my question) then I lost reference to it. I will keep looking so I can edit this post to give credit where credit is due.
public class CustomScrollView extends ScrollView {
// true if we can scroll the ScrollView
// false if we cannot scroll
private boolean scrollable = true;
public CustomScrollView(Context context) {
super(context);
}
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setScrollingEnabled(boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isScrollable() {
return scrollable;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if we can scroll pass the event to the superclass
if (scrollable)
return super.onTouchEvent(ev);
// only continue to handle the touch event if scrolling enabled
return false; // scrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (!scrollable)
return false;
else
return super.onInterceptTouchEvent(ev);
}
}
This part I just figured out for myself.... In the TouchImageView I added a callback interface which is called when a zoom begins and ends so in my Activity I only had to do this:
private class OnZoomListener implements TouchImageView.OnZoomListener {
#Override
public void onZoomBegin() {
isZoomed = true;
scrollView.scrollTo(0, 0);
scrollView.setScrollingEnabled(false); // <-- disables scrollview
hideImageControls();
sizeViewPager();
}
#Override
public void onZoomEnd() {
scrollView.setScrollingEnabled(true); // <-- enables scrollview
showImageControls();
isZoomed = false;
}
}
I have a use case where there are two views on screen one of which is partially covering another. The one that is above needs to handle scroll events and ignore touch up. The partially obscured view should handle touch up events, including those that happen in the area of overlap that are ignored by the obscuring view.
a simplified example layout is below.
the closest i've come uses GestureDetectorCompat on the top view returning true in onDown (otherwise i don't get any further events,) true in onScroll, and false in onSingleTapUp. i have tried several things in the view behind all with the same results: i get taps on the un-obscured section, but the top view eats all of the motion events for the obscured portion.
What you want to do is not as straightforward as you would probably like because of how Android handles touch event flow. So let me set the stage with a little context first:
The reason this is a tricky proposition is because Android defines a gesture as all the events between an ACTION_DOWN and the corresponding ACTION_UP. ACTION_DOWN is the only point at which the framework is searching for a touch target (which is why you have to return true for that event to see any others). Once a suitable target has been found, ALL the remaining events in that gesture will be delivered directly to that view and nobody else.
This means that if you want a single event to go to a different destination, you will have to capture and redirect it yourself. All touch events flow from parent views to child views in one long chain. Parent views control when and how touch events move from one child to the next, including modifying the coordinates of the MotionEvent so the match the local bounds of each child view. Because of this, the most effective place to manipulate touch events is in a custom ViewGroup parent implementation.
The following example comes with a big bag of assumptions. Basically, I'm assuming that both views are nothing more than a dumb View with no internal wishes to handle touch (which is probably wrong). Applying this code to other, more complex, child views may requires some rework...but this should get you started.
The best place to force touch redirection is in a common parent of the two views, since it is the origin of the touch for both (as described above).
public class TouchUpRedirectLayout extends FrameLayout implements View.OnTouchListener {
private int mTargetViewId;
private View mTargetView;
private boolean mTargetTouchActive;
private GestureDetector mGestureDetector;
public TouchUpRedirectLayout(Context context) {
super(context);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mGestureDetector = new GestureDetector(context, mGestureListener);
}
public void setTargetViewId(int resId) {
mTargetViewId = resId;
updateTargetView();
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
//Find the target view, if set, once inflated
updateTargetView();
}
//Set the target view to handle gestures
private void updateTargetView() {
if (mTargetViewId > 0) {
mTargetView = findViewById(mTargetViewId);
if (mTargetView != null) {
mTargetView.setOnTouchListener(this);
}
}
}
private Rect mHitRect = new Rect();
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
if (mTargetTouchActive) {
mTargetTouchActive = false;
//Validate the up
int index = indexOfChild(mTargetView) - 1;
if (index < 0) {
return false;
}
for (int i=index; i >= 0; i--) {
final View child = getChildAt(i);
child.getHitRect(mHitRect);
if (mHitRect.contains((int) event.getX(), (int) event.getY())) {
//Dispatch and mark handled
return child.dispatchTouchEvent(event);
}
}
//Steal this event
return true;
}
//Allow default processing
return false;
default:
//Allow default processing
return false;
}
}
//Receive touch events from the target (scroll handling) view
#Override
public boolean onTouch(View v, MotionEvent event) {
mTargetTouchActive = true;
return mGestureDetector.onTouchEvent(event);
}
//Handle gesture events in target view
private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onDown(MotionEvent e) {
Log.d("TAG", "onDown");
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d("TAG", "Scrolling...");
return true;
}
};
}
This example layout (I subclassed FrameLayout, but you could choose whichever layout you are using currently as the parent of the two views) tracks a single "target" view for the purposes of notifying the "down" and "scroll" gestures. It also notifies us when a gesture is in play that will include an ACTION_UP event that we need to capture and forward to another obscured view.
When an up event occurs, we use the intercept functionality of ViewGroup to direct that event away from the original "target" view, and dispatch it to the next available child view whose bounds fit the event. You could just as easily hard-code the second "obscured" view here as well, but I've written it to dispatch to any and all possible children underneath...similar to the way ViewGroup handles touch delegation to children in the first place.
Here is an example layout:
<com.example.touchoverlaptest.app.TouchUpRedirectLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/view_root"
android:layout_width="match_parent"
android:layout_height="400dp"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
tools:context="com.example.touchoverlaptest.app.MainActivity">
<View
android:id="#+id/view_obscured"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#7A00" />
<View
android:id="#+id/view_overlap"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_gravity="bottom"
android:background="#70A0" />
</com.example.touchoverlaptest.app.TouchUpRedirectLayout>
...and Activity with the view in action:
public class MainActivity extends Activity implements View.OnTouchListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TouchUpRedirectLayout layout = (TouchUpRedirectLayout) findViewById(R.id.view_root);
layout.setTargetViewId(R.id.view_overlap);
layout.findViewById(R.id.view_obscured).setOnTouchListener(this);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", "Obscured touch "+event.getActionMasked());
return true;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
The target view will fire all the gesture callbacks, and the obscured view will receive the up events. The OnTouchListener in the activity is simply to validate that the events are delivered.
If you would like more detail about custom touch handling in Android, here is a video link to a presentation I did recently on the topic.
I am using the onScroll method of GestureDetector.SimpleOnGestureListener to scroll a large bitmap on a canvas. When the scroll has ended I want to redraw the bitmap in case the user wants to scroll further ... off the edge of the bitmap, but I can't see how to detect when the scroll has ended (the user has lifted his finger from the screen).
e2.getAction() always seems to return the value 2 so that is no help.
e2.getPressure seems to return fairly constant values (around 0.25) until the final onScroll call when the pressure seems to fall to about 0.13. I suppose I could detect this reduction in pressure, but this will be far from foolproof.
There must be a better way: can anyone help, please?
Here is how I solved the problem. Hope this helps.
// declare class member variables
private GestureDetector mGestureDetector;
private OnTouchListener mGestureListener;
private boolean mIsScrolling = false;
public void initGestureDetection() {
// Gesture detection
mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
#Override
public boolean onDoubleTap(MotionEvent e) {
handleDoubleTap(e);
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
handleSingleTap(e);
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// i'm only scrolling along the X axis
mIsScrolling = true;
handleScroll(Math.round((e2.getX() - e1.getX())));
return true;
}
#Override
/**
* Don't know why but we need to intercept this guy and return true so that the other gestures are handled.
* https://code.google.com/p/android/issues/detail?id=8233
*/
public boolean onDown(MotionEvent e) {
Log.d("GestureDetector --> onDown");
return true;
}
});
mGestureListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) {
return true;
}
if(event.getAction() == MotionEvent.ACTION_UP) {
if(mIsScrolling ) {
Log.d("OnTouchListener --> onTouch ACTION_UP");
mIsScrolling = false;
handleScrollFinished();
};
}
return false;
}
};
// attach the OnTouchListener to the image view
mImageView.setOnTouchListener(mGestureListener);
}
You should take a look at http://developer.android.com/reference/android/widget/Scroller.html.
Especially this could be of help (sorted by relevance):
isFinished();
computeScrollOffset();
getFinalY(); getFinalX(); and getCurrY() getCurrX()
getDuration()
This implies that you have to create a Scroller.
If you want to use touching you could also use GestureDetector and define your own canvas scrolling. The following sample is creating a ScrollableImageView and in order to use it, you have to define the measurements of your image. You can define your own scrolling range and after finishing your scrolling the image gets redrawn.
http://www.anddev.org/viewtopic.php?p=31487#31487
Depending on your code you should consider invalidating (int l, int t, int r, int b); for the invalidation.
SimpleOnGestureListener.onFling()
It seems to take place when a scroll ends (i.e. the user lets the finger go), that's what I am using and it works great for me.
Coming back to this after a few months I've now followed a different tack: using a Handler (as in the Android Snake sample) to send a message to the app every 125 milliseconds which prompts it to check whether a Scroll has been started and whether more than 100 milliseconds has elapsed since the last scroll event.
This seems to work pretty well, but if anyone can see any drawbacks or possible improvements I should be grateful to hear of them.
The relevant the code is in the MyView class:
public class MyView extends android.view.View {
...
private long timeCheckInterval = 125; // milliseconds
private long scrollEndInterval = 100;
public long latestScrollEventTime;
public boolean scrollInProgress = false;
public MyView(Context context) {
super(context);
}
private timeCheckHandler mTimeCheckHandler = new timeCheckHandler();
class timeCheckHandler extends Handler{
#Override
public void handleMessage(Message msg) {
long now = System.currentTimeMillis();
if (scrollInProgress && (now>latestScrollEventTime+scrollEndInterval)) {
scrollInProgress = false;
// Scroll has ended, so insert code here
// which calls doDrawing() method
// to redraw bitmap re-centred where scroll ended
[ layout or view ].invalidate();
}
this.sleep(timeCheckInterval);
}
public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
}
}
#Override protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// code to draw large buffer bitmap onto the view's canvas
// positioned to take account of any scroll that is in progress
}
public void doDrawing() {
// code to do detailed (and time-consuming) drawing
// onto large buffer bitmap
// the following instruction resets the Time Check clock
// the clock is first started when
// the main activity calls this method when the app starts
mTimeCheckHandler.sleep(timeCheckInterval);
}
// rest of MyView class
}
and in the MyGestureDetector class
public class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
[MyView].scrollInProgress = true;
long now = System.currentTimeMillis();
[MyView].latestScrollEventTime =now;
[MyView].scrollX += (int) distanceX;
[MyView].scrollY += (int) distanceY;
// the next instruction causes the View's onDraw method to be called
// which plots the buffer bitmap onto the screen
// shifted to take account of the scroll
[MyView].invalidate();
}
// rest of MyGestureDetector class
}
This is what worked for me.
I've enriched the existing GestureDetector.OnGestureListener with onFingerUp() method. This listener does everything as the built-in GestureDetector and it can also listen to the finger up event (it's not onFling() as this is called only when the finger is lifted up along with a quick swipe action).
import android.content.Context;
import android.os.Handler;
import android.view.GestureDetector;
import android.view.MotionEvent;
public class FingerUpGestureDetector extends GestureDetector {
FingerUpGestureDetector.OnGestureListener fListener;
public FingerUpGestureDetector(Context context, OnGestureListener listener) {
super(context, listener);
fListener = listener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, OnGestureListener fListener) {
super(context, listener);
this.fListener = fListener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, OnGestureListener fListener) {
super(context, listener, handler);
this.fListener = fListener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, boolean unused, OnGestureListener fListener) {
super(context, listener, handler, unused);
this.fListener = fListener;
}
public interface OnGestureListener extends GestureDetector.OnGestureListener {
boolean onFingerUp(MotionEvent e);
}
public static class SimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements FingerUpGestureDetector.OnGestureListener {
#Override
public boolean onFingerUp(MotionEvent e) {
return false;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (super.onTouchEvent(ev)) return true;
if (ev.getAction() == MotionEvent.ACTION_UP) {
return fListener.onFingerUp(ev);
}
return false;
}
}
I think this will work as you need
protected class SnappingGestureDetectorListener extends SimpleOnGestureListener{
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
boolean result = super.onScroll(e1, e2, distanceX, distanceY);
if(!result){
//Do what you need to do when the scrolling stop here
}
return result;
}
}
I am sure it is too late for you, however, it seems I have found the right solution to your original question and not necessarily the intention.
If you are using Scroller/OverScroller Object for scrolling you should check the return value from the following function.
public boolean computeScrollOffset()
I was looking into this same issue. I saw Akos Cz's answer to your question. I created something similar, but with my version, I noticed that it only worked for a regular scroll - meaning one that doesn't generate a fling. But if a fling did get generated - regardless if I processed a fling or not, then it did NOT detect the "ACTION_UP" in "onTouchEvent". Now maybe this was just something with my implementation, but if it was I couldn't figure out why.
After further investigation, I noticed that during a fling, the "ACTION_UP" was passed into "onFling" in "e2" every time. So I figured that must be why it wasn't being handled in "onTouchEvent" in those instances.
To make it work for me I only had to call a method to handle the "ACTION_UP" in "onFling" and then it worked for both types of scrolling. Below are the exact steps I took to implement my app:
-initialized a "gestureScrolling" boolean to "false" in a constructor.
-I set it to "true" in "onScroll"
-created a method to handle the "ACTION_UP" event. Inside that event, I reset "gestureSCrolling" to false and then did the rest of the processing I needed to do.
-in "onTouchEvent", if an "ACTION_UP" was detected and "gestureScrolling" = true, then I called my method to handle "ACTION_UP"
-And the part that I did that was different was: I also called my method to handle "ACTION_UP" inside of "onFling".
I haven't done this myself but looking at onTouch() you always get a sequence 0<2>1, so the end has to be a 1 for finger lift.
I don't know Android, but looking at the documentation it seems Rob is right: Android ACTION_UP constant Try checking for ACTION_UP from getAction()?
Edit: What does e1.getAction() show? Does it ever return ACTION_UP? The documentation says it holds the initial down event, so maybe it'll also notify when the pointer is up
Edit: Only two more things I can think of. Are you returning false at any point? That may prevent ACTION_UP
The only other thing I'd try is to have a seperate event, maybe onDown, and set a flag within onScroll such as isScrolling. When ACTION_UP is given to onDown and isScrolling is set then you could do whatever you want and reset isScrolling to false. That is, assuming onDown gets called along with onScroll, and getAction will return ACTION_UP during onDown
i have not tried / used this but an idea for an approach:
stop / interrupt redrawing canvas on EVERY scroll event wait 1s and then start redrawing canvas on EVERY scroll.
this will lead to performing the redraw only at scroll end as only the last scroll will actually be uninterrupted for the redraw to complete.
hope this idea helps you :)
Extract from the onScroll event from GestureListener API: link text
public abstract boolean onScroll
(MotionEvent e1, MotionEvent e2, float
distanceX, float distanceY) Since: API
Level 1
Returns
* true if the event is consumed, else false
Perhaps once the event has been consumed, the action is finished and the user has taken their finger off the screen or at the least finished this onScroll action
You can then use this in an IF statement to scan for == true and then commence with the next action.
If you're using SimpleGestureDetector to handle your scroll events, you can do this
fun handleTouchEvents(event: MotionEvent): Boolean {
if(event.action == ACTION_UP) yourListener.onScrollEnd()
return gestureDetector.onTouchEvent(event)
}
My attempt at adding additional functionality to the gesture detector. Hope it helps someone put his time to better use... gist