edited for clarity
I feel like this question already has an answer, but I can't find one.
I have a ScrollView in my layout, and it contains a variety of clickable views.
Under a specific condition I would like to disable clicks and events for the ScrollView and ALL of its children.
The following have not been helpful:
ScrollView.setEnabled(false)
ScrollView.setClickable(false)
ScrollView.setOnTouchListener(null)
As well as:
(parent view of the ScrollView).requestDisallowInterceptTouchEvent()
I have created a custom ScrollView with the following code:
public class StoppableScrollView extends ScrollView
{
private static boolean stopped = false;
public StoppableScrollView(Context context)
{
super(context);
}
public StoppableScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if(stopped)
{
return true;
}
else
{
super.onInterceptTouchEvent(ev);
return false;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev)
{
if(stopped)
{
return true;
}
else
{
super.onTouchEvent(ev);
return false;
}
}
public static void setStopped(boolean inBool)
{
stopped = inBool;
}
public static boolean getStopped()
{
return stopped;
}
}
Using only onTouchEvent() will stop the scrolling, but not the clicking of child views.
Using only onInterceptTouchEvent() makes it such that when clicks work scrolling does not, and vice versa.
Using both onTouchEvent() and onInterceptTouchEvent() successfully stops unwanted clicks on child views when stopped is 'true' but it also disables scrolling regardless of the state of stopped.
Is there an easier way to get this behaviour, or is there a way to modify the StoppableScrollView class so that it will handle these touch events properly?
What probably should help is the following (because I had similar problems):
In the ScrollView you should do a RelativeLayout as Main Child (ScrollView does accept only 1 main child anyway). This RelativeLayout should of course of fill_parent in both directions.
At the really end of the RelativeLayout (after all other children), you could put now a LinearLayout with transparent background (#00FFFFFF) which has also fill_parent in both directions. This LinearLayout should have Visibility = View.GONE (by default)
Also you have to attach an empty OnClickListener to it. Now, because of zOrder if you make this LinearLayout Visibility = View.Visible it will catch all the events and avoid clicking the children above!
As scrollview allows immeditate one child say in my case i have linear layout.and in this linear layout i have other conreolls.
now our first task is to get this linear layout so what we can write is
LinearLayout l = (LinearLayout) scrollview.getChildAt(0);
now after getting this linear layour we can easily access other controlls placed inside it via this code and disable it.
for(int i =0; i<l.getChildCount(); i++)
{
Log.i(TAG,"child "+ l.getChildAt(i));
l.getChildAt(i).setEnabled(false);
}
Related
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 have a RecyclerView (with LinearLayoutManager) and a custom RecyclerView.ItemDecoration for it.
Let's say, I want to have buttons in the decoration view (for some reason..).
I inflate the layout with button, it draws properly. But I can't make the button clickable. If I press on it, nothing happening(it stays the same, no pressing effect) and onClick event is not firing.
The structure of ItemDecoration layout is
<LinearLayout>
<TextView/>
<Button/>
</LinearLayout>
And I'm trying to set listener in ViewHolder of the decoration
class ItemDecorationHolder extends RecyclerView.ViewHolder {
public TextView header;
public Button button;
public HeaderHolder(View itemView) {
super(itemView);
header = (TextView)itemView.findViewById(R.id.header);
button = (Button)itemView.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//.. Show toast, etc.
}
});
}
}
And i'm drawing the decoration in onDrawOver method. (actually, I'm modifying this codebase: https://github.com/edubarr/header-decor )
Any ideas? Is it doable?
Thanks!
While the real header is scroll off the screen, the visible one is drawing on canvas directly ,not like a normal interactive widget.
You have these options
Override RecyclerView.onInterceptTouchEvent(), though with some invasiveness so I prefer the next one.
Make use of RecyclerView.addOnItemTouchListener(), remember the motion event argument has been translated into RecyclerView's coordinate system.
Use a real header view, but that will go a little far I think.
If you take option 1/2, Button.setPressed(true) and redraw the header will have a visual press effect.
In addition to what Neil said,
the answer here might help.
Passing MotionEvents from RecyclerView.OnItemTouchListener to GestureDetectorCompat
And then you just need to calculate the height of the header and see if the click falls onto that header view and handle the event yourself.
private class RecyclerViewOnGestureListener extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
float touchY = e.getY();
ALLog.i(this, "Recyclerview single tap confirmed y: " + touchY);
//mGroupHeaderHeight is the height of the header which is used to determine whether the click is inside of the view, hopefully it's a fixed size it would make things easier here
if(touchY < mGroupHeaderHeight) {
int itemAdapterPosition = mRecyclerView.getLayoutManager().getPosition(mRecyclerView.findChildViewUnder(0, mGroupHeaderHeight));
//Do stuff here no you have the position of the item that's been clicked
return true;
}
return super.onSingleTapConfirmed(e);
}
#Override
public boolean onDown(MotionEvent e) {
float touchY = e.getY();
if(touchY < mGroupHeaderHeight) {
return true;
}
return super.onDown(e);
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
float touchY = e.getY();
if(touchY < mGroupHeaderHeight) {
return true;
}
return super.onSingleTapUp(e);
}
}
As Neil is pointing out, things are getting more complicated than that. However by definition you can't.
So, why not including good libraries that do that and more?
I propose my hard work for clickable sticky header in my FlexibleAdapter project, which uses a real view (not decorators) to handle click events on headers when sticky.
There's also a working demo and a Wiki page on that part (and not only).
I have a container ViewGroup, lets call it screen inside a ScrollView. This container view hosts a number of other Views let's call them widgets, and some of them are interested in preventing the ScrollView from scrolling and using the MotionEvent theirselves (for example a pannable image)
I can't figure out the proper event intercept strategy to use. ScrollView always processes the event before the children, or the children process the event but scrollview is disabled.
I read about issuing getParent().requestDisableInterceptTouchEvent() in the child views if this view wants to capture the event, but their onTouchEvent is not called, I suppose because ScrollView has engulfed the event beforehand. I guess the fact that I have 2 levels of layers (container + widgets) prevents this from working, I suppose the container ViewGroup has to play an important part here, but I can't figure out which one...
Can I know, at the ScrollView's onInterceptTouchEvent level, which widget on the container viewGroup has been touched to decide if I should intercept or not?
or...
How can the 'widget' layers in the ViewGroup get the event before ScrollView so I can call getParent().onRequestDisableInterceptTouch() ... or is it getParent().getParent().onRequestDisableInterceptTouch()?
Thanks in advance
I've read related questions but no luck ...
Handle touch events in ScrollView Android
Well after a night of coca cola & debugging I managed to get this to work. I share the solution just in case it is of interest to anyone, because it took me quite a lot of time to get it running.
I didn't manage to get it running with getParent().onRequestDisableInterceptTouch(), I was close, but couldn't find a way for the child widgets to get the MotionEvents they need for scrolling once I intercepted the touch on the parent, so even though the outer scroll was prevented correctly, the inner widgets didn't scroll.
So the solution is to interceptTouchEvents in the children ONLY, and if the children is scrollable (known property), and the touch is ACTION_DOWN, then disable the scrollview two levels above. If the touch is ACTION_UP, we enable the scrollview.
To enable/disable the scrollview I just intercept the touch event and with a flag filter the event or not.
I did three auxiliary classes, one for the ScrollView, one for the Container, One for the widgets:
This class wraps every widget and, if I call setNeedsScroll(true) , then touches will be intercepted, and when it is touched, it will (tell the container to) tell the scrollview to disable itself. When the touch is released, it will re-enable the scrollview.
class WidgetWrapperLayout extends FrameLayout {
private boolean mNeedsScroll=false;
public WidgetWrapperLayout(Context context) {
super(context);
}
/** Called anytime, ie, during construction, to indicate that this
* widget uses vertical scroll, so we need to disable its container scroll
*/
public void setNeedsScroll(boolean needsScroll) {
mNeedsScroll=needsScroll;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mNeedsScroll) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
((SlideLayout)getParent()).setEnableScroll(false);
break;
case MotionEvent.ACTION_UP:
((SlideLayout)getParent()).setEnableScroll(true);
break;
}
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
This is the container, only child of the scrollview, and holds the different widgets. It just provides methods for the children so they can enable/disable the scroll:
public class ContainerLayout extends FrameLayout {
public ContainerLayout(Context context) {
super(context);
}
public void setEnableScroll(boolean status) {
if (Conf.LOG_ON) Log.d(TAG, "Request enable scroll: "+status);
((StoppableScrollView)getParent()).setScrollEnabled(status);
}
}
and finally a scrollview capable of deactivation. It disables the scroll 'old-skool', intercepting and blocking events.
public class StoppableScrollView extends ScrollView {
private String TAG="StoppableScrollView";
private boolean mDisableScrolling=false;
public StoppableScrollView(Context context) {
super(context);
}
/** Enables or disables ScrollView scroll */
public void setScrollEnabled (boolean status) {
if (Conf.LOG_ON) Log.d(TAG, "Scroll Enabled "+status);
mDisableScrolling=!status;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mDisableScrolling) return false;
return super.onInterceptTouchEvent(ev);
}
}
Implement View.OnTouchListener in your Activity and add that listener to the ScrollView. Then return true in onTouchEvent(...). Before returning call the onTouch of the children you want to handle that event.
I have LinearLayout and inside ListView ( multi select list) with attr width and height match_parent. I need to register OnTouchListener in LinearLayout but it seems that ListView steal event. How to solve this ?
You should make a custom layout extending LinearLayout like the following:
public class MyLayout extends LinearLayout {
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// do whatever you want with the event
// and return true so that children don't receive it
return true;
}
}
I have a application that need event handling on a unusual way.
For my question, let me first explain a simple case that the current event handling system of Android don't fits for me.
Supposing that I have a FrameLayout (that I'll call ViewSwiper since now) that all Views added on it are MATCH_PARENT X MATCH_PARENT (that I'll call PageView), it's handles events by translating the actual View and replace it based on the direction of moving.
This component I already have done and work properly ( Using Animation to swipe views ).
But the problem is on that PageView I add on top of it, in case of ImageViews that return false on it's onTouchEvent, the ViewSwiper will handle the events and let another PageView enter the screen, but if I add a ScrollView on that, all the events will be consumed by the Scroll and the ViewSwiper will not have chance to replace the PageView.
So I figured out that returning false onTouchEvent of the ScrollView the parent can assume it's events, I wrote this sub-class of ScrollView to test it:
public class ScrollViewVertical extends ScrollView {
public ScrollViewVertical(Context context) {
super(context);
setOverScrollMode(OVER_SCROLL_ALWAYS);
setVerticalScrollBarEnabled(false);
}
public boolean onTouchEvent(MotionEvent evt) {
super.onTouchEvent(evt);
return false;
}
}
But returning false make any further events to get dispatched to the parent, but I need these events for VERTICAL scrolling, so I have the idea to return falses only if the user are moving HORIZONTAL, that's what my code looks like:
public class ScrollViewVertical extends ScrollView {
private MovementTracker moveTracker;
public ScrollViewVertical(Context context) {
super(context);
setOverScrollMode(OVER_SCROLL_ALWAYS);
setVerticalScrollBarEnabled(false);
moveTracker = new MovementTracker();
}
public boolean onTouchEvent(MotionEvent evt) {
if (moveTracker.track(evt))
if (moveTracker.getDirection() == Direction.HORIZONTAL)
return false;
return super.onTouchEvent(evt);
}
}
PS: MovementTracker will returns true on track() after some events and tell on which direction the user is moving.
But in that case, the ScrollView keep receiving events since it's returns true on the first events.
Any ideas on how can I handle the events on ViewSwiper when it's child returns false (even if some trues are returned).
PS: I can give more info about this if needed, and accept different solutions also.
Based on answers I tried the following:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
onTouchEvent(ev);
return intercept;
}
public boolean onTouchEvent(MotionEvent evt) {
boolean x = super.onTouchEvent(evt);
if (moveTracker.track(evt)) {
intercept = moveTracker.getDirection() != Direction.VERTICAL;
if (!intercept)
getParent().requestDisallowInterceptTouchEvent(false);
}
return x;
}
Still nothing.
try this in onTouchEvent() of the scrollview
//if (evt.getAction() == MotionEvent.ACTION_MOVE) {
if (moveTracker.track(evt)){
if (moveTracker.getDirection() == Direction.VERTICAL){
//Or the direction you want the scrollview keep moving
getParent().requestDisallowInterceptTouchEvent(true);
}
}
return true;
Update
Please try the following to the custom Scrollview
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
return false;
}
And nothing else
This way i assume the MotionEvent will perform on both views. And since they don't conflict (One is vertical the other one is Horizontal) this could work
Based on the answer from weakwire, I came to the following solution:
On ViewSwiper
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(!super.dispatchTouchEvent(ev))
onTouchEvent(ev);
return true;
}
And on ScrollHorizontal I return false on dispatchTouchEvent when I don't need then anymore.