I have a ScrollView on top of another view(with Buttons). The ScrollView is taking the whole screen and is obscuring the view that is beneath it.
At some point in my app I need the ScrollView to be disabled (but still visible) and transfer all the touch events to the Buttons that are beneath the ScrollView. How can I do that? Some views like Buttons are automatically doing that when disabled but a ScrollView is not doing that.
Try to implement your own ScrollView which has a flag to indicate the status(disabled/enabled) and also overrides the onTouchEvent and dispatchTouchEvent to let the touch events get pass the ScrollView. Here is an example:
public class DisabledScrollView extends ScrollView {
private boolean mIsDisable = false;
// if status is true, disable the ScrollView
public void setDisableStatus(boolean status) {
mIsDisable = status;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
// no more tocuh events for this ScrollView
if (mIsDisable) {
return false;
}
return super.onTouchEvent(ev);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// although the ScrollView doesn't get touch events , its children will get them so intercept them.
if (mIsDisable) {
return false;
}
return super.dispatchTouchEvent(ev);
}
}
Then all you have to do is change the value of that flag. See if it works.
In my case, I just needed to handle the touch event in View A, which was overlaping View B and then send the event to View B. Both views were child of the same RelativeLayout, but there was no parent-child relation between views A and B. This worked for me:
viewA.setOnTouchListener( new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
// do my stuff here
viewB.dispatchTouchEvent( event );
}
}
In this case I have a recyclerview under a scrollview. The top of scrollview is in vertical scroll, and the recyclerview is in horizontal scroll. The scrollview have top padding, making the recyclerview is visible through the transparency in the scrollview padding. I have to make it this way because when the scrollview is scrolled the recyclerview will scroll vertically to like parallax effect (this effect is in another code). This code below is working for my case, might help
scrollView.setOnTouchListener(new View.OnTouchListener() {
float mDownX,mDownY;
boolean mIsSwiping,isDown;
#Override
public boolean onTouch(View v, MotionEvent event) {
if(mIsSwiping){
recyclerView.dispatchTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mIsSwiping = false;
isDown = true;
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if(isDown){
float deltaX = Math.abs(event.getX() - mDownX);
float deltaY = Math.abs(event.getY() - mDownY);
mDownX = event.getX();
mDownY = event.getY();
if(deltaX!=deltaY){
isDown = false;
if(deltaX>deltaY){
mIsSwiping = true;
}
}
}
}
return mIsSwiping;
}
});
This is the layout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/gray0"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="70dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="320dp"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
<ScrollView
android:id="#+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="301.75dp"
android:paddingBottom="23.5dp"
android:clipChildren="false"
android:clipToPadding="false">
.
.
.
Related
I have ViewPager with three fragments and one of them is FrameLayout with ScrollView inside:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/table"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</ScrollView>
<Button
android:id="#+id/buttonRemove"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/bg_button_red"
android:layout_gravity="bottom"
android:layout_margin="4dp"
android:text="#string/button_remove_last_round"
android:textColor="#ffffff"
android:textSize="24sp"/>
</FrameLayout>
and if I swipe over button, it works. But if I swipe over the ScrollView, it doesn't work, only scroll up/down works
EDIT: I tried to override OnTouchEvent of Scrollview:
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
touchX = ev.getX();
touchY = ev.getY();
return super.onTouchEvent(ev);//if I change it to 'break', then only swipe works, but no scroll
case MotionEvent.ACTION_MOVE:
if(Math.abs(touchX-ev.getX())<40){
return super.onTouchEvent(ev);//Scroll works perfectly
}else{
return false;//Scroll disabled, but swipe still not working
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchX=0;
touchY=0;
break;
}
return false;
}
Now I can disable scroll, but swipe still not working, if the difference between X points more than 40, I pass the event to the viewpager, but the viewpager's onTouchListener doesn't get this event;
Ok, I found the solution with help of #yedidyak. I wrote my custom ScrollView:
public class CustomScrollView extends ScrollView {
float touchX = 0;
float touchY = 0;
ViewPager parentPager;
public void setParentPager(ViewPager parentPager) {
this.parentPager = parentPager;
}
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomScrollView(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()){
case MotionEvent.ACTION_DOWN:
touchX = ev.getX();
touchY = ev.getY();
return super.onTouchEvent(ev);
case MotionEvent.ACTION_MOVE:
if(Math.abs(touchX-ev.getX())<40){
return super.onTouchEvent(ev);
}else{
if (parentPager==null) {
return false;
} else {
return parentPager.onTouchEvent(ev);
}
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchX=0;
touchY=0;
break;
}
return super.onTouchEvent(ev);
}
}
then in the fragment I put the viewpager to this view and it works perfectly
The problem is that it isn't clear what scroll you want to happen, that of the ViewPager or that of the scrollview. If you really need a scroll-inside-a-scroll, then you need to override the OnTouchListener of the inner scrollview and add code that decides when to catch and use the touch, and when to pass it back to the parent views.
In this case, you can do something that tests if the swipe is up/down, and then keep the touch, otherwise if the swipe is sideways then passes it back.
For Future Readers living in +2019
use ViewPager2 to avoid this problem.
you can find good example of ViewPager2 implementation at this topic.
I am trying to achieve a scroll effect, I think it can be done because I see some apps implemented this.
I have a FrameLayout, in this layout I have:
- A recycler view
- A float view
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout // float layout here
android:layout_width="match_parent"
android:layout_height="100dp">
</LinearLayout>
</FrameLayout>
When I scroll the recycler view, I can see the float view scroll also, but when it reaches the top of the screen, I want it to stop there. I have successfully implemented this but after that I face to a new issue. Because the float view is above the recycler view, I can not scroll when touch and scroll the float view. In this case the float view seems consumes the touch event so that the recycler does nothing.
What I want to achieve is when user want to scroll the recycler view should consume it.
Im thinking of sending the float view's touch event to recycler view.
Thanks.
I have found same problem some time ago. Here is my solution (it is a little bit hacky, but didn't find better solution). Put in in your custom FrameLayout class:
public class CustomFrameLayout extends FrameLayout {
...
#InjectView(R.id.rv_details)
RecyclerView recyclerView;
#InjectView(R.id.ll_details_action_bar_wrapper)
ViewGroup actionBarWrapperViewGroup;
private List<MotionEvent> cachedEventList = new ArrayList<>();
private boolean touchIsFromActionBar;
private boolean yTranslationThresholdPassed;
// Pawel Janeczek
// Those two overrides is for forwarding touch events, that started on action bar, to recyclerview.
// But you may ask, why there are so many lines? it should by only recyclerView.dispatchTouchEvent(ev) and it should be fine
// It is because RecyclerView when it is starting scrolling it sends parent.requestDisallowInterceptTouchEvent which disables sending onInterceptTouchEvent to parent
// In such case we must set a flag touchIsFromActionBar when motion event starts and is in action bar, and then when this flag is set we remove calling super on requestDisallowInterceptTouchEvent
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN && viewUtils.isWithinViewBounds(actionBarWrapperViewGroup, ev.getRawX(), ev.getRawY())) {
touchIsFromActionBar = true;
}
if (touchIsFromActionBar && shouldDispatchEventToRecyclerView(ev)) {
if (!listUtils.isEmpty(cachedEventList)) {
for (MotionEvent motionEvent : cachedEventList) {
recyclerView.dispatchTouchEvent(motionEvent);
}
cachedEventList.clear();
}
recyclerView.dispatchTouchEvent(ev);
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
cachedEventList.clear();
yTranslationThresholdPassed = false;
touchIsFromActionBar = false;
}
return false;
}
private boolean shouldDispatchEventToRecyclerView(MotionEvent event) {
if (yTranslationThresholdPassed) {
return true;
} else if (listUtils.isEmpty(cachedEventList)) {
cachedEventList.add(MotionEvent.obtain(event));
return false;
}
int yTranslationThreshold = 2;
MotionEvent lastEvent = listUtils.getLast(cachedEventList);
if (Math.abs(lastEvent.getY() - event.getY()) > yTranslationThreshold) {
yTranslationThresholdPassed = true;
return true;
} else {
cachedEventList.add(MotionEvent.obtain(event));
return false;
}
}
#Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!touchIsFromActionBar) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
...
}
ViewGroup named actionBarWrapperViewGroup is a flow layout in your sample.
And xml for CustomFrameLayout:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<RecyclerView
android:id="#+id/rv_details"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<LinearLayout
android:id="#+id/ll_details_action_bar_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
<FrameLayout
android:id="#+id/ll_details_action_bar_container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="#dimen/default_bar_height"
android:background="?colorPrimary"/>
...
</LinearLayout>
</merge>
It is live copied from my project, so names can be misleading but I think it is understandable. If you have any questions go on.
Looks like setting RecyclerView's item layout to clickable="true", consume some touch events completely, particulary MotionEvent.ACTION_DOWN (ACTION_MOVE and ACTION_UP afterwards are working):
item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/demo_item_container"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"> <-- this what breaks touch event ACTION_DOWN
....
</LinearLayout>
Having very basic RecyclerView setup in onCreate():
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
... //Standard recyclerView init stuff
//Please note that this is NOT recyclerView.addOnItemTouchListener()
recyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d("", "TOUCH --- " + motionEvent.getActionMasked());
//Will never get here ACTION_DOWN when item set to android:clickable="true"
return false;
}
});
Is this intended behaviour or bug in RecyclerView cause it is still a preview?
PS. I want this to be clickable as per docs to react on pressed state and have ripple effect on click. When set to false ACTION_DOWN is working fine but pressed state is not triggered and selectableBackground does not have any effect.
This is intended behaviour NOT a bug.
When set item clickable true, ACTION_DOWN will be consumed, recycler view
will NEVER get ACTION_DOWN.
Why are you need ACTION_DOWN in onTouch() of recycler view? Does it necessary?
if you want to set lastY in ACTION_DOWN, why not this
case MotionEvent.ACTION_MOVE:
if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
// initial
if (lastY == -1)
lastY = y;
float dy = y - lastY;
// use dy to do your work
lastY = y;
break;
case:MotionEvent.ACTION_UP:
// reset
lastY = -1;
break;
Does it you want to? if you still want the ACTION_DOWN, try to get it in activity, such as:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN)
lastY = ev.getRawY();
return super.dispatchTouchEvent(ev);
I have a scrollView with lot of elements
ScrollView scroller = (ScrollView)findViewById(R.id.scrollView);
I need to attach an onClickListener to the scrollview so I do
scroller.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// This is all you need to do to 3D flip
AnimationFactory.flipTransition(viewAnimator, FlipDirection.LEFT_RIGHT);
}
});
But this is not getting triggered when I touch. Any Ideas?
The best solution seem to put LinearLayout into ScrollView and set the setOnClickListener on it.
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/myLayout"
android:clickable="true"
android:orientation="vertical">
<!-- content here -->
</LinearLayout>
</ScrollView>
in the Activity :
LinearLayout lin = (LinearLayout) fragment.rootView.findViewById(R.id.myLayout);
lin.setOnTouchListener(new setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// Whatever
}
});
You need to set the setOnClickListener directly on the ScrollView's child.
Since a ScrollView can have only one child, you can simply use this approach:
ScrollView scrollView = //...
View.OnClickListener mOnClickListener = new View.OnClickListener() {
#Override
public void onClick(View view) {
// ...
}
//Set the onClickListener directly on the ScrollView's child
scrollView.getChildAt(0).setOnClickListener(mOnClickListener);
It is because the child of the ScrollView is getting the touch event of the user and not the ScrollView. You must set android:clickable="false" attribute to each and every child of the ScrollView for the onClickListener to work on ScrollView.
Or else the alternate could be to set the onClickListener on each of the ScrollView's children and handle it.
UPDATE 22/12/2020
sadly this also triggers after each scroll event.
This the actually the answer to the question without any odd cases by using View.OnTouchListener instead of View.OnClickListener on the ScrollView and detecting the MotionEvent.ACTION_UP where the finger is left off the screen.
To make sure that it's not a scroll, then save previous touched screen x, y values of the MotionEvent.ACTION_DOWN and compare it to those of MotionEvent.ACTION_UP. If they are not equal then certainly the user is moving their finger (i.e. scrolling) before they left it off the screen.
int mXOld, mYOld; // field values to save the tap down on the ScrollView
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mXOld = x;
mYOld = y;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (x == mXOld || y == mYOld) { // detecting it's not a horizontal/vertical scrolling in ScrollView
// HERE add the code you need when the ScrollView is clicked
Toast.makeText(MainActivity.this, "Click", Toast.LENGTH_SHORT).show();
return false;
}
}
return false;
}
});
Original Answer: Odd case that is different than the question
My problem was somehow different, so I wanted to share it..
I have a ScrollView that I have to use match_parent in its width & height; and I have an internal TextView that is centered in the ScrollView.
The text of the TextView can be long so it occupies the full height of the ScrollView, and sometimes it can be short, so there will be blank areas on the top and bottom., So setting the OnClickListener on the TextView didn't help me whenever the text is short as I want the blank areas detects the click event as well; and also the OnClickListener on the ScrollView doesn't work..
So, I solved this by setting OnTouchListener on the ScrollView and put code into MotionEvent.ACTION_UP So it can kind of simulating complete tap by lefting off the finger off the screen.
private View.OnTouchListener mScrollViewTouchListener = new View.OnTouchListener() {
#SuppressLint("ClickableViewAccessibility")
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// DO Something HERE...
}
return false;
}
};
As #Zain pointed out, sometimes it is necessary to capture OnClicks for the whole area of the Scrollview, while the childs may be smaller or invisible.
To circumvent scrolling detected as an onClick, we used a GestureDetector:
final protected GestureDetector gestureDetector = new GestureDetector(getActivity(),
new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
in onCreateView
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if(gestureDetector.onTouchEvent(event)){
[do stuff]
}
return false;
}
});
I think you can custom a ScrollView, override the dispatchTouchEvent method, add add the custom onClick callback.
I'm having a problem that my method
#Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
is never called. Any ideas why is that so? I'm building a google's API 4.0.3 application, and I'm trying to enable swipes for my ViewFliper. However, it can't work because touch is never called.
Code:
public class MainActivity extends SherlockMapActivity implements ActionBar.TabListener {
Thats the declaration of my activity. and to detect swipes i have implemented that:
SimpleOnGestureListener simpleOnGestureListener = new SimpleOnGestureListener(){
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY) {
float sensitvity = 50;
if((e1.getX() - e2.getX()) > sensitvity){
SwipeLeft();
}else if((e2.getX() - e1.getX()) > sensitvity){
SwipeRight();
}
return true;
}
};
GestureDetector gestureDetector= new GestureDetector(simpleOnGestureListener);
Thank You.
edit:
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<ViewFlipper
android:id="#+id/ViewFlipper"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#ffffff" >
<include
android:layout_height="match_parent"
layout="#layout/mymain" />
<include layout="#layout/secondmain" />
<include layout="#layout/thirdmain" />
<include layout="#layout/fourthmain" />
</ViewFlipper>
</LinearLayout>
Edit2: all of my included layouts have scrollview implemented. Is it possible that scroll takes those events and handles them? And how to detect gestures if so?
I found a perfect solution. I implemented new method:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
View v = getCurrentFocus();
boolean ret = super.dispatchTouchEvent(event);
and now it all works fine!
Edit:
My final code:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
View v = getCurrentFocus();
if (v instanceof EditText) {
View w = getCurrentFocus();
int scrcoords[] = new int[2];
w.getLocationOnScreen(scrcoords);
float x = event.getRawX() + w.getLeft() - scrcoords[0];
float y = event.getRawY() + w.getTop() - scrcoords[1];
if (event.getAction() == MotionEvent.ACTION_UP
&& (x < w.getLeft() || x >= w.getRight() || y < w.getTop() || y > w
.getBottom())) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindow().getCurrentFocus()
.getWindowToken(), 0);
}
}
boolean ret = super.dispatchTouchEvent(event);
return ret;
}
As I wrote in Gabrjan's post's comments that this fires continuously while touching the screen, there's actually an easy way to get only the touch down events:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
System.out.println("TOUCH DOWN!");
//set immersive mode here, or whatever...
}
return super.dispatchTouchEvent(event);
}
This was very useful to me to put the Android into immersive mode whenever any part of the screen was touched regardless which element. But I didn't wish to set immersive mode repeatedly!
ok, now i'm sure that the problem is that scrollview handle touches, so anyway to ignore that and yet be the scrolling avaiable?
Yes that's the problem, when android handles touch events each event goes from child to parent, so first it's handled by ViewFlipper, but then it goes to ScrollView. So you have to implement getParent().requestDisallowInterceptTouchEvent(true) (see ViewParent class) in order to make all touch events handled by ViewFlipper, and then simply detect the direction of gesture if horizontal then flip view if not then pass touch event to ScrollView or just scroll ScrollView programmatically
EDIT: Also you can implement OnTouchListener in your ViewFlipper and in this listener trigger GestureDetector.onTouchEvent(event), but this also requires requestDisallowInterceptTouchEvent of your parent view set to true
All gestures and touch events goes to the lowest element in view hierarchy who can handle it.
So if you have any listener in included layout you can call ((TypeOfParrent)yourView.getParent()).onTouchEvent(event) to delegate event to the handler you want.
ADD: I recoment you to use ViewPager for flipping views.
In the ViewPager you dont need to implements your own onGestureListener.
http://www.edumobile.org/android/android-beginner-tutorials/view-pager-example-in-android-development/
http://developer.android.com/reference/android/support/v4/view/ViewPager.html