I am using a ViewPager with a TouchImageView inside it and it works great, (I have used this solution in many of my Android apps).
However I have an app for which there are many other controls on the same screen so they are all inside a scrollview control.
In this scenario I see the scrollview does not play nice and I am not able to pan within the zoomed image. When I use my finger to pan upward or downward the entire page scrolls instead of the image panning.
So here is what I am trying to do....
Inside the TouchImageView I detect Zoom Begin and Zoom End and have created an interface to make a callback to my Activity onZoomBegin() and onZoomEnd() methods.
In the onZoomBegin() method I want to disable the scrollview from responding to any touch events and in onZoomEnd() I can re-enable it.
So far here are the things I have tried doing in the onZoomBegin() method for which none are working....
scrollView.setEnabled(false);
scrollView.requestDisallowInterceptTouchEvent(true);
also I have tried the answer to a similar question which was to takeover the onTouchListener like such:
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
This does stop the scrollview from scrolling but the scrollview is still intercepting the touch events cause the image still will not pan up or down.
I've tried checking nestedScrollingEnabled in the layout designer, no joy....
I just want to know is there a way to totally disable a scrollview and then re-enable it from responding to touch events?
I found this answer on another question somewhere but by the time I realized it was the solution to my problem (answer to my question) then I lost reference to it. I will keep looking so I can edit this post to give credit where credit is due.
public class CustomScrollView extends ScrollView {
// true if we can scroll the ScrollView
// false if we cannot scroll
private boolean scrollable = true;
public CustomScrollView(Context context) {
super(context);
}
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setScrollingEnabled(boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isScrollable() {
return scrollable;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if we can scroll pass the event to the superclass
if (scrollable)
return super.onTouchEvent(ev);
// only continue to handle the touch event if scrolling enabled
return false; // scrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (!scrollable)
return false;
else
return super.onInterceptTouchEvent(ev);
}
}
This part I just figured out for myself.... In the TouchImageView I added a callback interface which is called when a zoom begins and ends so in my Activity I only had to do this:
private class OnZoomListener implements TouchImageView.OnZoomListener {
#Override
public void onZoomBegin() {
isZoomed = true;
scrollView.scrollTo(0, 0);
scrollView.setScrollingEnabled(false); // <-- disables scrollview
hideImageControls();
sizeViewPager();
}
#Override
public void onZoomEnd() {
scrollView.setScrollingEnabled(true); // <-- enables scrollview
showImageControls();
isZoomed = false;
}
}
Related
Solved
The goal I wanted to achieve is make the ViewPager to be more easier to scroll. I have solved it the by editing the original ViewPager source code instead of extending like below.
How can i return touch coordinates to original reference frame for any child views?Here is the ViewPager java file i always return y = 0 to make it scroll whenever x change just like the launcher's home screen.
My qusetion is that i need to return the real touch motion to the child views of the ViewPager
public class myViewPager extends ViewPager
{
public myViewPager(Context context) {
super(context);
}
public myViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* always return y = 0 to make it scroll whenever x change
*/
private MotionEvent changeY(MotionEvent ev) {
ev.setLocation(ev.getX(), 0);
return ev;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev){
// how to return touch coordinates to original reference frame for any child views ?
return ???;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(changeY(ev));
}
}
How about changing it in dispatchTouchEvent? Like this:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(changeY(ev));
}
I was not very clean your problem, hope this can help you.
I have a use case where there are two views on screen one of which is partially covering another. The one that is above needs to handle scroll events and ignore touch up. The partially obscured view should handle touch up events, including those that happen in the area of overlap that are ignored by the obscuring view.
a simplified example layout is below.
the closest i've come uses GestureDetectorCompat on the top view returning true in onDown (otherwise i don't get any further events,) true in onScroll, and false in onSingleTapUp. i have tried several things in the view behind all with the same results: i get taps on the un-obscured section, but the top view eats all of the motion events for the obscured portion.
What you want to do is not as straightforward as you would probably like because of how Android handles touch event flow. So let me set the stage with a little context first:
The reason this is a tricky proposition is because Android defines a gesture as all the events between an ACTION_DOWN and the corresponding ACTION_UP. ACTION_DOWN is the only point at which the framework is searching for a touch target (which is why you have to return true for that event to see any others). Once a suitable target has been found, ALL the remaining events in that gesture will be delivered directly to that view and nobody else.
This means that if you want a single event to go to a different destination, you will have to capture and redirect it yourself. All touch events flow from parent views to child views in one long chain. Parent views control when and how touch events move from one child to the next, including modifying the coordinates of the MotionEvent so the match the local bounds of each child view. Because of this, the most effective place to manipulate touch events is in a custom ViewGroup parent implementation.
The following example comes with a big bag of assumptions. Basically, I'm assuming that both views are nothing more than a dumb View with no internal wishes to handle touch (which is probably wrong). Applying this code to other, more complex, child views may requires some rework...but this should get you started.
The best place to force touch redirection is in a common parent of the two views, since it is the origin of the touch for both (as described above).
public class TouchUpRedirectLayout extends FrameLayout implements View.OnTouchListener {
private int mTargetViewId;
private View mTargetView;
private boolean mTargetTouchActive;
private GestureDetector mGestureDetector;
public TouchUpRedirectLayout(Context context) {
super(context);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mGestureDetector = new GestureDetector(context, mGestureListener);
}
public void setTargetViewId(int resId) {
mTargetViewId = resId;
updateTargetView();
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
//Find the target view, if set, once inflated
updateTargetView();
}
//Set the target view to handle gestures
private void updateTargetView() {
if (mTargetViewId > 0) {
mTargetView = findViewById(mTargetViewId);
if (mTargetView != null) {
mTargetView.setOnTouchListener(this);
}
}
}
private Rect mHitRect = new Rect();
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
if (mTargetTouchActive) {
mTargetTouchActive = false;
//Validate the up
int index = indexOfChild(mTargetView) - 1;
if (index < 0) {
return false;
}
for (int i=index; i >= 0; i--) {
final View child = getChildAt(i);
child.getHitRect(mHitRect);
if (mHitRect.contains((int) event.getX(), (int) event.getY())) {
//Dispatch and mark handled
return child.dispatchTouchEvent(event);
}
}
//Steal this event
return true;
}
//Allow default processing
return false;
default:
//Allow default processing
return false;
}
}
//Receive touch events from the target (scroll handling) view
#Override
public boolean onTouch(View v, MotionEvent event) {
mTargetTouchActive = true;
return mGestureDetector.onTouchEvent(event);
}
//Handle gesture events in target view
private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onDown(MotionEvent e) {
Log.d("TAG", "onDown");
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d("TAG", "Scrolling...");
return true;
}
};
}
This example layout (I subclassed FrameLayout, but you could choose whichever layout you are using currently as the parent of the two views) tracks a single "target" view for the purposes of notifying the "down" and "scroll" gestures. It also notifies us when a gesture is in play that will include an ACTION_UP event that we need to capture and forward to another obscured view.
When an up event occurs, we use the intercept functionality of ViewGroup to direct that event away from the original "target" view, and dispatch it to the next available child view whose bounds fit the event. You could just as easily hard-code the second "obscured" view here as well, but I've written it to dispatch to any and all possible children underneath...similar to the way ViewGroup handles touch delegation to children in the first place.
Here is an example layout:
<com.example.touchoverlaptest.app.TouchUpRedirectLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/view_root"
android:layout_width="match_parent"
android:layout_height="400dp"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
tools:context="com.example.touchoverlaptest.app.MainActivity">
<View
android:id="#+id/view_obscured"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#7A00" />
<View
android:id="#+id/view_overlap"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_gravity="bottom"
android:background="#70A0" />
</com.example.touchoverlaptest.app.TouchUpRedirectLayout>
...and Activity with the view in action:
public class MainActivity extends Activity implements View.OnTouchListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TouchUpRedirectLayout layout = (TouchUpRedirectLayout) findViewById(R.id.view_root);
layout.setTargetViewId(R.id.view_overlap);
layout.findViewById(R.id.view_obscured).setOnTouchListener(this);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", "Obscured touch "+event.getActionMasked());
return true;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
The target view will fire all the gesture callbacks, and the obscured view will receive the up events. The OnTouchListener in the activity is simply to validate that the events are delivered.
If you would like more detail about custom touch handling in Android, here is a video link to a presentation I did recently on the topic.
I have a ViewPager with a custom PagerAdapter that overwrites the method getPageWidth to return 0.3f, so I can show multiple pages in the screen, as suggested in the following blog:
http://commonsware.com/blog/2012/08/20/multiple-view-viewpager-options.html
In this case the ViewPager starts empty and the user starts filling the pages (images) as requested. The images are filled from left to right. All is working fine and the pages are dinamically created, and it is easy to swipe between them when there are multiple images.
However there is a problem when there are less than three pages instantiated. A swipe gesture from right (no content) to left produces a flickering with pages moving from left to right alternatively, as if the ViewPager were trying to move the first pages to right but later turning to its original position. This happens during the whole gesture, and stop when leaving the gesture. As I said this does not happen when there are pages enough to cover the whole screen width and a real scroll is necessary.
Any Ideas?
I know this is an old question, but I was just looking for a solution and came across this link (which oddly references this question). Anyway, I was able to figure out a solution based on their comments. The basic idea is to allow touch events based on the state of a boolean flag which you set.
Extend ViewPager on override onInterceptTouchEvent & onTouchEvent to only call super if you've set the flag. My class looks like this:
public class MyViewPager extends ViewPager {
private boolean isPagingEnabled = false;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewPager(Context context) {
super(context);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (isPagingEnabled) {
return super.onInterceptTouchEvent(event);
}
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (isPagingEnabled) {
return super.onTouchEvent(event);
}
return false;
}
public void setPagingEnabled(boolean pagingEnabled) {
isPagingEnabled = pagingEnabled;
}
}
In your layout xml, replace your com.android.support.v4.ViewPager elements with com.yourpackage.MyViewPager elements.
Since you return 0.3f from getPageWidth() in your pager adapter, you would want scrolling enabled when the fourth item has been added to it. The tricky part is having this line of code everywhere when you define your pager adapter, and add or remove any objects from the adapter backing list.
mPager.setPagingEnabled(items.size() > 3);
Rahuls solution works, but if you want auto enabling of paging according to content, you can modify ViewPager like this:
public class MyViewPager extends ViewPager {
public MyViewPager(Context context,
AttributeSet attrs) {
super(context, attrs);
}
public MyViewPager(Context context) {
super(context);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (canScrollHorizontally(1)||canScrollHorizontally(-1)) {
return super.onInterceptTouchEvent(event);
}
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (canScrollHorizontally(1)||canScrollHorizontally(-1)) {
return super.onTouchEvent(event);
}
return false;
}
}
Now, if the ViewPager is scrollable, it will allow touches, otherwise disable.
I was able to solve it this way, in your getPageWidth() method,if there is only one image[you can get to know by checking size of array or arraylist] return 1, if the input size is greater than 1 return your decimal.
Here's the code to achieve this.
#override
public float getPageWidth(int position){
if(inpputsize==1){
return 1.0;
}
return 0.9;
}
I need to limit the swipe area inside a ViewPager. For example, if the user make the gesture to swipe on top half space of the screen it swipe to the next fragment, but if the user make the gesture on the bottom half of the screen it do nothing.
There is a way to do that?
This might be a what you need:
public class MyPager extends ViewPager {
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(inNeutralArea(ev.getX(),ev.getY())){
//--events re-directed to this ViewPager's onTouch() and to its child views from there--
return false;
}else {
//--events intercepted by this ViewPager's default implementation, where it looks for swipe gestures--
return super.onInterceptTouchEvent(ev);
}
}
private boolean inNeutralArea(float x, float y) {
//--check if x,y inside non reactive area, return true/false accordingly--
return false;
}
}
Use this MyPager class in layout xml in place of ViewPager.
Don't forget to do to the same in overriding the onTouchEvent, otherwise the ViewPager will still scroll on Android 4.1 and later. Caught me ofguard and took me a while to figure it out.
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.