Stop scrollView scrolling on user touch - android

I'm automatically scrolling the scrollView to the view's bottom.
scrollView.smoothScrollTo(0, scrollView.getBottom());
If the user touches the layout, I need the scrolling to stop & stay at the current position.
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//// TODO: 01/08/16 STOP SCROLLING
}
return false;
}
});
I've tried to smoothScrollBy(0,0); but it's not working.

Well I solved it by using an ObjectAnimator. It not only elegantly worked as a solution but also gave me control on the scrolling speed.
I replaced
scrollView.smoothScrollTo(0, scrollView.getBottom());
with
objectAnimator = ObjectAnimator
.ofInt(scrollView, "scrollY", scrollView.getBottom())
.setDuration(3000);
objectAnimator.start();
and then
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
objectAnimator.cancel();
}
return false;
}
});

One approach may be to use smoothScrollToPosition, which stops any existing scrolling motion. Note this method requires API level >= 8 (Android 2.2, Froyo).
Note that if the current position is far away from the desired position, then the smooth scrolling will take quite a long time and look a bit jerky (at least in my testing on Android 4.4 KitKat). I also found that a combination of calling setSelection and smoothScrollToPosition could sometimes causes the position to "miss" slightly, this seems to happen only when the current position was very close to the desired position.
In my case, I wanted my list to jump to the top (position=0) when the user pressed a button (this is slightly different from your use case, so you will need to adapt this to your needs).
I used the following method to
private void smartScrollToPosition(ListView listView, int desiredPosition) {
// If we are far away from the desired position, jump closer and then smooth scroll
// Note: we implement this ourselves because smoothScrollToPositionFromTop
// requires API 11, and it is slow and janky if the scroll distance is large,
// and smoothScrollToPosition takes too long if the scroll distance is large.
// Jumping close and scrolling the remaining distance gives a good compromise.
int currentPosition = listView.getFirstVisiblePosition();
int maxScrollDistance = 10;
if (currentPosition - desiredPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition + maxScrollDistance);
} else if (desiredPosition - currentPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition - maxScrollDistance);
}
listView.smoothScrollToPosition(desiredPosition); // requires API 8
}
In my action handler for the button I then called this as follows
case R.id.action_go_to_today:
ListView listView = (ListView) findViewById(R.id.lessonsListView);
smartScrollToPosition(listView, 0); // scroll to top
return true;
The above does not directly answer your question, but if you can detect when the current position is at or near your desired position, then maybe you could use smoothScrollToPosition to stop the scrolling.

Related

How to attach a rotary scroll listener to Android Wear numberPicker?

I have been working for a day at this and still can't figure it out. I am making an Android app for an Android Wear device and I can't figure out how to detect the rotary wheel on the device to get scroll events. I know that by default the rotary wheel gets assigned to ScrollViews and Listviews, etc. and I've got that to work, but I can't get it to work on a simple NumberPicker. As suggested by Google API's, I've used the OnGenericMotionListener and while it does not throw any errors, it still doesn't pick up anything.
I would like the NumberPicker to scroll with the rotary wheel. My code is as follows. Nothing in logcat to show any errors:
MyActivity.this.setContentView(R.layout.number_picker);
NumberPicker numberPicker = (NumberPicker) findViewById(R.id.numberPicker1);
numberPicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
numberPicker.setMaxValue(100);
numberPicker.setMinValue(0);
numberPicker.setWrapSelectorWheel(true);
numberPicker.setOnValueChangedListener( new NumberPicker.
OnValueChangeListener() {
public void onValueChange(NumberPicker picker, int
oldVal, int newVal) {
}
});
numberPicker.setOnGenericMotionListener(new OnGenericMotionListener() {
#Override
public boolean onGenericMotion(View v, MotionEvent event) {
Log.i(TAG, "Received scroll event:" + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL:
}
return false;
}
});
I think you're looking for this:
Add custom rotary input scrolling:
If your scrollable view doesn't natively support rotary input
scrolling, or if you want to do something other than scrolling in
response to rotary input events (zoom in/out, turn dials, etc.), you
can use the RotaryEncoder methods in the Wearable Support Library.
The following code snippet shows how to use RotaryEncoder to add
custom scrolling in your app's view:
myView.setOnGenericMotionListener(new View.OnGenericMotionListener() {
#Override
public boolean onGenericMotion(View v, MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_SCROLL && RotaryEncoder.isFromRotaryEncoder(ev)) {
// Don't forget the negation here
float delta = -RotaryEncoder.getRotaryAxisValue(ev) * RotaryEncoder.getScaledScrollFactor(
getContext());
// Swap these axes if you want to do horizontal scrolling instead
scrollBy(0, Math.round(delta));
return true;
}
return false;
}
});
Anyway you can check the full guide about rotary events in android
wear.
The documentation it's a little confusing but it's all there, pay attention to the next paragraph:
Rotary input events are only sent to the focused view. These events
do not bubble up the view hierarchy. If there is no focused view, or
if the focused view returns false from View.onGenericMotionEvent,
then (and only then) the event is sent to
Activity.onGenericMotionEvent.
Basically all you have to do is override onGenericMotionEvent on your Activity like this:
#Override
public boolean onGenericMotion(View v, MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_SCROLL && RotaryEncoder.isFromRotaryEncoder(ev)) {
// Don't forget the negation here
float delta = -RotaryEncoder.getRotaryAxisValue(ev) * RotaryEncoder.getScaledScrollFactor(
getContext());
// Swap these axes if you want to do horizontal scrolling instead
scrollBy(0, Math.round(delta));
return true;
}
return false;
}
And that's it your Activity will receive all the rotary input events.

