Preventing a NestedScrollView from scrolling parent NestedScrollView? - android

I have a NestedScrollView(NSVchild), inside a horizontal RecyclerView, inside another NestedScrollView(NSVparent). Inside NSVchild is a TextView with long texts.
What I want to know now is, how can I prevent NSVchild from scrolling NSVparent, when NSVchild finishes scrolling from top or bottom?
What I tried:
I tried extending NestedScrollView, and overriding onTouchEvent, and returning false when NSVchild is at scroll 0 and user is dragging down on the screen, and when NSVchild is at it's max scroll and user is dragging up. Returning false works as expected, but problem is understanding when user is dragging up or down on the screen. here's is the code for that:
float deltaY = 0;
float oldY = 0;
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
float y = ev.getY();
deltaY = y - oldY;
oldY = y;
Log.i("deltaY", deltaY+"");
break;
}
[...]
return super.onTouchEvent(ev);
}
in this approach, deltaY < 0 means that screen is being dragged upwards, and deltaY > 0 means vice versa. It works well as long as there's room for NSVchild to scroll inside itself. when it finishes scrolling from one side, and starts scrolling NSVparent, deltaY will become less than 0 in one frame, and in the next, more than 0, And this makes my code useless.
Is there any other way to achieve what I want? Or a better way to understand if screen is being dragged up or down?

Related

How to revoke `requestDisallowInterceptTouchEvent(false)` without lifting finger (Action.UP or Cancel)?

I have WebView(s) inside the RecyclerView. In order to get smooth scrolling experience such that when user scrolls, the RecyclerView will be responsible for scrolling (WebView should not scroll) I called getParent().requestDisallowInterceptTouchEvent(false); inside webview#onTouchEvent(event) when there is only one touch point and is moving vertcially (scrolling up and down).
private void handleSingleFingerTouch(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = ev.getX();
y1 = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = ev.getX();
y2 = ev.getY();
// -ve param for canScrollHorizontally is to check scroll left. +ve otherwise
if (Math.abs(x1 - x2) >= Math.abs(y1 - y2)
&& canScrollHorizontally((int) (x1 - x2))) {
// scrolling horizontally. Retain the touch inside the webView.
getParent().requestDisallowInterceptTouchEvent(true);
} else {
// scrolling vertically. Share the touch event with the parent.
getParent().requestDisallowInterceptTouchEvent(false);
}
x1 = x2;
y1 = y2;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
boolean multiTouch = ev.getPointerCount() > 1;
if (multiTouch) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
handleSingleFingerTouch(ev);
}
return super.onTouchEvent(ev);
}
It works as expected with just one bug, I found that while RecyclerView(and webview) scrolling and I touch inside the WebView, then RecyclerView stops scrolling as expected, then if I don't lift up my finger but keep finger on the screen and try to zoom, the webview would not zoom and actually it wouldn't receive touch event at all. I have to lift my fingers and touch again to zoom. I know this is because getParent().requestDisallowInterceptTouchEvent(false); won't cancel unless UI receive CANCEL or UP event. I tried to implement an interface that call getParent().requestDisallowInterceptTouchEvent(true); when multi-touch happen. Though it did get called, but seems it doesn't work. Zoom still not happen and onTouchEvent inside the WebView still not get triggered. Any idea to solve this?
So basically the solution is to override the onInterceptTouchEvent of the recyclerView
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
// When recyclerview is scrolling this will stop scrolling and allow touch event passed to child views.
if (e.action == MotionEvent.ACTION_DOWN && this.scrollState == RecyclerView.SCROLL_STATE_SETTLING) {
this.stopScroll()
}
return super.onInterceptTouchEvent(e)
}

Floating List view in android?

