How can I implement a custom onInterceptTouchEvent() in a ListView that give the scrolling priority to the child's of the ListView and as soon as they did their scrolling , give it back to the ListView ? I want to give priority to the inner views.
Try overriding onInterceptTouchEvent() of your children like this:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(!isAtTop && !isAtBottom){
getParent().requestDisallowInterceptTouchEvent(true);
}
return super.onInterceptTouchEvent(ev);
}
In onInterceptTouchEvent() calculate if the ListView has scrolled totally to the top or bottom. If it is somewhere in between then ask the parent to not intercept touches.
To check for top or bottom try:
int scrollRange = computeVerticalScrollRange();
int scrollOffset = computeVerticalScrollOffset();
int scrollExtend = computeVerticalScrollExtent();
if(scrollOffset == 0){
//AtTop
}else if(scrollRange == scrollOffset + scrollExtend){
//AtBottom
}
Related
Really not sure if I understand this or maybe going about it wrong. I have a gridlayout (cells are textview) wrapped inside a horizontal scroll which is inside a vertical scroll. I am using the Dpad to navigate across the grid. This works well, as I press the right arrow pad the grid cells move left to right as expected and right to left as left arrow pad is pressed. I have added an onKeylistener attached to each textView of the grid, as I scroll across the grid I am changing the color back ground. The problem is that the onKeyListener apparently takes over the control of the grid. The scroll right works for changing the color but the cells no longer move on the grid. Once I get to last visible column focus continues off screen but the cells stay off screen. Is there a way to implement the scroll inside the onkey event so the cells shift and I have control over the properties of the cell? Or is there a totally different way of doing making this work?
The main components are
textViewD.setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
if(keyEvent.getAction()==KeyEvent.ACTION_DOWN && i == KeyEvent.KEYCODE_DPAD_RIGHT) {
gridLayoutE.getChildAt(childIndex[0]).setBackgroundColor(Color.BLACK);
gridLayoutE.getChildAt(childIndex[0] + 1).setBackgroundColor(Color.GREEN);
childIndex[0] = childIndex[0] + 1;
gridLayoutE.requestFocus();
return true;
}
return false;
}
The scrolllistener, I have a class that extends the horizontalscroll in order to have my header table scroll along with my grid. This works.
#Override
public void onScrollChanged (ObservableScrollView scrollView,int x, int y, int oldx, int oldy){
if (scrollView == hsvHeader) {
hsvBody.scrollTo(x, y);
} else if (scrollView == hsvBody) {
hsvHeader.scrollTo(x, y);
}
}
I was able to find a solution to my problem by using dispatchKeyEvent(KeyEvent event). I ended up with something like
public boolean dispatchKeyEvent(KeyEvent event) {
mcols= mcols + 1;
if(event.getAction()==KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
gridLayoutE.getFocusedChild().setBackgroundColor(Color.BLACK);
gridLayoutE.findViewById(mcol).setBackgroundColor(Color.GREEN);
}
return super.dispatchKeyEvent(event);
}
This along with my scrolllistener, allowed my to scroll the grid and change each cell as I did.
I have a ViewPager below a NestedScrollView width some top padding, and clipToPadding(false) and transparent background (like as image).
My ViewPager can't get touch event and doesn't work.
How can I solve this problem?
(I can't change my structure and can't move ViewPager to above of NestedScrollView or set TopMargin to NestedScrollView)
NestedScrollView
nestedScrollView = new NestedScrollView(getContext());
nestedScrollView.setFillViewport(true);
nestedScrollView.setLayoutParams(scrollParams);
nestedScrollView.setClipToPadding(false);
Solution:
This Problem solved With overwriting NestedScrollView and Override onTouchEvent.
(Thanks to #petrumo)
public class MyNestedScrollView extends NestedScrollView {
private boolean topZone = false;
public MyNestedScrollView(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN ){
topZone = (getPaddingTop() - getScrollY() > ev.getY());
}
if(topZone){
if(ev.getAction() == MotionEvent.ACTION_UP){
topZone = false;
}
return false;
}
return super.onTouchEvent(ev);
}
}
There is a workaround for this case, you can override onInterceptTouchEvent and onTouchEvent in the nestedscrollview. There are posts explaining how to do it, https://developer.android.com/training/gestures/viewgroup.html and http://neevek.net/posts/2013/10/13/implementing-onInterceptTouchEvent-and-onTouchEvent-for-ViewGroup.html. When you intercept the event, based on the position and your custom logic you would decide to not use the touch to leave it for the viewpager or let the default scrollview logic handle it.
I am not in favor of this solution, but as you explained you need to have the NestedScrollview cover the viewPager, unless you can reconsider the restrictions
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;
}
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.
I use a SlidingDrawer as my main layout. Inside the content area I have a Fragment (which contains a ListView) When the activity first loads everything is great, the listview scrolls correctly.
When I start a different activity and then come back, the first scroll motion I try is intercepted by the SlidindDrawer, and either opens or closes it. As soon as you stop the scroll and pick up your finger, the ListView is again able to scroll.
I would like the ListView to be able to scroll when the activity resumes. And just generally be able to control whether the SlidingDrawer is the one getting focus.
UPDATE:
I have narrowed the issue down a little bit. I have extended the SLidingDrawer to allow for click on buttons in the handle with the following code.
Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
if (mHandleLayout != null) {
int clickX = (int) (event.getX() - mHandleLayout.getLeft());
int clickY = (int) (event.getY() - mHandleLayout.getTop());
if (isAnyClickableChildHit(mHandleLayout, clickX, clickY))
return false;
}
return super.onInterceptTouchEvent(event);
}
private boolean isAnyClickableChildHit(ViewGroup viewGroup, int clickX, int clickY) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View childView = viewGroup.getChildAt(i);
if (TAG_CLICK_INTERCEPTED.equals(childView.getTag())) {
childView.getHitRect(mHitRect);
if (mHitRect.contains(clickX, clickY))
return true;
}
if (childView instanceof ViewGroup && isAnyClickableChildHit((ViewGroup) childView, clickX, clickY))
return true;
}
return false;
}
If I comment out the onInterceptTouchEvent function, everything seems to work normally.
I noticed that you are calling super.onInterceptTouchEvent(event) twice. Why?
That could be the reason for the issue.