Get touch position while dragging

I have some views which I like to drag around. The views are within
a LinearLayout, which itself is within a scrollview.
I want to get the position of the current finger (touch), to
make a smooth scroll on my scrollview, depending of the
height of the current drag.
I start my draggin after a long click with the
View build-in listener startDrag
view.startDrag(null, shadowBuilder, view, 0);
I am also able to get the relativ position of the drag
on the view that is currently hovered by
view.setOnDragListener(new OnDragListener() {
#Override
public boolean onDrag(View v, DragEvent event) {
//get event positions
}
});
But this only works for the view where the drag shadow is currently on
and DragEvent only provides relative positions, not raw positions.
What I need is the position of the finger, while the drag happens. Unfortunately all onTouchEvents get consumed while dragging.
Does anybody know how I get this to work?
Ps: One way that works (kind of) which I currently use, is to calculate the
position of the touch via the dragEvent.relativePosition in combination
with the view position. But isn't there a better way to go?
OK, since there seems not to be an easier answer, I will provide my own relativly easy solution for further readers, with no need of gesture detection.
At first you need the view that receives the drag event and at second the
drag event ifself (or at least the x and y coordinates).
With fetching of the view position and some simple addition you get the
raw touch position.
This method is tested with the show pointer position developer options
to provide the correct data.
The method for the calculation looks like this:
/**
* #param item the view that received the drag event
* #param event the event from {#link android.view.View.OnDragListener#onDrag(View, DragEvent)}
* #return the coordinates of the touch on x and y axis relative to the screen
*/
public static Point getTouchPositionFromDragEvent(View item, DragEvent event) {
Rect rItem = new Rect();
item.getGlobalVisibleRect(rItem);
return new Point(rItem.left + Math.round(event.getX()), rItem.top + Math.round(event.getY()));
}
Call this method inside of your onDragListener implementation:
#Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
//etc etc. do some stuff with the drag event
break;
case DragEvent.ACTION_DRAG_LOCATION:
Point touchPosition = getTouchPositionFromDragEvent(v, event);
//do something with the position (a scroll i.e);
break;
default:
}
return true;
}
Addtional:
If you want to determinate if the touch is inside of a specific view, you can
do something like this:
public static boolean isTouchInsideOfView(View view, Point touchPosition) {
Rect rScroll = new Rect();
view.getGlobalVisibleRect(rScroll);
return isTouchInsideOfRect(touchPosition, rScroll);
}
public static boolean isTouchInsideOfRect(Point touchPosition, Rect rScroll) {
return touchPosition.x > rScroll.left && touchPosition.x < rScroll.right //within x axis / width
&& touchPosition.y > rScroll.top && touchPosition.y < rScroll.bottom; //withing y axis / height
}
It is also possible to implement a smooth scroll on a ListView based on this solution.
So that the user can drag an item out of a list, and scroll the list via dragging the item either to the top or bottom of the list view.
Cheers.

stop scrollView in the middle of the scroll

