I've implemented SwipeRefreshLayout and ViewPager in my app but there is a big trouble: whenever I'm going to swipe left / right to switch between pages the scrolling is too sensitive. A little swipe down will trigger the SwipeRefreshLayout refresh too.
I want to set a limit to when horizontal swipe starts, then force horizontal only until swiping is over. In other words, I want to cancel vertical swipping when finger is moving horizontally.
This problem only occurs on ViewPager, if I swipe down and SwipeRefreshLayout refresh function is triggered (the bar is shown) and then I move my finger horizontally, it still only allows vertical swipes.
I've tried to extend the ViewPager class but it isn't working at all:
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean in = super.onInterceptTouchEvent(ev);
if (in) {
getParent().requestDisallowInterceptTouchEvent(true);
this.requestDisallowInterceptTouchEvent(true);
}
return false;
}
}
Layout xml:
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/viewTopic"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.myapp.listloader.foundation.CustomViewPager
android:id="#+id/topicViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
any help would be appreciated, thanks
I am not sure if you still have this issue but Google I/O app iosched solves this problem thusly:
viewPager.addOnPageChangeListener( new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled( int position, float v, int i1 ) {
}
#Override
public void onPageSelected( int position ) {
}
#Override
public void onPageScrollStateChanged( int state ) {
enableDisableSwipeRefresh( state == ViewPager.SCROLL_STATE_IDLE );
}
} );
private void enableDisableSwipeRefresh(boolean enable) {
if (swipeContainer != null) {
swipeContainer.setEnabled(enable);
}
}
I have used the same and works quite well.
EDIT: Use addOnPageChangeListener() instead of setOnPageChangeListener().
Solved very simply without extending anything
mPager.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
mLayout.setEnabled(false);
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
mLayout.setEnabled(true);
break;
}
return false;
}
});
work like a charm
I've met your problem. Customize the SwipeRefreshLayout would solve the problem.
public class CustomSwipeToRefresh extends SwipeRefreshLayout {
private int mTouchSlop;
private float mPrevX;
public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = MotionEvent.obtain(event).getX();
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (xDiff > mTouchSlop) {
return false;
}
}
return super.onInterceptTouchEvent(event);
}
See the ref: link
I based this off a previous answer but found this to work a bit better. The motion starts with an ACTION_MOVE event and ends in either ACTION_UP or ACTION_CANCEL in my experience.
mViewPager.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mSwipeRefreshLayout.setEnabled(false);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mSwipeRefreshLayout.setEnabled(true);
break;
}
return false;
}
});
For some reason best known only to them, the support library developer team saw fit to forcefully intercept all vertical drag motion events from SwipeRefreshLayout's child layout, even when a child specifically requests ownership of the event. The only thing they check for is that vertical scroll state of it's main child is at zero (in the case that it's child is vertically scrollable). The requestDisallowInterceptTouchEvent() method has been overridden with an empty body, and the (not so) illuminating comment "Nope".
The easiest way to solve this issue would be to just copy the class from the support library into your project and remove the method override. ViewGroup's implementation uses internal state for handling onInterceptTouchEvent(), so you cannot simply override the method again and duplicate it. If you really want to override the support library implementation, then you will have to set up a custom flag upon calls to requestDisallowInterceptTouchEvent(), and override onInterceptTouchEvent() and onTouchEvent() (or possibly hack canChildScrollUp()) behavior based on that.
There is one problem with the solution of nhasan:
If the horizontal swipe that triggers the setEnabled(false) call on the SwipeRefreshLayout in the OnPageChangeListener happens when the SwipeRefreshLayout has already recognized a Pull-to-Reload but has not yet called the notification callback, the animation disappears but the internal state of the SwipeRefreshLayout stays on "refreshing" forever as no notification callbacks are called that could reset the state. From a user perspective this means that Pull-to-Reload is not working anymore as all pull gestures are not recognized.
The problem here is that the disable(false) call removes the animation of the spinner and the notification callback is called from the onAnimationEnd method of an internal AnimationListener for that spinner which is set out of order that way.
It took admittedly our tester with the fastest fingers to provoke this situation but it can happen once in a while in realistic scenarios as well.
A solution to fix this is to override the onInterceptTouchEvent method in SwipeRefreshLayout as follows:
public class MySwipeRefreshLayout extends SwipeRefreshLayout {
private boolean paused;
public MySwipeRefreshLayout(Context context) {
super(context);
setColorScheme();
}
public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setColorScheme();
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (paused) {
return false;
} else {
return super.onInterceptTouchEvent(ev);
}
}
public void setPaused(boolean paused) {
this.paused = paused;
}
}
Use the MySwipeRefreshLayout in your Layout - File and change the code in the solution of mhasan to
...
#Override
public void onPageScrollStateChanged(int state) {
swipeRefreshLayout.setPaused(state != ViewPager.SCROLL_STATE_IDLE);
}
...
I've found a solution for ViewPager2. I use reflection for reducing drag sensitivity like this:
/**
* Reduces drag sensitivity of [ViewPager2] widget
*/
fun ViewPager2.reduceDragSensitivity() {
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(this) as RecyclerView
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
touchSlopField.isAccessible = true
val touchSlop = touchSlopField.get(recyclerView) as Int
touchSlopField.set(recyclerView, touchSlop*8) // "8" was obtained experimentally
}
It works like a charm for me.
2020-10-17
a minimal addition to #nhasan perfect answer.
if you have migrated from ViewPager to ViewPager2, use
registerOnPageChangeCallback method for listening scroll events
mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
}
});
There could be a problem with #huu duy answer when the ViewPager is placed in a vertically-scrollable container which, in turn, is placed in the SwiprRefreshLayout
If the content scrollable container is not fully scrolled-up, then it may be not possible to activate swipe-to-refresh in the same scroll-up gesture.
Indeed, when you start scrolling the inner container and move finger horizontally more then mTouchSlop unintentionally (which is 8dp by default),
the proposed CustomSwipeToRefresh declines this gesture. So a user has to try once more to start refreshing.
This may look odd for the user.
I extracted the source code f the original SwipeRefreshLayout from the support library to my project and re-wrote the onInterceptTouchEvent().
private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
private boolean mPendingActionDown;
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || mRefreshing ) {
// Fail fast if we're not in a state where a swipe is possible
if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
mActivePointerId = ev.getPointerId(0);
if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {
if (mNestedScrollInProgress || canChildScrollUp()) {
if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
mPendingActionDown = true;
} else {
mInitialDownX = ev.getX(pointerIndex);
mInitialDownY = ev.getY(pointerIndex);
}
}
return false;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
} else if (mGestureDeclined) {
if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
return false;
} else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
return false;
} else if (mNestedScrollInProgress || canChildScrollUp()) {
if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
return false;
} else if (mPendingActionDown) {
// This is the 1-st Move after content stops scrolling.
// Consider this Move as Down (a start of new gesture)
if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
mPendingActionDown = false;
mInitialDownX = ev.getX(pointerIndex);
mInitialDownY = ev.getY(pointerIndex);
return false;
} else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
mGestureDeclined = true;
if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
if (!mIsBeingDragged) {
if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
} else {
if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mGestureDeclined = false;
mPendingActionDown = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
See my example project on Github.
Related
I have a View Pager (VP) which contains a Horizontal Scroll View (HSV). If the HSV reaches one of its edges or is not able to scroll at all, on a new swipe in the blocked direction VP should take over scrolling to the next page. I hesitated to ask this question because I found similar ones like these:
Can I use Horizontal Scrollview Inside a Viewpager in Android?
or
horizontalscrollview inside viewpager
But the solution did not work for me. 'v instanceof HorizontalScrollView' gets true but viewPager does not scroll
Any other ideas how to achieve the desired behaviour?
public class MyViewPager extends ViewPager {
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Update 1
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
//return super.onInterceptTouchEvent(ev);
}
/**
* https://stackoverflow.com/questions/22781496/can-i-use-horizontal-scrollview-inside-a-viewpager-in-android
*/
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof HorizontalScrollView) {
return true;
}
return super.canScroll(v, checkV, dx, x, y);
}
}
child view: view_pager_page.xml:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<include layout="#layout/" />
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</FrameLayout>
</RelativeLayout>
parent view: view_pager.xml
<android.support.design.widget.CoordinatorLayout>
...
<LinearLayout>
<packagepath.MyViewPager
android:layout_width="match_parent"
android:layout_height="wrap_content">
</packagepath.MyViewPager>
</LinearLayout>
...
<android.support.design.widget.CoordinatorLayout>
Update 1: When overriding 'onInterceptTouchEvent' and let it always return true VP scrolls, but HSV doesn't. I think this must return true only if HSV reaches edges right? How can I figure out in this method if it is the case?
Update 2: I reconstructed the touch event mechanism of android hoping to get some insight of how to intercept the motion event flow. E.g. in HSV I can simply return false to let VP consume this and all subsequent motion events. Unfortunately I need two motion events of type MotionEvent.MOVE to decide if HSV or VP should scroll when reaching an edge (if HSV has reached right edge, a right swipe scrolls HSV back and a left swipe scrolls to next page of VP). But if I skip the MotionEvent.DOWN action neither HSV or VP starts scrolling... so hard to solve. Any ideas?
Touchevent Mechanism in Android
(Warning: Graphic is not complete and will contain mistakes, everyone is invited to correct it :-))
Update 3: Finally I got it working. Understanding the Touchevent mechanism helped a lot and also the first comment of ZeroOne. I will post my solution when I have time for it.
I solved this with a custom HorizontalScrollView. The key is to override the onTouchEvent() method and return false if you are at the left edge and swiping right, or the right edge and swiping left. Returning false means this view didn't consume the touch event and this event can bubble back up the view hierarchy to be handled by the ViewPager.
public class HorizontalScrollViewForViewPager extends HorizontalScrollView {
float old_x, old_y;
public HorizontalScrollViewForViewPager(Context context) {
super(context);
}
public HorizontalScrollViewForViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalScrollViewForViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
//Start of touch. Could be tap, could be drag.
old_x = ev.getX();
old_y = ev.getY();
} else if (action == MotionEvent.ACTION_MOVE) {
//Drag movement underway
float deltaX = ev.getX() - old_x;
float deltaY = ev.getY() - old_y;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//scrolling more left/right than up/down
if (deltaX > 0 && getScrollX() == 0) {
//dragging left, at left edge of HorizontalScrollView. Don't handle this touch event, let it bubble up to ViewPager
return false;
} else {
//dragging right. Use first child to determine width of content inside HorizontalScrollView
int childWidth = getChildAt(0).getWidth();
if (deltaX < 0 && (this.getScrollX() + this.getWidth()) >= childWidth) {
//swiping left, and at right edge of HorizontalScrollView. Don't handle this touch event, let it bubble up to ViewPager
return false;
}
}
}
}
return super.onTouchEvent(ev);
}
}
1.Extend ViewPager Class:
public class ViewPagerContainingHorizontalScrollView extends ViewPager {
private Float x_old;
private boolean bDoIntercept = false;
private boolean bHsvRightEdge = false;
private boolean bHsvLeftEdge = true;
public ViewPagerContainingHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private float calculateDistanceSwipe(MotionEvent ev){
float distance = 0;
if (x_old == null) {
x_old = ev.getX();
} else {
distance = ev.getX() - x_old;
x_old = null;
}
return distance;
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
mDoIntercept = false;
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
float distance = calculateDistanceSwipe(ev);
if (distance < 0) {//scrolling left direction
if (bHsvRightEdge) { //HSV right edge
bDoIntercept = true;
//When scrolling slow VP may not switch page.
//Then HSV snaps back into old position.
//To allow HSV to scroll into non blocked direction set following to false.
bHsvRightEdge = false;
}
bHsvLeftEdge = false;//scrolling left means left edge not reached
} else if (distance > 0) {//scrolling right direction
if (bHsvLeftEdge) { //HSV left edge
bDoIntercept = true;
//When scrolling slow VP may not switch page.
//Then HSV snaps back into old position.
//To allow HSV to scroll into non blocked direction set following to false.
bHsvLeftEdge = false;
}
bHsvRightEdge = false;//scrolling right means right edge not reached
}
}
return super.dispatchTouchEvent(ev);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(bDoIntercept){
return true;
}else{
return super.onInterceptTouchEvent(ev);
}
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof HorizontalScrollView) {
HorizontalScrollView hsv = (HorizontalScrollView) v;
int max_scrollX = hsv.getChildAt(0).getWidth() - hsv.getWidth();
int min_scrollX = 0;
int current_scroll_x = hsv.getScrollX();
if (current_scroll_x == max_scrollX) { //HSV right edge
bHsvRightEdge = true;
}
if (current_scroll_x == min_scrollX) { //HSV left edge
bHsvLeftEdge = true;
}
return true;
}
return super.canScroll(v, checkV, dx, x, y);
}
}
Use this custom VP in XML.
Enjoy nested HSV scrolling in VP :-)
Touch Event Mechanism Overview for this specific case
I am finishing my live wallpaper and it is working well apart from performance issues, for now I applied a script that helps for swipe gestures and touch detection.
public void onTouchEvent(MotionEvent touchevent)
{
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext());
if (prefs.getBoolean("Touch_lock_key",true))
{
switch (touchevent.getAction())
{
// when user first touches the screen we get x and y coordinate
case MotionEvent.ACTION_DOWN:
{
x1 = touchevent.getX();
y1 = touchevent.getY();
break;
}
case MotionEvent.ACTION_MOVE:
{
x2 = touchevent.getX();
y2 = touchevent.getY();
//right swipe
if (x1-x2< 0 )
{
try {
mImagesArrayIndex++;
Thread.sleep(SeekBarPreference1.mCurrentValue1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// left swipe
if (x1-x2>0 )
{
try {
mImagesArrayIndex--;
Thread.sleep(SeekBarPreference1.mCurrentValue1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
break;
}
}
return;
}
}
As you can see I utilize .getaction function to manage touch and swipe gestures. Its working but not as I expected. The problems are:
ultra sensitive to the touch and becomes annoying when using this live wallpaper.
doesn't follow left-right swipes properly.
left-right swipes doesn't stop when finger swipe stops across the screen.
What I want:
any other swipe gesture manager script that might help with my case (live wallpaper - no activity is involved here)
smooth swipe effect like we see on any live wallpapers out there, take the titanfall LW as a sample.
Here's a video that might help you understand the situation:
https://drive.google.com/file/d/0B44QwXQHh5irOGlXNDNpOThYb3c/view?usp=sharing
onTouch() is called several times for any given gesture, so you're incrementing much more than you want to. A start would be to use GestureDetector and GestureDetector.SimpleGestureListener.
Sic:
private GestureDetector mDetector
...
#Override
protected void onCreate(...) {
...
mDetector = new GestureDetector(this, mGestureListener);
setOnTouchListener(new CustomTouchListener());
...
}
...
private class CustomTouchListener implements View.OnTouchListener {
#Override
public boolean onTouch(View v, MotionEvent e) {
return mDetector.onTouchEvent(e);
}
}
...
private GestureDetector.SimpleOnGestureListener mGestureListener
= new GestureDetector.SimpleOnGestureListener {
#Override
public boolean onDown(MotionEvent e) {
...
}
// the IDE will create all these methods for you
//EDIT: I forgot SimpleOnGestureListener isn't abstract, so it won't do this. OnGestureListener is, so the IDE will.
};
The basic sequence of a swipe looks like this
onDown()
onScroll()
onScroll()
....
onScroll()
onFling()
So if you really want to handle the gesture yourself, the events you're looking for are onDown and on Fling (you could do this with onTouch ACTION_DOWN and ACTION_UP, but SimpleOnGestureListener gives you more functions).
But I would really suggest looking into a view that already handles all this for you, like ViewPager
I found a solution, this code works the same as the previous one I just added some modifications concerning precision and touch tracking..
#Override
public void onTouchEvent(MotionEvent event) {
//when user touches the screen
if(event.getAction() == MotionEvent.ACTION_DOWN)
{
//reset deltaX and deltaY
deltaX=0;
//get initial positions
initialX = event.getRawX();
initialY = event.getRawY();
}
//swipe gestures tracking
if(event.getAction() == MotionEvent.ACTION_MOVE)
{
deltaX = event.getRawX() - initialX;
deltaY = event.getRawY() - initialY;
if(deltaY < 0)
{
//Actions UP
}
else
{
//Actions DOWN
}
if(deltaX < 0)
{
initialX = event.getRawX();
// actions RIGHT
}
}
else
{
initialX = event.getRawX();
// actions LEFT
}
}
return;
}
}
here's a result:
https://drive.google.com/file/d/0B44QwXQHh5irZUtEM3ZZQmRHcFk/view?usp=sharing
I hope someone finds this helpful &... HAPPY CODING !
I am currently trying to implement Drag and Drop in Android. The draging should be done on an item from a ListView. The thing is that the elements(items) in this ListView should have some sort of action when they are touched (single taped) and they should be dragable after the user perform a long press on one of the items. I got the code for draging from a tutorial but the problem that i cannot solve is:
After the user holds his finger on an item from the ListView the onTouch(MotionEvent ev) is called at the beginning and after a second GestureDetector.onLongPress is called.
My draging logic is implemented in the onTouch methods. What i currently do is set a boolean (isLongPressed) to true after LongPress is enabled and in the onTouch methods i check if this boolean is true - if yes then perform the draging.
Here is my code :
public class DragNDropListView extends ListView implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener
{
private boolean isLongPressed
#Override
public boolean onTouchEvent(MotionEvent ev)
{
mGestureDetector.onTouchEvent(ev);
if (!isLongPressed)
{
Log.e("Not Long", "Its Not Longpressed yet");
return super.onTouchEvent(ev);
}
final int action = ev.getAction();
final int x = (int) ev.getX();
final int y = (int) ev.getY();
if (action == MotionEvent.ACTION_DOWN && isLongPressed)
{
mDragMode = true;
}
if (!mDragMode)
return super.onTouchEvent(ev);
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e("Action down", "Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION MOVE", "Move");
drag()
break;
case MotionEvent.ACTION_UP:
{
isLongPressed = false;
}
}
#Override
public void onLongPress(MotionEvent ev)
{
Log.e("LongPress", "LongPressed");
isLongPressed = true;
mGestureDetector.setIsLongpressEnabled(isLongPressed);
super.onTouchEvent(ev);
}
What happens is that i get "isNotLongPressed" if i just tab the items (desired behavior), After a long press i get "LongPressed" but then if i start draging i get nothing. If i release my finger and then start the drag everything is ok but i want to be able to drag immediately after the long press is registered.
I have a grid of buttons in my activity. When the user slides their finger across this grid of buttons, I want all of the buttons touched to be essentially recorded so I know which buttons were touched.
I have been doing research on this and I do not see how this can be done, as most examples are with Bitmaps. The first view that it selected is the only one that is picked up from my OnTouchListener. From my readings, MotionEvent.ACTION_MOVE is what is to be used for this and I saw someone who said to use view.getRawX() and view.getRawY() but I don't understand how this would be used to determine which other buttons were pressed and which were not when the user is swiping their finger on the screen.
I am new to Android so my apologies if this is a much simpler task than I thought. Any input would be much appreciated as I wouldn't think this should be so complicated. Thank you for your time! :)
Once your View returns true to say that it's consuming the touch event, the rest of the views won't receive it. What you can do is make a custom ViewGroup (you say you have a grid, I'll just assume GridView?) that intercepts and handles all touch events:
public class InterceptingGridView extends GridView {
private Rect mHitRect = new Rect();
public InterceptingGridView (Context context) {
super(context);
}
public InterceptingGridView (Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent (MotionEvent ev) {
//Always let the ViewGroup handle the event
return true;
}
#Override
public boolean onTouchEvent (MotionEvent ev) {
int x = Math.round(ev.getX());
int y = Math.round(ev.getY());
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.getHitRect(mHitRect);
if (mHitRect.contains(x, y)) {
/*
* Dispatch the event to the containing child. Note that using this
* method, children are not guaranteed to receive ACTION_UP, ACTION_CANCEL,
* or ACTION_DOWN events and should handle the case where only an ACTION_MOVE is received.
*/
child.dispatchTouchEvent(ev);
}
}
//Make sure to still call through to the superclass, so that
//the ViewGroup still functions normally (e.g. scrolling)
return super.onTouchEvent(ev);
}
}
How you choose to handle the event depends on the logic that you require, but the takeaway is to let the container view consume all of the touch events, and let it handle dispatching the events to the children.
maybe this will help you:
#Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
int pointerId = event.getPointerId(pointerIndex);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
}
break;
}
return true;
}
it works for multitouch
My class extends View and I need to get continuous touch events on it.
If I use:
public boolean onTouchEvent(MotionEvent me) {
if(me.getAction()==MotionEvent.ACTION_DOWN) {
myAction();
}
return true;
}
... the touch event is captured once.
What if I need to get continuous touches without moving the finger?
Please, tell me I don't need to use threads or timers. My app is already too much heavy.
Thanks.
Use if(me.getAction() == MotionEvent.ACTION_MOVE). It's impossible to keep a finger 100% completely still on the screen so Action_Move will get called every time the finger moves, even if it's only a pixel or two.
You could also listen for me.getAction() == MotionEvent.ACTION_UP - until that happens, the user must still have their finger on the screen.
You need to set this properties for the element
android:focusable="true"
android:clickable="true"
if not, just produce the down action.
Her is the simple code snippet which shows that how you can handle the continues touch event. When you touch the device and hold the touch and move your finder, the Touch Move action performed.
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if(isTsunami){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Write your code to perform an action on down
break;
case MotionEvent.ACTION_MOVE:
// Write your code to perform an action on contineus touch move
break;
case MotionEvent.ACTION_UP:
// Write your code to perform an action on touch up
break;
}
}
return true;
}
Try this. It works to me:
public static OnTouchListener loadContainerOnTouchListener() {
OnTouchListener listener = new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
LinearLayout layout = (LinearLayout)v;
for(int i =0; i< layout.getChildCount(); i++)
{
View view = layout.getChildAt(i);
Rect outRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
if(outRect.contains((int)event.getX(), (int)event.getY()))
{
Log.d(this.getClass().getName(), String.format("Over view.id[%d]", view.getId()));
}
}
}
Remember: the listener you´ll set must be a container layout (Grid, Relative, Linear).
LinearLayout layout = findViewById(R.id.yourlayoutid);
layout.setOnTouchListener(HelperClass.loadContainerOnTouchListener());
This might help,
requestDisallowInterceptTouchEvent(true);
on the parent view, like this -
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.getParent().requestDisallowInterceptTouchEvent(true);
switch(motionEvent.getAction()){
}
return false;
}
I was making a game with a custom view used as a thumb control. . . here is what I did
float x = 0, y = 0;
#Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX();
y = event.getY();
// handle touch events with
switch( event.getActionMasked() ) {
case MotionEvent.ACTION_DOWN :
if(cont)
{
// remove any previous callbacks
removeCallbacks(contin);
// post new runnable
postDelayed(contin, 10);
}
invalidate();
return true;
case MotionEvent.ACTION_MOVE :
if(!cont && thumbing != null)
{
// do non-continuous operations here
}
invalidate();
return true;
case MotionEvent.ACTION_UP :
// set runnable condition to false
x = 0;
// remove the callbacks to the thread
removeCallbacks(contin);
invalidate();
return true;
default :
return super.onTouchEvent(event);
}
}
public boolean cont = false;
// sets input to continuous
public void set_continuous(boolean b) { cont = b; }
public Runnable contin = new Runnable()
{
#Override
public void run() {
if(x != 0)
{
// do continuous operations here
postDelayed(this, 10);
}
}
};
A quick note however, make sure in your main activity that is calling this view removes the callbacks manually via the onPause method as follows
#Override
protected void onPause() {
if(left.cont) left.removeCallbacks(left.contin);
if(right.cont) right.removeCallbacks(left.contin);
super.onPause();
}
That way if you pause and come back touch events aren't being handled twice and the view is free from it's thread's overhead.
** tested on Samsung Galaxy S3 with hardware acceleration on **
All these answer are partially correct but they do not resolve in the right way the problem.
First of all, for everyone out there that decide to track when the event is ACTION_MOVE. Well that works only guess when? When user move his finger, so could if you decide to implement a custom thumb control is okay but for a normal custom button that's not the case.
Second, using a flag inside ACTION_DOWN and check it in ACTION_UP seems the logic way to do it, but as Clusterfux find out if you implement a while(!up_flag) logic you get stuck into troubles ;)
So the proper way to do it is mentioned here:
Continuous "Action_DOWN" in Android
Just keep in mind that if the logic you're going to write during the continuous press has to modify the UI in some way, you have to do it from the main thread in all the other cases it's better use another thread.
You can use the below code snippet as a reference in which I used the background to detect if the screen is held or not...
Main_Layout.setOnTouchListener(new View.OnTouchListener() {
#SuppressLint("ResourceAsColor")
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
Main_Layout.setBackgroundColor(R.color.green);
event.setAction(MotionEvent.ACTION_DOWN);
break;
default:
Main_Layout.setBackgroundColor(R.color.blue);
break;
}
return false;
}
});