Is it possible to create a list view in android that is not fixed to a particular position and user can move the list view on a gesture some what floating listview that can be moved anywhere on the screen?I tried finding it but could not find some link.Does any one have some idea about it?
You can achieve this by setting an OnTouchListener on the list view and overriding it's onTouch() method. In the onTouch() method you will have to handle the touch events like(ACTION_DOWN, ACTION_MOVE). Here is how you can handle touch event:
#Override
public boolean onTouch (View v, MotionEvent event)
{
switch(event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
start.set(event.getX(), event.getY()); //saving the initial x and y position
mode = DRAG;
break;
case MotionEvent.ACTION_MOVE:
if(mode == DRAG) {
float scrollByX = event.getX() - start.x; //computing the scroll in X
float scrollByY = event.getY() - start.y; //computing the scroll in Y
v.scrollBy((int)scrollByX/20, 0);
}
break;
}
return false;
}
When the user places the finger down on the screen the ACTION_DOWN event is triggered and when the user drags his finger on the screen ACTION_MOVE event is triggered. Hence a drag event is a combination of ACTION_DOWN and ACTION_MOVE.
Once you have the scrollX and scrollY value the you can move the view on the screen by passing these values to the scrollBy() method of the View class.
Here in the code there are two things to notice:
first is that I have passed 0 as the value of scrollY as it will prevent the list view to be dragged and scrolled simultaneously.
second is that the return value is false as you want the android to handle the scrolling of the list items
Here you can find the complete code for this
I would move the listview using the margins from the layout.
Outline:
Override OnTouch, and based on the event.getX() and event.getY(), update the margins of the listview as follows:
if the parent is a relative layout:
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)mListView.getLayoutParams();
lp.leftMargin = event.getX();
lp.topMargin = event.getY();
mListView.setLayoutParams(lp);
Add the dispatchTouchEvent to your activity so that events from the listview get passed up to onTouch:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
Hope this helps.
Although it's hard to maintain, AbsoluteLayout lets you specify a position in (x,y) coordinates. You would just declare your ListView inside a AbsoluteLayout, get a handle to it in java code where you intercept the clicks and then set new coordinates.
In the Android framework, the word "floating" is usually synonymous with new Window (such as a Dialog). It depends somewhat on what your end goal is, but if you want to create a ListView that floats over the rest of the content in your hierarchy, that you can easily hide and show, you should probably consider wrapping that view inside a PopupWindow, which has methods to allow you to easily show the window with showAsDropdown() and shotAtLocation() and also move it around while visible with update().
This is how the newer versions of the SDK create pop-up lists for Spinner and Menu

adapt scrolling/swiping to e-ink screens

Scrolling screens and animations are bad things on e-ink screens. Their fast screen refreshes make the screen wildly flicker (first going negative, then black, then new content, repeat many times for scrolling).
One simple way to tame an e-ink screen is to change scrolling to paging. Another method would be to still do scrolling - but without showing intermediate screen updates:
Screen before scroll -> lifting the finger: update to screen after scroll
Screen before scroll -> leaving the finger + timeout: update to actual screen
Any ideas how to implement this?
Is there a way to teach/overwrite existing scroll code?
Thx
#Override
public boolean dispatchTouchEvent(MotionEvent event)
{
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
hit_scrollx = view.getScrollX();
hit_scrolly = view.getScrollY();
hit_touchx = (int) event.getX();
hit_touchy = (int) event.getY();
break;
case MotionEvent.ACTION_UP:
int x = (int) event.getX();
int y = (int) event.getY();
if (x<hit_touchx-5 || x>hit_touchx+5 || y<hit_touchy-5 || y>hit_touchy+5)
scrollAbsolute(hit_scrollx+hit_touchx-x, hit_scrolly+hit_touchy-y);
break;
}
return(true);
}
This presumes that you have your own scrollAbsolute which does clipping for your application and uses View.scrollTo. This is designed for an activity with a main scrolling view. It captures all touch events. The "5" is to prevent unnecessary refresh on insignificant touches.

looking to implement a wall (GridView?) that goes off-screen and user can touch to move it around

