I am putting a Custom WebView inside of a Custom ViewPager:
https://github.com/JakeWharton/Android-DirectionalViewPager
I've set the ViewPager to page in the vertical direction, which is the same direction my WebView scrolls, how ever the ViewPager intercepts all touch events.
So how it should work is that the WebView scrolls till it reaches the end, then once it is at the end of its scroll, the ViewPager should be allowed to page to the next page.
I guess, in the ViewPager, I need to find out, when a touch event occurs, the list of child Views that could potentially respond to the event, see if they are scrollable and respond appropriately.
How can I find out this list of potential child views that would receive the touch event if the ViewPager ignored the touch event?
Take a cue from how Android does their ViewPager.
/**
* Tests scrollability within child views of v given a delta of dx.
*
* #param v View to test for horizontal scrollability
* #param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
* #param dx Delta scrolled in pixels
* #param x X coordinate of the active touch point
* #param y Y coordinate of the active touch point
* #return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
It literally goes through the entire view hierarchy and does a hit test to see if the touch lies within the view bounds, and if so, check if that view can scroll.
Related
Using something similar to the answer of this question, I've tried to disable the ViewPager swipe action for when the user is swiping over a particular item. The view in question is a scrollable chart from the MPAndroidChart library, so naturally I don't want the view pager interfering with the scrolling of the chart.
The issue I am having is that the ViewPager will often have "onInterceptTouch" invoked before the onTouchListener is invoked on my desired view.
In this segment of code, I'm recording when the view is pressed/unpressed:
private long lastDown;
private long lastUp;
...
public void foo(){
barChart.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("AAA");
if(event.getAction() == MotionEvent.ACTION_DOWN){
lastDown = System.currentTimeMillis();
}else if(event.getAction() == MotionEvent.ACTION_UP){
lastUp = System.currentTimeMillis();
}
return false;
}
});
}
In this segment of code, I determine if the view is selected:
public boolean isGraphTouched(){
return lastDown > lastUp;
}
And in this segment of code I'm overriding the onInterceptTouchEvent method:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
System.out.println("BBB");
return isGraphSelected() ? super.onInterceptTouchEvent(ev) : false;
}
And if you take note of the printlines, the onInterceptTouchEvent method is called before...
The only way I can think of getting around this is to make a method which checks if the graph exists at the coordinates of the motion event (although I'm not sure if this is even possible) and then use that to determine if the pager will be swipable or not.
I managed to make it work using the function parent.requestDisallowInterceptTouchEvent(true); being placed inside the child view onTouchEvent(). This way the View does not allow none of his parents to interecpt his touch events in case a scroll happened and was to be handled by the ViewPager.
However in my case I had a ViewPager with a draggable custom views inside it which I wanted to move without that the ViewPager changes page.
My solution in terms of code: (Kotlin)
view.setOnTouchListener { v, event ->
parent.requestDisallowInterceptTouchEvent(true);
//Drag and drop handling is here and the rest of the event logic
}
I hope this will help you as well.
I've managed to solve my problem by incorporating this answer from another post. The code allows you to get the coordinates of a given view and compare them to your own coordinates.
Inside of my CustomViewPager class, I implemented what I mentioned above:
private boolean isPointInsideView(float x, float y, View view) {
int location[] = new int[2];
view.getLocationOnScreen(location);
int viewX = location[0];
int viewY = location[1];
return ((x > viewX && x < (viewX + view.getWidth())) && (y > viewY && y < (viewY + view.getHeight())));
}
I then had a method which returns a boolean, which checks a couple of conditions and returns true/false on if the pager should be able to be swiped:
public boolean canSwipe(float x, float y) {
boolean canSwipe = true;
if (launchActivity.isReady(MainScreenPagerAdapter.STAT_PAGE)) {
FragmentStatistics fragmentStatistics = (FragmentStatistics) launchActivity.getPageAdapter().instantiateItem(this, MainScreenPagerAdapter.STAT_PAGE);
View chart = fragmentStatistics.getView().findViewById(R.id.chart);
canSwipe = !isPointInsideView(x, y, chart) || !fragmentStatistics.isGraphPannable();
}
return canSwipe;
}
And then, of course, I overwrote the onInterceptTouchEvent like so:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canSwipe(ev.getX(), ev.getY()) ? super.onInterceptTouchEvent(ev) : false;
}
And now, the graph can be fully panned without the Pager interfering with it at all.
Full CustomViewPager code:
public class CustomViewPager extends ViewPager {
/** Reference to the launch activity */
private LaunchActivity launchActivity;
/**
* Constructor to call the super constructor
*
* #param context The application context
* #param attrs The attributes
*/
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Sets object reference for the {#code launchActivity}
*
* #param launchActivity The LaunchActivity to be set
*/
public void set(LaunchActivity launchActivity) {
this.launchActivity = launchActivity;
}
/**
* Determines if the pager can be swiped based off the x and y inputs provided, as well as if the
* barchart can be panned or not.
*
* #param x The x coordinate to check
* #param y The y coordinate to check
* #return True if the ViewPager will continue with normal swiping action.
*/
public boolean canSwipe(float x, float y) {
boolean canSwipe = true;
if (launchActivity.isReady(MainScreenPagerAdapter.STAT_PAGE)) {
FragmentStatistics fragmentStatistics = (FragmentStatistics) launchActivity.getPageAdapter().instantiateItem(this, MainScreenPagerAdapter.STAT_PAGE);
View chart = fragmentStatistics.getView().findViewById(R.id.chart);
canSwipe = !isPointInsideView(x, y, chart) || !fragmentStatistics.isGraphPannable();
}
return canSwipe;
}
/**
* Takes x and y coordinates and compares them to the coordinates of the passed view. Returns true if the passed coordinates
* are within the range of the {#code view}
*
* #param x The x coordinate to compare
* #param y The y coordinate to compare
* #param view The view to check the coordinates of
* #return True if the x and y coordinates match that of the view
*/
private boolean isPointInsideView(float x, float y, View view) {
int location[] = new int[2];
view.getLocationOnScreen(location);
int viewX = location[0];
int viewY = location[1];
// point is inside view bounds
return ((x > viewX && x < (viewX + view.getWidth())) && (y > viewY && y < (viewY + view.getHeight())));
}
/**
* Override of the onInterceptTouchEvent which allows swiping to be disabled when chart is selected
*
* #param ev The MotionEvent object
* #return Call to super if true, otherwise returns false
*/
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canSwipe(ev.getX(), ev.getY()) ? super.onInterceptTouchEvent(ev) : false;
}
}
How can I find the view causing a MotionEvent ACTION_CANCEL? I have a view "A" which is receiving ACTION_CANCEL and I don't want that to happen. Somewhere, a view "B" is "consuming" the MotionEvent. I'm hoping there is a way to find out who is "B" so I can address this misfunctionality.
I've tried looking through my code for various OnTouchEvent() and OnInterceptTouchEvent() handlers but haven't yet found a culprit.
I've also put a break point at the problematic ACTION_CANCEL but am not able to recognize anything in the MotionEvent that might represent "B".
If a parent is intercepting the motion event, the only way to prevent this is to prevent the parent from
intercepting that event. This can be managed well in two ways.
Without seeing specific code and wanting a generalised solution I would suggest the following.
I would suggest managing your touch events for the parents and the child by managing the
requestDisallowInterceptTouchEvent(boolean) and
onInterceptTouchEvent(android.view.MotionEvent) event handlers of every view/viewgroup within the affected views A, B C.
By disallowing parent intercepts in the child, this assists you in catching parent intercepts you
haven't accounted for, and also to customise and vary the child elements from within one parent.
This must be managed from your highest parent of your view/viewGroup and managed through all parent and
child relationships.
Checking for listviews, any element that has a inbuilt touch events.
android.com/training/gestures/viewgroup
In terms of finding the offending view that is intercepting the event, that cannot be answered except by the logic of:
Go through each parent to child/parent to child view. Methodically check the ontouch handling within each view/view group
as shown in my diagram.
There is some more detail in these answers here:
https://stackoverflow.com/a/30966413/3956566
https://stackoverflow.com/a/6384443/3956566
I'm sure you understand this, but to me, it's the simplest solution.
Beyond this, we'd need to look at your code to troubleshoot it.
If I got your question correct, you are receiving ACTION_CANCEL probably in the parent and you need to find that view. Given event X and Y, you can find view that contains these coordinates at the first moment ACTION_CANCEL occurred. Try calling this method either with top parent (android.R.id.content) or the ViewGroup you are dealing with.
private View findViewAt(View contentView, int eventX, int eventY) {
ArrayList<View> unvisited = new ArrayList<View>();
unvisited.add(contentView);
while (!unvisited.isEmpty()) {
View child = unvisited.remove(0);
if(isViewContains(child, eventX, eventY) {
Log.i(TAG, "view found! ");
unvisited.clear();
return child;
}
if (!(child instanceof ViewGroup)){
continue;
}
ViewGroup group = (ViewGroup) child;
final int childCount = group.getChildCount();
for (int i=0; i< childCount; i++){
unvisited.add(group.getChildAt(i));
}
}
return null;
}
private boolean isViewContains(View view, int eventX, int eventY) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
int width = view.getWidth();
int height = view.getHeight();
return eventX < x || eventX > x + width || eventY < y || eventY > y + height;
}
On my experience , a case that makes A view receive a ACTION_CANCEL event is after A is touched by users and they drags their finger out of area of A, If you face this case,Add a method to check the location on dispatchTouchEvent() can help.
I decided to post this question and answer in response to this comment to this question:
How to handle click in the child Views, and touch in the parent ViewGroups?
I will paste the comment here:
Suppose I want to override the touch events only for handling some of
the children, what can I do inside this function to have it working ?
I mean, for some children it would work as usual, and for some, the
parent-view will decide if they will get the touch events or not.
So the question is this: How do I prevent the parent onTouchEvent() from overriding some child elements' onTouchEvent(), while having it override those of other children?
The onTouchEvents() for nested view groups can be managed by the boolean onInterceptTouchEvent.
The default value for the OnInterceptTouchEvent is false.
The parent's onTouchEvent is received before the child's. If the OnInterceptTouchEvent returns false, it sends the motion event down the chain to the child's OnTouchEvent handler. If it returns true the parent's will handle the touch event.
However there may be instances when we want some child elements to manage OnTouchEvents and some to be managed by the parent view (or possibly the parent of the parent).
This can be managed in more than one way.
One way a child element can be protected from the parent's OnInterceptTouchEvent is by implementing the requestDisallowInterceptTouchEvent.
public void requestDisallowInterceptTouchEvent (boolean
disallowIntercept)
This prevents any of the parent views from managing the OnTouchEvent for this element, if the element has event handlers enabled.
If the OnInterceptTouchEvent is false, the child element's OnTouchEvent will be evaluated. If you have a methods within the child elements handling the various touch events, any related event handlers that are disabled will return the OnTouchEvent to the parent.
This answer:
https://stackoverflow.com/a/13540006/3956566 gives a good visualisation of how the propagation of touch events passes through:
parent -> child|parent -> child|parent -> child views.
Another way is returning varying values from the OnInterceptTouchEvent for the parent.
This example taken from Managing Touch Events in a ViewGroup and demonstrates how to intercept the child's OnTouchEvent when the user is scrolling.
4a.
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the scroll.
mIsScrolling = false;
return false; // Do not intercept touch event, let the child handle it
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mIsScrolling) {
// We're currently scrolling, so yes, intercept the
// touch event!
return true;
}
// If the user has dragged her finger horizontally more than
// the touch slop, start the scroll
// left as an exercise for the reader
final int xDiff = calculateDistanceX(ev);
// Touch slop should be calculated using ViewConfiguration
// constants.
if (xDiff > mTouchSlop) {
// Start scrolling!
mIsScrolling = true;
return true;
}
break;
}
...
}
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
Edit: To answer comments.
This is some code from the same link showing how to create the parameters of the rectangle around your element:
4b.
// The hit rectangle for the ImageButton
myButton.getHitRect(delegateArea);
// Extend the touch area of the ImageButton beyond its bounds
// on the right and bottom.
delegateArea.right += 100;
delegateArea.bottom += 100;
// Instantiate a TouchDelegate.
// "delegateArea" is the bounds in local coordinates of
// the containing view to be mapped to the delegate view.
// "myButton" is the child view that should receive motion
// events.
TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton);
// Sets the TouchDelegate on the parent view, such that touches
// within the touch delegate bounds are routed to the child.
if (View.class.isInstance(myButton.getParent())) {
((View) myButton.getParent()).setTouchDelegate(touchDelegate);
}
Lets revamp the issue.
You happen to have a ViewGroup with a bunch of children. You want to intercept the touch event for everything withing this ViewGroup with a minor exception of some children.
I have been looking for an answer for the same question for quite a while. Did not manage to find anything reasonable and thus came up on my own with the following solution.
The following code snippet provides an overview of the ViewGroup's relevant code that intercepts all touches with the exception of the ones coming from views that happen to have a special tag set (You should set it elsewhere in your code).
private static int NO_INTERCEPTION;
private boolean isWithinBounds(View view, MotionEvent ev) {
int xPoint = Math.round(ev.getRawX());
int yPoint = Math.round(ev.getRawY());
int[] l = new int[2];
view.getLocationOnScreen(l);
int x = l[0];
int y = l[1];
int w = view.getWidth();
int h = view.getHeight();
return !(xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
for (int i=0; i<floatingMenuItems.getChildCount(); i++){
View child = floatingMenuItems.getChildAt(i);
if (child == null || child.getTag(NO_INTERCEPTION) == null) {
continue;
}
if(isWithinBounds(child, ev)){
return false;
}
}
return true;
}
I have one view that has some vertical gestures like swipe down to move the view down etc. lets call this View rootView. Everything was good util I needed to add a view pager on top of it. Now the ViewPager is consuming all the touch events. The ViewPager is scrolling horizontally as it should, but consuming the vertical swipes too and doing nothing (not passing the event to the rootView). How to make both the Views listen to their corresponding touch events and send the rest to other. I tried creating a CustomViewPager and overriding its onTouchEvent in the hope of recognizing the swipe down first and return false in that case so that the rootview will get a chance to handle that touch event. But in the process of recognizing the gesture as down/up , the ViewPager is consuming ACTION_DOWN and ACTION_MOVE event which are needed by the rootView to process the amount of finger movement.
A solution that came to my mind is to add onTouchEvent on all the layout over the ViewPager, which recognize the Vertical vs horizontal and call the appropriate touchevent (of rootView vs ViewPager) , but in order to recognize the up/down/side gesture, the layout will consume some events which may be valueable to the rootView.
Another solution that comes to mind is to override the ontouchEvent of ViewPager and call the onTouchEvent of the rootView irrespective of the up/down/side movement. In this way both the ViewPager and rootView can use the event, but it is sometimes making the screen fluctuates.
How should I solve this problem ? I would appreciate some suggestions, and no need to provide the code, just a good way to solve this problem.
I've had to completely modify my original answer. My first solution was to subclass ViewPager and override onTouchEvent(), adding logic to switch between the CustomViewPager and the RelativeLayout below it. Returning false means the view ignores the event, and returning super.onTouchEvent(event) means the view consumes the event until the user lifts their finger. All the logic was in the ACTION_MOVE part of the event, but since you only get one chance to return a value (which is the first part of the event, ACTION_DOWN), you can't use any movement to determine whether to consume or ignore the event.
I finally got it to work by writing an entirely new class, starting with the ViewPager code and adding logic to recognize vertical gestures. When a vertical gesture occurs, code is run that acts on the RelativeLayout below it, instead of passing the touch event to it. Below is a code snippet from the onTouchEvent() of my class.
public class CustomViewPager extends ViewGroup {
// code from ViewPager class...
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "!mIsBeingDragged");
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (xDiff > mTouchSlop && xDiff > yDiff) {
isVerticalMovement = false;
hasMovedHorizontally = true;
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
else {
if (yDiff > mTouchSlop) {
// VERTICAL SWIPE -- do stuff here...
}
}
}
}
Have you tried placing the rootView on top of the ViewPager?
That way, the ViewPager should still work although being under the rootView
Is it possible to do it?
The main point are:
A Complex header out of the listView but that must scroll
A ListView whose scroll is handled by the far ScrollView parent.
The ListView must have recyling, so the linearLayout cannot be a solution
Thanks
to directly answer the question:
Is it possible to do it?
Yes it is. You can see this example here in the Albums and Profiles sections. But it's not simple, nor straight forward.
There're two issues in adding a ListView inside a ScrollView (you can research that on your own) that is TouchEvents get mixed up because it doesn't know which View will consume the touch and the system don't know how to layout a infinite sized View inside another infinite sidez View.
Because it's it's not simple nor straight forward to implement, there several possible implementations and all of them is A LOT of code and A LOT of testing. I'll point you to a open source example and you can go from there:
https://github.com/kmshack/Android-ParallaxHeaderViewPager
The problem here is enclosing ListView inside ScrollView. The steps given below describe how to enclose any vertically scrollable view into another vertically scrollable view.
Step 1
Write a method which determines wheather a View can be scrolled vertically and place the method inside a common utility class as follows. This method is taken from ViewPager.java and modified to find whether a View can vertically scrollable.
public static boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft()
&& x + scrollX < child.getRight()
&& y + scrollY >= child.getTop()
&& y + scrollY < child.getBottom()
&& canScroll(child, true, dy,
x + scrollX - child.getLeft(), y + scrollY
- child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollVertically(v, -dy);
}
Step 2
Subclass the enclosing vertically scrollable view, it may be ScrollView or ListView(In your case it is ScrollView), or the like and override the onInterceptTouchEvent() method as follows.
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
float dy = y - mLastMotionY;
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
if (Util.canScroll(this, false, (int) dy, (int) x, (int) y)) {
mLastMotionY = y;
return false;
}
break;
}
return super.onInterceptTouchEvent(event);
}
Step 3
Subclass the enclosed vertically scrollable view, it may be GridView or ListView or the like(In your case ListView) and override the onMeasure() method as follows. No need to override this method in ScrollView. Its default implementation behaves in the right way.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode == MeasureSpec.UNSPECIFIED) {
int height = getLayoutParams().height;
if (height > 0)
setMeasuredDimension(getMeasuredWidth(), height);
}
}
Step 4
Finally create an xml layout file and use the ScrollView and ListView that you subclassed and you must hard code the layout_height. If you are creating the view hierarchy through java code, then hard code the height by LayoutParams. This hard coding is not necessary if you use your own measuring strategy rather than one specified in step 3.
For further details please view this post ScrollInsideScroll, download the project and examine the code.
you cannot get recycling ListView if you do not write it yourself. If you want to use the solution for nested fragments (as on image) try this.