I have a scrollview with a lot of content. Now when user do a fling or scroll down, I want the scrollview to stop at a particular view location, where I am doing some animation, and then user can again fling or scroll down.
I have tried the disabling of scrollview as mentioned here but It only disables when the scrollview completely stops and cannot stop in the middle of a fling.
Is there any way I can stop a fling of the scrollview when a certain view location or a certain y value is reached?
I had the same problem my solution was.
listView.smoothScrollBy(0,0)
This will stop the scrolling.
To stop a fling at a particular point simply call
fling(0)
If you are only concerned about flings this is the most logical way to do so in my opinion, because velosityYis set to 0 and thereby the fling is stopped immediately.
Here is the javadoc of the fling method:
/**
* Fling the scroll view
*
* #param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/cursor is moving down the screen,
* which means we want to scroll towards the top.
*/
One approach may be to use smoothScrollToPosition, which stops any existing scrolling motion. Note this method requires API level >= 8 (Android 2.2, Froyo).
Note that if the current position is far away from the desired position, then the smooth scrolling will take quite a long time and look a bit jerky (at least in my testing on Android 4.4 KitKat). I also found that a combination of calling setSelection and smoothScrollToPosition could sometimes causes the position to "miss" slightly, this seems to happen only when the current position was very close to the desired position.
In my case, I wanted my list to jump to the top (position=0) when the user pressed a button (this is slightly different from your use case, so you will need to adapt this to your needs).
I used the following method to
private void smartScrollToPosition(ListView listView, int desiredPosition) {
// If we are far away from the desired position, jump closer and then smooth scroll
// Note: we implement this ourselves because smoothScrollToPositionFromTop
// requires API 11, and it is slow and janky if the scroll distance is large,
// and smoothScrollToPosition takes too long if the scroll distance is large.
// Jumping close and scrolling the remaining distance gives a good compromise.
int currentPosition = listView.getFirstVisiblePosition();
int maxScrollDistance = 10;
if (currentPosition - desiredPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition + maxScrollDistance);
} else if (desiredPosition - currentPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition - maxScrollDistance);
}
listView.smoothScrollToPosition(desiredPosition); // requires API 8
}
In my action handler for the button I then called this as follows
case R.id.action_go_to_today:
ListView listView = (ListView) findViewById(R.id.lessonsListView);
smartScrollToPosition(listView, 0); // scroll to top
return true;
The above does not directly answer your question, but if you can detect when the current position is at or near your desired position, then maybe you could use smoothScrollToPosition to stop the scrolling.
You need to disable ScrollView handling of the fling operation. To do this simply override the fling method in ScrollView and comment super.fling(). Let me know if this works !
public class CustomScrollView extends ScrollView {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
return false;
}
#Override
public void fling (int velocityY)
{
/*Scroll view is no longer gonna handle scroll velocity.
* super.fling(velocityY);
*/
}
}
I'm trying to achieve a similar situation. I have a partial solution:
I've managed to stop the ScrollView at a particular y coordinate by overriding onScrollChanged and then calling smoothScrollTo. I allow the user to continue scrolling down past that point by keeping a boolean that indicates if this was the first fling/drag or not. And the same goes for scrolling from bottom to top - I stop the scroll at the same point again. And then allow the user to continue scrolling upward again.
The code looks something like this:
boolean beenThereFromTop = false;
boolean beenThereFromBottom = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
TextView whereToStop = (TextView) findViewById(R.id.whereToStop);
final int y = whereToStop.getBottom();
int scrollY = scrollView.getScrollY();
// manage scrolling from top to bottom
if (scrollY > y) {
if (!beenThereFromTop) {
beenThereFromTop = true;
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, y);
}
});
}
}
if (scrollY < y && beenThereFromTop) {
beenThereFromTop = false;
}
// manage scrolling from bottom to top
if (scrollY < y) {
if (!beenThereFromBottom) {
beenThereFromBottom = true;
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, y);
}
});
}
}
if (scrollY > y && beenThereFromBottom) {
beenThereFromBottom = false;
}
}
});
}
Well im actualy using this method:
list.post(new Runnable()
{
#Override
public void run()
{
list.smoothScrollToPositionFromTop(arg2, 150);
}
});

keep ontouch event when view is removed and add