Desired effect
I have a bunch of small images that I'd like to show on a "wall" and then let the user fling this wall in any direction and select an image.
Initial Idea
as a possible implementation I was thinking a GridView that is larger than the screen can show - but all examples of using this widget indicate that the Gallery doesn't extend beyond the size of the screen.
Question
What is the best widget to use to implement the desired effect ?
A code sample would be especially beneficial.
EDIT...
if someone has example code that will let me put about 30 images on a "wall" (table would be good) then I will accept that as the answer. Note that the "wall" should look like it extends beyond the edges of the display and allow a user to use the finger to drag the "wall" up down left right. Dragging should be in "free-form" mode. A single tap on an image selects it and a callback shall be detectable. I have created a bounty for this solution.
The solution is actually quite simple, but a pain. I had to implement exactly this within a program I recently did. There are a few tricksy things you have to get around though. It must be noted that different OS versions have different experiences. Certain expertise or cludging around is required to make this work as drawing is sometimes adversely affected by this method. While I have a working copy, the code I provide is not for everyone, and is subject to whatever other changes or customizations have been made in your code.
set android:layout_width to wrap_content
set android:layout_height to wrap_content
In your code:
determine how many rows you will have.
divide number of items by the number of rows.
add gridView.setStretchMode(NO_STRETCH);
add gridView.setNumColumns( number of Columns );
add gridView.setColumnWidth( an explicit width in pixels );
To Scroll:
You may simply use gridView.scrollBy() in your onTouchEvent()
These are the steps. All are required in order to get it to work. The key is the NO_STRETCH with an explicit number of columns at a specific width. Further details can be provided, if you need clarification. You may even use a VelocityTracker or onFling to handle flinging. I have snappingToPage enabled in mine. Using this solution, there is not even a requirement to override onDraw() or onLayout(). Those three lines of code get rid of the need for a WebView or any other embedding or nesting.
Bi-directional scrolling is also quite easy to implement. Here is a simple code solution:
First, in your class begin tracking X and Y positions by making two class members. Also add State tracking;
// Holds the last position of the touch event (including movement)
int myLastX;
int myLastY;
// Tracks the state of the Touch handling
final static private int TOUCH_STATE_REST = 0;
final static private int TOUCH_STATE_SCROLLING = 1;
int myState = TOUCH_STATE_REST;
Second, make sure to check for Scrolling, that way you can still click or longclick the images themselves.
#Override public boolean onInterceptTouchEvent(final MotionEvent ev)
{//User is already scrolling something. If we haven't interrupted this already,
// then something else is handling its own scrolling and we should let this be.
// Once we return TRUE, this event no longer fires and instead all goes to
// onTouch() until the next TouchEvent begins (often beginning with ACTION_DOWN).
if ((ev.getAction() == MotionEvent.ACTION_MOVE)
&& (myState != TOUCH_STATE_REST))
return false;
// Grab the X and Y positions of the MotionEvent
final float _x = ev.getX();
final float _y = ev.getY();
switch (ev.getAction())
{ case MotionEvent.ACTION_MOVE:
final int _diffX = (int) Math.abs(_x - myLastX);
final int _diffY = (int) Math.abs(_y - myLastY);
final boolean xMoved = _diffX > 0;
final boolean yMoved = _diffY > 0;
if (xMoved || yMoved)
myState = TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
myLastX = _x;
myLastY = _y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (myState == TOUCH_STATE_SCROLLING)
// Release the drag
myState = TOUCH_STATE_REST;
}
//If we are not At Rest, start handling in our own onTouch()
return myState != TOUCH_STATE_REST;
}
After the GridView knows that you are Scrolling, it will send all Touch Events to onTouch. Do your Scrolling here.
#Override public boolean onTouchEvent(final MotionEvent ev)
{
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
final View child;
switch (action)
{
case MotionEvent.ACTION_DOWN:
//Supplemental code, if needed
break;
case MotionEvent.ACTION_MOVE:
// This handles Scrolling only.
if (myState == TOUCH_STATE_SCROLLING)
{
// Scroll to follow the motion event
// This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
scrollBy(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_UP:
// If Scrolling, stop the scroll so we can scroll later.
if (myState == TOUCH_STATE_SCROLLING)
myState = TOUCH_STATE_REST;
break
case MotionEvent.ACTION_CANCEL:
// This is just a failsafe. I don't even know how to cancel a touch event
myState = TOUCH_STATE_REST;
}
return true;
}
If course, this solution moves at X and Y at the same time. If you want to move just one direction at a time, you can differentiate easily by checking the greater of the X and Y differences. (i.e. Math.abs(deltaX) > Math.abs(deltaY)) Below is a partial sample for one directional scrolling, but can switch between X or Y direction.
3b. Change this in your OnTouch() if you want to handle one direction at a time:
case MotionEvent.ACTION_MOVE:
if (myState == TOUCH_STATE_SCROLLING)
{
//This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
// Check which direction is the bigger of the two
if (Math.abs(deltaX) > Math.abs(deltaY))
scrollBy(deltaX, 0);
else if (Math.abs(deltaY) > Math.abs(deltaX))
scrollBy(0, deltaY);
// We do nothing if they are equal.
}
break;
FuzzicalLogic
A GridView can have content that extends beyond the size of the screen, but only in the vertical direction.
Question: What is the best widget to use to implement the desired effect ?
The are no default widgets (at least prior to 3.0) which implement 2D scrolling. All of them implement 1D (scrolling in one direction) only. Sadly the solution to your problem is not that easy.
You could:
Place a TableLayout inside a custom ScrollView class, and handle the touch events yourself. This would allow you to do proper 2D panning, but would not be so good for large data-sets since there would be no view recycling
Modify GridView if possible to enable horizontal scrolling functionality (probably the best solution if you can do it)
Try putting a TableLayout inside a ScrollView (with layout_width="wrap_content) which you put inside a HorizontalScrollView - though this may work it wouldn't be the optimum solution
Do something with OpenGL and draw straight to a canvas - this is how the default Gallery 3D app does it's "wall of pictures"
I think that the only Android native solution for this is to have an embebed WebView in which you can have that bi-dimensional scroll (and by the way is the only thing that has this capability).
Be pressing an image you can control the behavior by a callback from JavaScript to JAva.
This is not very "pretty" but... it's the only viable way to do what you want "out of the box".
I have implemented a GridView that overrides the onInterceptTouchEvent methods. I set the OnTouchListeneras explained by Fuzzical Logic. My issue is that setting a custom OnTouchListener, the objects of my gridView are not clickable anymore. Is that because the default OnTouchListener of a GridView calls onItemClickListener ?
I would like to have a 2 directions scrollable GridView but that has still cliackable objects. Should I implement my own method to check whether a touch matches a item or is there a easier way to achieve this ?
Grodak

Android View stops receiving touch events when parent scrolls

I have a custom Android view which overrides onTouchEvent(MotionEvent) to handle horizontal scrolling of content within the view. However, when the ScrollView in which this is contained scrolls vertically, the custom view stops receiving touch events. Ideally what I want is for the custom view to continue receiving events so it can handle its own horizontal scrolling, while the containing view hierarchy deals with vertical scrolling.
Is there any way to continue receiving those motion events on scroll? If not, is there any other way to get the touch events I need?
Use requestDisallowInterceptTouchEvent(true) in the childview to prevent from vertical scrolling if you want to continue doing horizontal scrolling and latter reset it when done.
private float downXpos = 0;
private float downYpos = 0;
private boolean touchcaptured = false;
#Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
downXpos = event.getX();
downYpos = event.getY();
touchcaptured = false;
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
float xdisplacement = Math.abs(event.getX() - downXpos);
float ydisplacement = Math.abs(event.getY() - downYpos);
if( !touchcaptured && xdisplacement > ydisplacement && xdisplacement > 10) {
requestDisallowInterceptTouchEvent(true);
touchcaptured = true;
}
break;
}
super.onTouchEvent(event);
return true;
}
I'm answering my own question in case anyone else is as bad at Googling for the answer as I apparently was. :P
A workaround for this problem is to extend ScrollView and override the onInterceptTouchEvent method so that it only intercepts touch events where the Y movement is significant (greater than the X movement, according to one suggestion).

Categories

Resources