Related
I've just started writing a custom view in Android (for the first time) and I've realised that I need to implement a scrolling feature.
The custom view also uses a header containing some text (which should stay fixed and not scroll).
I've read through the documentation on GestureDetector.SimpleOnGestureListener and Scroller. I also read the documentation on Animating a Scroll Gesture but I found the examples difficult to understand. I've also looked at other questions on Stack Overflow which helped a little.
Using what I understood from the documentation with the Stack Overflow answer as a reference to guide me, I have added the following to my custom view:
Variables and fields:
private OverScroller mScroller;
private final GestureDetector mGestureDetector =
new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
// Note 0 as the x-distance to prevent horizontal scrolling
scrollBy(0, (int) distanceY);
return true;
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
final int maxScrollX = 0;
// wholeViewHeight is height of everything that is drawn
int wholeViewHeight = calculateWholeHeight();
int visibleHeight = getHeight();
final int maxScrollY = wholeViewHeight - visibleHeight;
mScroller.forceFinished(true);
mScroller.fling(0, // No startX as there is no horizontal scrolling
getScrollY(),
0, // No velocityX as there is no horizontal scrolling
- (int) velocityY,
0,
maxScrollX,
0,
maxScrollY);
invalidate();
return true;
}
#Override
public boolean onDown(MotionEvent e) {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
});
Initialization of mScroller:
// Called from the constructor
private void init() {
mScroller = new OverScroller(getContext(), new FastOutLinearInInterpolator());
...
}
Stuff in onDraw():
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
}
Stuff in onTouchEvent():
#Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
The result of these additions is a custom view which can scroll vertically (and not horizontally), however there are a few issues:
I can scroll further past what is drawn
There is no edge glow effect as I reach the end of the custom view (I mean like a RecyclerView or ScrollView)
All of the custom view scrolls as opposed to just a certain part of it
I don't fully understand what is going on
Could someone explain how scrolling works in a custom view and how to implement it properly with these features?
May I offer a simple layout that has a fixed header and vertically scrolling content that sounds like it will do what you want and does not require complex programming? It may save you from having to spend hours of research and programming in the long run:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<FrameLayout
android:id="#+id/scrolling_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
Using this layout, you would replace or insert inside the FrameLayout with ID "header" with your fixed header view. You would then put your scolling content inside of the FrameLayout with ID "scrolling_content". The NestedScrollingView would be located directly below your fixed header and automatically give you the scrolling behavior you want without any further code.
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 would like to have a linearlayout with a header section on top and a webview below. The header will be short and the webview may be longer and wider than the screen.
What is the best way to get horizontal and vertical scrolling? Is a ScrollView nested inside a HorizontalScrollView a good idea?
Is a ScrollView nested inside a HorizontalScrollView a good idea?
Yes, and no.
Yes, my understanding is that ScrollView and HorizontalScrollView can be nested.
No, AFAIK, neither ScrollView nor HorizontalScrollView work with WebView.
I suggest that you have your WebView fit on the screen.
there is another way. moddified HorizontalScrollView as a wrapper for ScrollView. normal HorizontalScrollView when catch touch events don't forward them to ScrollView and you only can scroll one way at time. here is solution:
package your.package;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.view.MotionEvent;
import android.content.Context;
import android.util.AttributeSet;
public class WScrollView extends HorizontalScrollView
{
public ScrollView sv;
public WScrollView(Context context)
{
super(context);
}
public WScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public WScrollView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override public boolean onTouchEvent(MotionEvent event)
{
boolean ret = super.onTouchEvent(event);
ret = ret | sv.onTouchEvent(event);
return ret;
}
#Override public boolean onInterceptTouchEvent(MotionEvent event)
{
boolean ret = super.onInterceptTouchEvent(event);
ret = ret | sv.onInterceptTouchEvent(event);
return ret;
}
}
using:
#Override public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/*BIDIRECTIONAL SCROLLVIEW*/
ScrollView sv = new ScrollView(this);
WScrollView hsv = new WScrollView(this);
hsv.sv = sv;
/*END OF BIDIRECTIONAL SCROLLVIEW*/
RelativeLayout rl = new RelativeLayout(this);
rl.setBackgroundColor(0xFF0000FF);
sv.addView(rl, new LayoutParams(500, 500));
hsv.addView(sv, new LayoutParams(WRAP_CONTENT, MATCH_PARENT /*or FILL_PARENT if API < 8*/));
setContentView(hsv);
}
Two years further down the line I think the open source community might have to your rescue:
2D Scroll View.
Edit: The Link doesn't work anymore but here is a link to an old version of the blogpost;
I searched really long to make this work and finally found this thread here. wasikuss' answer came quite close to the solution, but still it did not work properly. Here is how it works very well (at least for me (Android 2.3.7)). I hope, it works on any other Android version as well.
Create a class called VScrollView:
package your.package.name;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
public class VScrollView extends ScrollView {
public HorizontalScrollView sv;
public VScrollView(Context context) {
super(context);
}
public VScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
sv.dispatchTouchEvent(event);
return true;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
sv.onInterceptTouchEvent(event);
return true;
}
}
Your layout should look like:
<your.package.name.VScrollView
android:id="#+id/scrollVertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<HorizontalScrollView
android:id="#+id/scrollHorizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TableLayout
android:id="#+id/table"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:stretchColumns="*" >
</TableLayout>
</HorizontalScrollView>
</your.package.name.VScrollView>
In your activity, you should do something like:
hScroll = (HorizontalScrollView) findViewById(R.id.scrollHorizontal);
vScroll = (VScrollView) findViewById(R.id.scrollVertical);
vScroll.sv = hScroll;
... and that's how it works. At least for me.
There is an easy workaround:
In you activity get a reference to the outer scrollView (I'm going to assume a vertical scrollview) and a reference to the first child of that scroll view.
Scrollview scrollY = (ScrollView)findViewById(R.id.scrollY);
LinearLayout scrollYChild = (LinearLayout)findViewById(R.id.scrollYChild);
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
scrollYChild.dispatchTouchEvent(event);
scrollY.onTouchEvent(event);
return true;
}
One could argue that this solution is a bit hacky. But it has worked great for me in several applications!
Late to answer, but hopefully might be helpful to someone.
You can check out droid-uiscrollview. This is heavily based on #MrCeeJ's answer, but I seemed to have a lot of trouble getting the actual content to be rendered. Hence I pulled in the latest source from HorizontalScrollView & ScrollView to create droid-uiscrollview. There are a few todo's left which I haven't gotten around to finish, but it does suffice to get content to scroll both horizontally & vertically at the same time
I've try both wasikuss and user1684030 solutions and I had to adapt them because of one warning log: HorizontalScrollView: Invalid pointerId=-1 in onTouchEvent, and because I wasn't fan of this need of creating 2 scroll views.
So here is my class:
public class ScrollView2D extends ScrollView {
private HorizontalScrollView innerScrollView;
public ScrollView2D(Context context) {
super(context);
addInnerScrollView(context);
}
public ScrollView2D(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() == 1) {
View subView = getChildAt(0);
removeViewAt(0);
addInnerScrollView(getContext());
this.innerScrollView.addView(subView);
} else {
addInnerScrollView(getContext());
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = super.onTouchEvent(event);
handled |= this.innerScrollView.dispatchTouchEvent(event);
return handled;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
return true;
}
public void setContent(View content) {
if (content != null) {
this.innerScrollView.addView(content);
}
}
private void addInnerScrollView(Context context) {
this.innerScrollView = new HorizontalScrollView(context);
this.innerScrollView.setHorizontalScrollBarEnabled(false);
addView(this.innerScrollView);
}
}
And when using it in XML, you have nothing to do if the content of this scroll view is set in here. Otherwise, you just need to call the method setContent(View content) in order to let this ScrollView2D knows what is its content.
For instance:
// Get or create a ScrollView2D.
ScrollView2D scrollView2D = new ScrollView2D(getContext());
scrollView2D.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
addView(scrollView2D);
// Set the content of scrollView2D.
RelativeLayout testView = new RelativeLayout(getContext());
testView.setBackgroundColor(0xff0000ff);
testView.setLayoutParams(new ViewGroup.LayoutParams(2000, 2000));
scrollView2D.setContent(testView);
For a while I've been trying solutions from here, but the one that worked best still had one problem: It ate all events, none were making it through to elements within the scroller.
So I've got ... yet another answer, in Github and well-commented at least hopefully: https://github.com/Wilm0r/giggity/blob/master/app/src/main/java/net/gaast/giggity/NestedScroller.java
Like all solutions, it's a nested HorizontalScrollview (outer) + ScrollView (inner), with the outer receiving touch events from Android, and the inner receiving them only internally from the outer view.
Yet I'm relying on the ScrollViews to decide whether a touch event is interesting and until they accept it, do nothing so touches (i.e. taps to open links/etc) can still make it to child elements.
(Also the view supports pinch to zoom which I needed.)
In the outer scroller:
#Override
public boolean onInterceptTouchEvent(MotionEvent event)
{
if (super.onInterceptTouchEvent(event) || vscroll.onInterceptTouchEventInt(event)) {
onTouchEvent(event);
return true;
}
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
super.onTouchEvent(event);
/* Beware: One ugliness of passing on events like this is that normally a ScrollView will
do transformation of the event coordinates which we're not doing here, mostly because
things work well enough without doing that.
For events that we pass through to the child view, transformation *will* happen (because
we're completely ignoring those and let the (H)ScrollView do the transformation for us).
*/
vscroll.onTouchEventInt(event);
return true;
}
vscroll here is the "InnerScroller", subclassed from ScrollView, with a few changes to event handling: I've done some terrible things to ensure incoming touch events directly from Android are discarded, and instead it will only take them from the outer class - and only then pass those on to the superclass:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
/* All touch events should come in via the outer horizontal scroller (using the Int
functions below). If Android tries to send them here directly, reject. */
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
/* It will still try to send them anyway if it can't find any interested child elements.
Reject it harder (but pretend that we took it). */
return true;
}
public boolean onInterceptTouchEventInt(MotionEvent event) {
return super.onInterceptTouchEvent(event);
}
public boolean onTouchEventInt(MotionEvent event) {
super.onTouchEvent(event);
}
I know you have accepted your answer but may be this could give you some idea.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<HorizontalScrollView
android:layout_alignParentBottom="true"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ImageView
android:src="#drawable/device_wall"
android:scaleType="center"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</HorizontalScrollView>
</RelativeLayout>
</LinearLayout>
</ScrollView>
I have a flipper:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="#+id/ParentLayout"
xmlns:android="http://schemas.android.com/apk/res/android" style="#style/MainLayout" >
<LinearLayout android:id="#+id/FlipperLayout" style="#style/FlipperLayout">
<ViewFlipper android:id="#+id/viewflipper" style="#style/ViewFlipper">
<!--adding views to ViewFlipper-->
<include layout="#layout/home1" android:layout_gravity="center_horizontal" />
<include layout="#layout/home2" android:layout_gravity="center_horizontal" />
</ViewFlipper>
</LinearLayout>
</LinearLayout>
The first layout,home1, consists of a scroll view.
What should I do to distinguish between the flipping gesture and the scrolling?
Presently:
if I remove the scroll view, I can swipe across
if I add the scroll view, I can only scroll.
I saw a suggestion that I should override onInterceptTouchEvent(MotionEvent), but I do not know how to do this. My code, at this moment, looks like this:
public class HomeActivity extends Activity {
-- declares
#Override
public void onCreate(Bundle savedInstanceState) {
-- declares & preliminary actions
LinearLayout layout = (LinearLayout) findViewById(R.id.ParentLayout);
layout.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
return true;
}
return false;
}});
#Override
public boolean onTouchEvent(MotionEvent event) {
gestureDetector.onTouchEvent(event);
return true;
}
class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// http://www.codeshogun.com/blog/2009/04/16/how-to-implement-swipe-action-in-android/
}
}
}
Can anybody please guide me in the right direction?
Thank you.
The view flipper only displays one view at a time as explained here. It is a kind of switch that is very useful when the developer wants to give the user a choice as to how to view the data (list, thumbnails, ect). therefore, the reason why you cant scroll and fling at the same time is because one view only scrolls and the other view only flings and only one is open at a time.
If you want your display to both scroll and fling, you will have to design a layout that is capable of both and override the needed methods. The first step towards that would be to remove your ViewFLipper and use something else.
Hope this was helpful!
Basically, if you have a ScrollView in a ViewFlipper, all you have to do is attach onTouchListener to the ScrollView:
ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event){
if (myGestureDetector.onTouchEvent(event)){
return true;
}
else{
return false;
}
}
});
I try this way to make it works:
#Override
public boolean onTouchEvent(MotionEvent event)
{
scrollView.onTouchEvent(event);
return super.onTouchEvent(event);
}
#Override
public boolean dispatchTouchEvent(MotionEvent e)
{
gestureDetector.onTouchEvent(e);
return super.dispatchTouchEvent(e);
}
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