I have a view in a linearlayout. When the view is longpressed the view will be removed from this linearlayout and placed, on the same position of the screen, to a relative layout. On this way a can move the view over the screen with my finger.
it almost works:
i got the longpress event working (remove the view and place the view to the relativelayout). after that i add an ontoucheventlistener so my view stays with my finger, but only for a second. the last time the touchevent is fired i got "MotionEvent.ACTION_CANCEL". When i remove my finger and place my finger again to the view i can go feature with my movement, then it will keep until i remove my finger.
I think that my problem is that the view it removed for a short moment, at that time i get a "MotionEvent.ACTION_CANCEL", however, there are still some unhandled events, they will be fired first. Thats why i got for about 1 second still ontouchevents. (this is just a thought).
someone a idee how i can keep the ontouchevent, or let the ontouchevent fired without replacing my finger?
Edited
My thought is not correct. When i do a longpress the view stays with my finger, however i lost the view as soon as i move about 50 to 100 pixels to any direction.
Edited 2
the longpress code of the view inside the linearlayout
view.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
_linearLayout.removeView(v);
moveView(v);
return true;
}
});
moveView will be called by the longpress
private void moveView(View v) {
_relativeLayout.addView(v);
v.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getRawX();
int y = (int) event.getRawY();
v.layout(x, y, x + v.getWidth(), y + v.getHeight());
break;
case MotionEvent.ACTION_UP:
_relativeLayout.removeView(v);
v = null;
break;
case MotionEvent.ACTION_CANCEL:
//it comes here when i move my finger more then 100 pixels
break;
}
return true;
}
});
}
of corse, this is the relevant part of the code and not the original code
Have a look at the Experience - Android Drag and Drop List post. There is also a source code that does something very similar to what are you trying to get. Eric, the author of that post, uses a separate temporary ImageView to hold a dragged view image, taken with getDrawingCache(). He also hides the original list item by setVisibility(View.INVISIBLE).
In the DragNDrop project (link above) you can replace drag(0,y) calls by drag(x,y) in DragNDrop.java to see a dragging in all directions.
Just throwing some possibilities in because I can't see the code but MotionEvent.ACTION_CANCEL could be being called as the parent layout is switched and the view is getting redrawn.
You could try overriding the onTouchEvent and at the point the ACTION_CANCEL event is sent try get hold of an equivalent view in the new layout and operate on that but I still think the better idea is to redesign so it all takes place in the same layout.

Overriding onTouchEvent competing with ScrollView

From a simplistic overview I have a custom View that contains some bitmaps the user can drag around and resize.
The way I do this is fairly standard as in I override onTouchEvent in my CustomView and check if the user is touching within an image, etc.
My problem comes when I want to place this CustomView in a ScrollView. This works, but the ScrollView and the CustomView seem to compete for MotionEvents, i.e. when I try to drag an image it either moves sluggishly or the view scrolls.
I'm thinking I may have to extend a ScrollView so I can override onInterceptTouchEvent and let it know if the user is within the bounds of an image not to try and scroll. But then because the ScrollView is higher up in the hierarchy how would I get access to the CustomView's current state?
Is there a better way?
Normally Android uses a long press to begin a drag in cases like these since it helps disambiguate when the user intends to drag an item vs. scroll the item's container. But if you have an unambiguous signal when the user begins dragging an item, try getParent().requestDisallowInterceptTouchEvent(true) from the custom view when you know the user is beginning a drag. (Docs for this method here.) This will prevent the ScrollView from intercepting touch events until the end of the current gesture.
None of the solutions found worked "out of the box" for me, probably because my custom view extends View, not ViewGroup, and thus I can't implement onInterceptTouchEvent.
Also calling getParent().requestDisallowInterceptTouchEvent(true) was throwing NPE, or doing nothing at all.
Finally this is how I solved the problem:
Inside your custom onTouchEvent call requestDisallow... when your view will take care of the event. For example:
#Override
public boolean onTouchEvent(MotionEvent event) {
Point pt = new Point( (int)event.getX(), (int)event.getY() );
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (/*this is an interesting event my View will handle*/) {
// here is the fix! now without NPE
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
clicked_on_image = true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (clicked_on_image) {
//do stuff, drag the image or whatever
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
clicked_on_image = false;
}
return true;
}
Now my custom view works fine, handling some events and letting scrollView catch the ones we don't care about. Found the solution here: http://android-devblog.blogspot.com.es/2011/01/scrolling-inside-scrollview.html
Hope it helps.
There is an Android event called MotionEvent.ACTION_CANCEL (value = 3). All I do is override my custom control's onTouchEvent method and capture this value. If I detect this condition then I respond accordingly.
Here is some code:
#Override
public boolean onTouchEvent(MotionEvent event) {
if(isTouchable) {
int maskedAction = event.getActionMasked();
if (maskedAction == MotionEvent.ACTION_DOWN) {
this.setTextColor(resources.getColor(R.color.octane_orange));
initialClick = event.getX();
} else if (maskedAction == MotionEvent.ACTION_UP) {
this.setTextColor(defaultTextColor);
endingClick = event.getX();
checkIfSwipeOrClick(initialClick, endingClick, range);
} else if(maskedAction == MotionEvent.ACTION_CANCEL)
this.setTextColor(defaultTextColor);
}
return true;
}

Categories

Resources