I am placing custom child views into a ViewPager. I don't have any significant experience with intercepting touch events so this is all quite new to me.
I've looked at a large number of questions related to this on SO and other blogs, but so far none of the suggested advice has helped me too much.
My custom view has two panels which overlap one another. I need to allow the user to swipe away the front-most panel without passing those touch events to the parent ViewPager.
I'm having a lot of difficulty understanding this since it seems that my view's onTouchEvent
is always called despite returning false
when appropriate in my view's onInterceptTouchEvent
. My understanding may be wrong here, but I have the impression that returning false
should mean that my view's onTouchEvent
should not be called.
Here is the code for my custom view:
package com.example.dm78.viewpagertouchexample;
import android.content.Context;
import android.support.v4.view.GestureDetectorCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MyCustomView extends FrameLayout {
public static final String TAG = MyCustomView.class.getSimpleName();
private GestureDetectorCompat mGestureDetector;
private PanelData mPanelData;
private LinearLayout mFrontPanel;
private float xAdditive;
private float mFrontPanelInitialX = Float.NaN;
private float mDownX = Float.NaN;
private float frontPanelXSwipeThreshold = 100; // arbitrary value
public MyCustomView(Context context, PanelData holder) {
super(context);
mPanelData = holder;
init();
}
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mGestureDetector = new GestureDetectorCompat(getContext(), new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
View view = View.inflate(getContext(), R.layout.my_custom_view, this);
mFrontPanel = (LinearLayout) view.findViewById(R.id.front_panel);
FrameLayout mRearPanel = (FrameLayout) findViewById(R.id.rear_panel);
TextView mFrontTextView = (TextView) findViewById(R.id.front_textView);
TextView mRearTextView = (TextView) findViewById(R.id.rear_textView);
mFrontTextView.setText(mPanelData.frontText);
mRearTextView.setText(mPanelData.rearText);
if (mPanelData.showFrontPanel) {
mFrontPanel.setVisibility(View.VISIBLE);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mFrontPanelInitialX = mFrontPanel.getX();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev) || mGestureDetector.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mPanelData.showFrontPanel) {
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float dX;
long duration;
boolean result = false;
int xDir = 0;
if (mDownX != Float.NaN) {
xDir = (int) Math.abs(event.getRawX() - mDownX);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// for later ACTION_MOVE events, we'll add xAdditive to event.getRawX() to get a dX for animations
xAdditive = mFrontPanel.getX() - event.getRawX();
mDownX = event.getRawX();
result = true;
break;
case MotionEvent.ACTION_MOVE:
// left movement detected
if (xDir < 0) {
// animate panel interactively with new events coming in
// assume we haven't passed the point of no return
dX = event.getRawX() + xAdditive;
duration = 0;
if ((mFrontPanel.getX() + dX) < frontPanelXSwipeThreshold) {
// go ahead and animate the panel away
dX = -mFrontPanel.getWidth();
duration = 200;
}
mFrontPanel.animate().x(dX).setDuration(duration).start();
result = true;
}
break;
case MotionEvent.ACTION_UP:
// test value here is arbitrary
if (xDir < -10) {
int newX;
// if panel has been moved left enough, just animate it away
if (mFrontPanel.getX() < frontPanelXSwipeThreshold) {
newX = -mFrontPanel.getWidth();
}
// otherwise, animate return to initial position
else {
newX = -getContext().getResources().getDisplayMetrics().widthPixels;
}
mFrontPanel.animate().x(newX).setDuration(200).start();
result = true;
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
result = super.onTouchEvent(event) || mGestureDetector.onTouchEvent(event);
}
return result;
}
public static class PanelData {
public String rearText;
public String frontText;
public boolean showFrontPanel;
}
}
Right now, there is nothing happening that doesn't normally happen in a ViewPager. My child view completely ignores the touch input. I'm sure I'm doing something stupid here, but I don't have enough experience with this portion of the Android SDK to know what it is.
I've tossed a GestureDetector.OnGestureListener
in on the suggestion of some blog, but I haven't found much use for it and don't know how it might help me to begin with.
Can you find something obviously wrong with my code? Am I even on the right track?
Update 2015-09-01:
I've discovered that ViewParent#requestDisallowInterceptTouchEvent(boolean)
method should probably be called on my view's parent at a couple of points. It seems like perhaps onInterceptTouchEvent
might be a good place to set the flag and in onTouchEvent
in the case for ACTION_UP
might be a good place to cancel it. This is obviously not correct, since when I scroll to a page that has a front panel that the user needs to swipe away, the ViewPager stops scrolling entirely and there is no user-visible response from the app. How can I make this work?
Working example app repo: https://gitlab.com/dm78/ViewPagerTouchExample/tree/master
There were a number of problems with my code.
Math.abs()
when determining the direction of motion for an event.requestDisallowInterceptTouchEvent(boolean)
on my view's parent in a few places to enable/disable the parent from swallowing up all of the events being generated on my view.By using requestDisallowInterceptTouchEvent(boolean)
in my view, this allows the child to handle the events rather than tightly coupling the view to the ViewPager
that is displaying it, which I feel is far preferable since this technique can be applied to (as far as I can imagine) just about any view anywhere in any UI context to get the desired behavior. It doesn't rely on the other elements of the view knowing intimate details about what is going on in order for the solution to function correctly.
After solving the above problems, it was just a matter of tweaking how the animations are triggered to achieve the desired interaction.
My solution still has one minor problem, but that is for another SO question.