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
Related
I'm writing a html page reader, cant load all .html because its huge and performance is bad, so I decided to split it into 3(or more) html and load that inside Web View nested with View Pager (swipe vertically)
My problem is that I should swipe slowly to scroll the web view and swipe fast to change view pager
Slow swiping
fast swipe change the Page on ViewPager
1) Can i expand webview to all its content heigh inside viewpager ?
2) Can change viewpager item only when topScroll or endScroll?
what i've tried so far:
CustomWebView
#Override
public boolean onTouchEvent(MotionEvent event) {
requestDisallowInterceptTouchEvent(true);
return super.onTouchEvent(event);
}
worked but was unable to change viewpager item
so i though about enable/disable it when i get to the top or to the end
removed webview onTouchEvent and added:
#Override
protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
requestDisallowInterceptTouchEvent(true);
int height = (int) Math.floor(this.getContentHeight() * this.getScale());
int webViewHeight = this.getMeasuredHeight();
boolean scrollTop = this.getTop() == t;
boolean scrollEnd = this.getScrollY() + webViewHeight >= height;
if(scrollTop || scrollEnd) {
requestDisallowInterceptTouchEvent(false);
}
worked randomly, the most common thing is that when i change page, i must scroll down and scroll up to trigger the requestDisallow to false so i can change page =[
Vertical CustomViewPage is this one ->
public class VerticalViewPager extends ViewPager {
public VerticalViewPager(Context context) {
super(context);
init();
}
public VerticalViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// The majority of the magic happens here
setPageTransformer(true, new VerticalPageTransformer());
// The easiest way to get rid of the overscroll drawing that happens on the left and right
setOverScrollMode(OVER_SCROLL_NEVER);
}
private class VerticalPageTransformer implements ViewPager.PageTransformer {
#Override
public void transformPage(View view, float position) {
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);
} else if (position <= 1) { // [-1,1]
view.setAlpha(1);
// Counteract the default slide transition
view.setTranslationX(view.getWidth() * -position);
//set Y position to swipe in from top
float yPosition = position * view.getHeight();
view.setTranslationY(yPosition);
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}
/**
* Swaps the X and Y coordinates of your touch event.
*/
private MotionEvent swapXY(MotionEvent ev) {
float width = getWidth();
float height = getHeight();
float newX = (ev.getY() / height) * width;
float newY = (ev.getX() / width) * height;
ev.setLocation(newX, newY);
return ev;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev){
boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
swapXY(ev); // return touch coordinates to original reference frame for any child views
return intercepted;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(swapXY(ev));
}
}
1st i think it's a bad idea to split your content into 3 different HTML files and putting them on 3 different WebViews.
3 fils can be a good idea but 3 WebView in 3 Views of ViewPager is a bad idea.
Instead, you should detect top and bottom of the webpage using ".js" / "jQuery" and route page/HTML within same WebView.
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
// you're at the bottom of the page
}};
There are many ways to detect TOP and BOTTOM of your HTML content.
Still, if you want to increase the height of your review to content height use
Getting WebView Content height once its loaded Android it worked for me.
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.
I am using Navigation Drawer in my app, that contains some Fragments just like in the below picture.
Every Fragment Contains another ViewPager that is an ImageSlider, and below that is a Listview and at the top I am using the SwipeRefreshLayout. My problem is the image slider works well on devices that has Android version 3.0 or higher but the swipe left or right doesn't works on devices 2.3 and lower, instead it invokes the Parent ViewPager's swipe that is it navigates the fragment. I am using support Version 4 library for this purpose to support devices lower than 3.0. All functions works quite well on 2.3 devices except that one. I have googled it but I haven't found any help anywhere. So to make it scroll what should I do for this, any idea/help will be highly appreciated.
You can use this ViewPager as your parent ViewPager. This allows the child ViewPager to scroll.
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
try {
//Handle the issue only in lower versions of android
if (v != this && v instanceof ViewPager && CJRAppCommonUtility.getOSVersion() < android.os.Build.VERSION_CODES.HONEYCOMB) {
ViewPager viewPager = (ViewPager) v;
int currentPage = viewPager.getCurrentItem();
int size = viewPager.getAdapter().getCount();
//if ViewPager has reached its end and if user tries to swipe left allow the parent to scroll
if (currentPage == (size - 1) && dx < 0) {
return false;
}
//if ViewPager has reached its start and if user tries to swipe right allow the parent to scroll
else if (currentPage == 0 && dx > 0) {
return false;
}
//Allow the child to scroll hence blocking parent scroll
else {
return true;
}
}
return super.canScroll(v, checkV, dx, x, y);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
Android developers site has a nice explanation about handling touch events in a Viewgroup. You can refer it here: http://developer.android.com/training/gestures/viewgroup.html
Hope it helps!!
In older version of Android requestDisallowInterceptTouchEvent doesn't work that great. The solution here is to extend view pager and override the onInterceptTouchEvent and store a list of children that are scrollable. Then, when onInterceptTouchEvent is called you can iterate through the list of scrollable children, get their hit rect, and see if the touch event is inside the hit rect. If it is, you can just return false to not handle it and let the child take it.
Something like this:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
for (View view : scrollableChildren)
{
// get the hit rectangle for the view
Rect rect = new Rect();
view.getHitRect(rect);
// check to see if the click was inside this child
if (rect.contains((int) ev.getX(), (int) ev.getY()))
{
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
i'm using a SemiClosedSlidingDrawer (http://pastebin.com/FtVyrcEb) and i've added on content part some buttons on the top of slider which are always visibles.
The problems is that they are clickable (or click event is dispatched) only when slider is fully opened... When slider is "semi-opened" click event not seems dispached to button... I have inspected with debugger into onInterceptTouchEvent() and in both cases (opened/semi-collapsed) the following code
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (mLocked) {
return false;
}
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
//FOLLOWING THE CRITICAL CODE
if (!mTracking && !frame.contains((int) x, (int) y)) {
return false;
}
return false but only when slider is opened event was dispached...
It checks if a (x,y) relative to the click are contained in a rectangle created starting from the HandleButton view of sliding drawer...
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
and this is obviously false because i'm clicking on a button contained inside the content part of slidingdrawer and that's ok...
As i said above the problem is that in semi-collapsed state, buttons contained in content part are not receiving the event...
Have you any idea how can i solve this issue?
Can be some state of slidingdrawer that avoid to click childs when collapsed?
Thanks in advance...
Right, I think I've figured out a way to do this.
First you need to modify onInterceptTouchEvent() to return true whenever the user presses the visible content during the semi-opened state. So, for instance, if your SemiClosedSlidingDrawer view is located at the very bottom of the screen, you can use a simple detection algorithm, like this:
public boolean onInterceptTouchEvent(MotionEvent event) {
...
handle.getHitRect(frame);
// NEW: Check if the user pressed on the "semi-open" content (below the handle):
if(!mTracking && (y >= frame.bottom) && action == MotionEvent.ACTION_DOWN) {
return true;
}
if (!mTracking && !frame.contains((int) x, (int) y)) {
...
}
Now the touch events during the user's interaction with the semi-opened content will be dispatched to onTouchEvent(). Now we just need to intercept these events and "manually" redirect them to the right view (note that we also need to offset the coordinates for the child view):
public boolean onTouchEvent(MotionEvent event) {
...
if (mTracking) {
...
}
else
{
// NEW: Dispatch events to the "semi-open" view:
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
float x = event.getX();
float y = event.getY() - frame.bottom;
MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setLocation(x, y);
return mContent.dispatchTouchEvent(newEvent);
}
return mTracking || mAnimating || super.onTouchEvent(event);
}
It's a bit of a messy implementation, but I think the basic concept is right. Let me know how it works for you!
I have a ScrollView that surrounds my entire layout so that the entire screen is scrollable. The first element I have in this ScrollView is a HorizontalScrollView block that has features that can be scrolled through horizontally. I've added an ontouchlistener to the horizontalscrollview to handle touch events and force the view to "snap" to the closest image on the ACTION_UP event.
So the effect I'm going for is like the stock android homescreen where you can scroll from one to the other and it snaps to one screen when you lift your finger.
This all works great except for one problem: I need to swipe left to right almost perfectly horizontally for an ACTION_UP to ever register. If I swipe vertically in the very least (which I think many people tend to do on their phones when swiping side to side), I will receive an ACTION_CANCEL instead of an ACTION_UP. My theory is that this is because the horizontalscrollview is within a scrollview, and the scrollview is hijacking the vertical touch to allow for vertical scrolling.
How can I disable the touch events for the scrollview from just within my horizontal scrollview, but still allow for normal vertical scrolling elsewhere in the scrollview?
Here's a sample of my code:
public class HomeFeatureLayout extends HorizontalScrollView {
private ArrayList<ListItem> items = null;
private GestureDetector gestureDetector;
View.OnTouchListener gestureListener;
private static final int SWIPE_MIN_DISTANCE = 5;
private static final int SWIPE_THRESHOLD_VELOCITY = 300;
private int activeFeature = 0;
public HomeFeatureLayout(Context context, ArrayList<ListItem> items){
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
setFadingEdgeLength(0);
this.setHorizontalScrollBarEnabled(false);
this.setVerticalScrollBarEnabled(false);
LinearLayout internalWrapper = new LinearLayout(context);
internalWrapper.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
internalWrapper.setOrientation(LinearLayout.HORIZONTAL);
addView(internalWrapper);
this.items = items;
for(int i = 0; i< items.size();i++){
LinearLayout featureLayout = (LinearLayout) View.inflate(this.getContext(),R.layout.homefeature,null);
TextView header = (TextView) featureLayout.findViewById(R.id.featureheader);
ImageView image = (ImageView) featureLayout.findViewById(R.id.featureimage);
TextView title = (TextView) featureLayout.findViewById(R.id.featuretitle);
title.setTag(items.get(i).GetLinkURL());
TextView date = (TextView) featureLayout.findViewById(R.id.featuredate);
header.setText("FEATURED");
Image cachedImage = new Image(this.getContext(), items.get(i).GetImageURL());
image.setImageDrawable(cachedImage.getImage());
title.setText(items.get(i).GetTitle());
date.setText(items.get(i).GetDate());
internalWrapper.addView(featureLayout);
}
gestureDetector = new GestureDetector(new MyGestureDetector());
setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
return true;
}
else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){
int scrollX = getScrollX();
int featureWidth = getMeasuredWidth();
activeFeature = ((scrollX + (featureWidth/2))/featureWidth);
int scrollTo = activeFeature*featureWidth;
smoothScrollTo(scrollTo, 0);
return true;
}
else{
return false;
}
}
});
}
class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
try {
//right to left
if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
activeFeature = (activeFeature < (items.size() - 1))? activeFeature + 1:items.size() -1;
smoothScrollTo(activeFeature*getMeasuredWidth(), 0);
return true;
}
//left to right
else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
activeFeature = (activeFeature > 0)? activeFeature - 1:0;
smoothScrollTo(activeFeature*getMeasuredWidth(), 0);
return true;
}
} catch (Exception e) {
// nothing
}
return false;
}
}
}
Update: I figured this out. On my ScrollView, I needed to override the onInterceptTouchEvent method to only intercept the touch event if the Y motion is > the X motion. It seems like the default behavior of a ScrollView is to intercept the touch event whenever there is ANY Y motion. So with the fix, the ScrollView will only intercept the event if the user is deliberately scrolling in the Y direction and in that case pass off the ACTION_CANCEL to the children.
Here is the code for my Scroll View class that contains the HorizontalScrollView:
public class CustomScrollView extends ScrollView {
private GestureDetector mGestureDetector;
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(context, new YScrollDetector());
setFadingEdgeLength(0);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
}
// Return false if we're scrolling in the x direction
class YScrollDetector extends SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return Math.abs(distanceY) > Math.abs(distanceX);
}
}
}
Thank you Joel for giving me a clue on how to resolve this problem.
I have simplified the code(without need for a GestureDetector) to achieve the same effect:
public class VerticalScrollView extends ScrollView {
private float xDistance, yDistance, lastX, lastY;
public VerticalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if(xDistance > yDistance)
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
I think I found a simpler solution, only this uses a subclass of ViewPager instead of (its parent) ScrollView.
UPDATE 2013-07-16: I added an override for onTouchEvent as well. It could possibly help with the issues mentioned in the comments, although YMMV.
public class UninterceptableViewPager extends ViewPager {
public UninterceptableViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean ret = super.onInterceptTouchEvent(ev);
if (ret)
getParent().requestDisallowInterceptTouchEvent(true);
return ret;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
boolean ret = super.onTouchEvent(ev);
if (ret)
getParent().requestDisallowInterceptTouchEvent(true);
return ret;
}
}
This is similar to the technique used in android.widget.Gallery's onScroll().
It is further explained by the Google I/O 2013 presentation Writing Custom Views for Android.
Update 2013-12-10: A similar approach is also described in a post from Kirill Grouchnikov about the (then) Android Market app.
I've found out that somethimes one ScrollView regains focus and the other loses focus. You can prevent that, by only granting one of the scrollView focus:
scrollView1= (ScrollView) findViewById(R.id.scrollscroll);
scrollView1.setAdapter(adapter);
scrollView1.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
scrollView1.getParent().requestDisallowInterceptTouchEvent(true);
return false;
}
});
It wasn't working well for me. I changed it and now it works smoothly. If anyone interested.
public class ScrollViewForNesting extends ScrollView {
private final int DIRECTION_VERTICAL = 0;
private final int DIRECTION_HORIZONTAL = 1;
private final int DIRECTION_NO_VALUE = -1;
private final int mTouchSlop;
private int mGestureDirection;
private float mDistanceX;
private float mDistanceY;
private float mLastX;
private float mLastY;
public ScrollViewForNesting(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
}
public ScrollViewForNesting(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScrollViewForNesting(Context context) {
this(context,null);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDistanceY = mDistanceX = 0f;
mLastX = ev.getX();
mLastY = ev.getY();
mGestureDirection = DIRECTION_NO_VALUE;
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
mDistanceX += Math.abs(curX - mLastX);
mDistanceY += Math.abs(curY - mLastY);
mLastX = curX;
mLastY = curY;
break;
}
return super.onInterceptTouchEvent(ev) && shouldIntercept();
}
private boolean shouldIntercept(){
if((mDistanceY > mTouchSlop || mDistanceX > mTouchSlop) && mGestureDirection == DIRECTION_NO_VALUE){
if(Math.abs(mDistanceY) > Math.abs(mDistanceX)){
mGestureDirection = DIRECTION_VERTICAL;
}
else{
mGestureDirection = DIRECTION_HORIZONTAL;
}
}
if(mGestureDirection == DIRECTION_VERTICAL){
return true;
}
else{
return false;
}
}
}
Thanks to Neevek his answer worked for me but it doesn't lock the vertical scrolling when user has started scrolling the horizontal view(ViewPager) in horizontal direction and then without lifting the finger scroll vertically it starts to scroll the underlying container view(ScrollView). I fixed it by making a slight change in Neevak's code:
private float xDistance, yDistance, lastX, lastY;
int lastEvent=-1;
boolean isLastEventIntercepted=false;
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if(isLastEventIntercepted && lastEvent== MotionEvent.ACTION_MOVE){
return false;
}
if(xDistance > yDistance )
{
isLastEventIntercepted=true;
lastEvent = MotionEvent.ACTION_MOVE;
return false;
}
}
lastEvent=ev.getAction();
isLastEventIntercepted=false;
return super.onInterceptTouchEvent(ev);
}
This finally became a part of support v4 library, NestedScrollView. So, no longer local hacks is needed for most of cases I'd guess.
Neevek's solution works better than Joel's on devices running 3.2 and above. There is a bug in Android that will cause java.lang.IllegalArgumentException: pointerIndex out of range if a gesture detector is used inside a scollview. To duplicate the issue, implement a custom scollview as Joel suggested and put a view pager inside. If you drag (don't lift you figure) to one direction (left/right) and then to the opposite, you will see the crash. Also in Joel's solution, if you drag the view pager by moving your finger diagonally, once your finger leave the view pager's content view area, the pager will spring back to its previous position. All these issues are more to do with Android's internal design or lack of it than Joel's implementation, which itself is a piece of smart and concise code.
http://code.google.com/p/android/issues/detail?id=18990
Date : 2021 - May 12
Looks jibberish..but trust me its worth the time if you wanna scroll any view horizontally in a vertical scrollview butter smooth!!
Works in jetpack compose as well by by making a custom view and extending the view that you wanna scroll horizontally in; inside a vertical scroll view and using that custom view inside AndroidView composable (Right now, "Jetpack Compose is in 1.0.0-beta06"
This is the most optimal solution if you wanna scroll horizontally freely and vertically freely without the vertical scrollbar intercepting ur touch when u are scrolling horizontally and only allowing the vertical scrollbar to intercept the touch when u are scrolling vertically through the horizontal scrolling view :
private class HorizontallyScrollingView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ViewThatYouWannaScrollHorizontally(context, attrs){
override fun onTouchEvent(event: MotionEvent?): Boolean {
// When the user's finger touches the webview and starts moving
if(event?.action == MotionEvent.ACTION_MOVE){
// get the velocity tracker object
val mVelocityTracker = VelocityTracker.obtain();
// connect the velocity tracker object with the event that we are emitting while we are touching the webview
mVelocityTracker.addMovement(event)
// compute the velocity in terms of pixels per 1000 millisecond(i.e 1 second)
mVelocityTracker.computeCurrentVelocity(1000);
// compute the Absolute Velocity in X axis
val xVelocityABS = abs(mVelocityTracker.getXVelocity(event?.getPointerId((event?.actionIndex))));
// compute the Absolute Velocity in Y axis
val yVelocityABS = abs(mVelocityTracker.getYVelocity(event?.getPointerId((event?.actionIndex))));
// If the velocity of x axis is greater than y axis then we'll consider that it's a horizontal scroll and tell the parent layout
// "Hey parent bro! im scrolling horizontally, this has nothing to do with ur scrollview so stop capturing my event and stay the f*** where u are "
if(xVelocityABS > yVelocityABS){
// So, we'll disallow the parent to listen to any touch events until i have moved my fingers off the screen
parent.requestDisallowInterceptTouchEvent(true)
}
} else if (event?.action == MotionEvent.ACTION_CANCEL || event?.action == MotionEvent.ACTION_UP){
// If the touch event has been cancelled or the finger is off the screen then reset it (i.e let the parent capture the touch events on webview as well)
parent.requestDisallowInterceptTouchEvent(false)
}
return super.onTouchEvent(event)
}
}
Here, ViewThatYouWannaScrollHorizontally is the view that you want to scroll horizontally in and when u are scrolling horizontally, you dont want the vertical scrollbar to capture the touch and think that "oh! the user is scrolling vertically so parent.requestDisallowInterceptTouchEvent(true) will basically say the vertical scroll bar "hey you! dont capture any touch coz the user is scrolling horizontally"
And after the user is done scrolling horizontally and tries to scroll vertically through the horizontal scrollbar which is placed inside a vertical scrollbar then it will see that the touch velocity in Y axis is greater than X axis, which shows user is not scrolling horizontally and the horizontal scrolling stuff will say "Hey you! parent, you hear me?..the user is scrolling vertically through me, now u can intercept the touch and show the stuffs present below me in the vertical scroll"