I am having trouble with a portion of an application I am working on regarding gesture recognition which has been driving me nuts for a good few hours now...
I have an activity containing two FrameLayouts which fill the entire screen, one contains the main content for the app, the other a chat log. Only one is set to visible at a time. The idea is that I can swipe right on the main content fragment and the chat fragment will become visible in a facebook style interface. Again, swiping back left on the chat fragment will bring the main content fragment back into view.
The code for the swipe listeners:
private void attachFragmentSwipeListeners() {
int screenOrientation = getResources().getConfiguration().orientation;
if (screenOrientation == Configuration.ORIENTATION_PORTRAIT) {
FrameLayout mainContent = (FrameLayout) findViewById(R.id.flMainContent);
mainContent.setOnTouchListener(new OnSwipeTouchListener(ExICS_Main.this) {
@Override
public void onSwipeLeft() {
showChatLog();
super.onSwipeLeft();
}
});
FrameLayout chatWindow = (FrameLayout) findViewById(R.id.flChatWindow);
chatWindow.setOnTouchListener(new OnSwipeTouchListener(ExICS_Main.this) {
@Override
public void onSwipeRight() {
hideChatLog();
super.onSwipeRight();
}
});
}
}
The listener itself:
public class OnSwipeTouchListener implements View.OnTouchListener {
private static final String TAG = OnSwipeTouchListener.class.getName();
private final GestureDetector gestureDetector;
public OnSwipeTouchListener(Context context) {
gestureDetector = new GestureDetector(context, new GestureListener());
}
public void onSwipeLeft() {
}
public void onSwipeRight() {
}
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_DISTANCE_THRESHOLD = 100;
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i(TAG, "OnSwipeTouchListener Fling");
float distanceX = e2.getX() - e1.getX();
float distanceY = e2.getY() - e1.getY();
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (distanceX > 0) onSwipeRight();
else onSwipeLeft();
return true;
}
return false;
}
}
}
This has been tested and works all as it should which is great when using two placeholder fragments containing simply textviews etc. The issue I'm having is when the fragment contains an interactive widget, such as a scrollview or listview.
In these cases, the widgets are used such that they should be only scrollable in a vertical manner, however, they are consuming the fling interaction, even horizontal, such that the GestureDetectors listening on the fragment level in the application are never triggered.
What I'd like to do is when a swipe is made on the fragment, let the gesturedetector on the fragment decide if its a horizontal swipe sufficient to trigger switching between main content and chat or vice versa and if so, call show/hideChatLog() at the activity level. If not, propagate the TouchEvent back into the fragment's child view, whatever it may be, so it can do as it wants.
Alternatively, override the gesturedetection on the widget itself, and if a horizontal swipe that the fragment gesture listener is listening for, push it back up the view hierarchy and not consume it so the fragment gesture detector will see and so act on it.
I have tried creating a new gesture listener which implements the onFling method as follows and attach it to the ListView or ScrollView etc.
public class IgnoreHorizontalSwipeInterceptor implements View.OnTouchListener {
private static final String TAG = IgnoreHorizontalSwipeInterceptor.class.getName();
private final GestureDetector gestureDetector;
public IgnoreHorizontalSwipeInterceptor(Context context) {
gestureDetector = new GestureDetector(context, new GestureListener());
}
public void onSwipeLeft() {
}
public void onSwipeRight() {
}
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_DISTANCE_THRESHOLD = 100;
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i(TAG, "OnSwipeTouchListener Fling");
float distanceX = e2.getX() - e1.getX();
float distanceY = e2.getY() - e1.getY();
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
return false;
}
return false;
}
}
}
and attaching it like this:
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View fragmentView = inflater.inflate(R.layout.fragment_room_list, container, false);
ListView roomListView = (ListView) fragmentView.findViewById(R.id.lvRoomList);
...
RoomListFragmentListAdapter adapter = new RoomListFragmentListAdapter(getActivity(), R.layout.room_list_item_layout, rooms);
roomListView.setAdapter(adapter);
roomListView.setOnTouchListener(new IgnoreHorizontalSwipeInterceptor(getActivity().getBaseContext()));
return fragmentView;
}
I had hoped that by always returning false in onFling() in the IgnoreHorizontalSwipeInterceptor attached to the listView that it would cause the event to carry back up to the parent, the fragment itself but this isn't happening.
When I swipe on the listview I can see in the log OnSwipeTouchListener Fling from the IgnoreHorizontalSwipeInterceptor onFling(). If I do the same on a fragment not containing a scrollable widget, I can similarly see OnSwipeTouchListener Fling as I would hope to.
I have found the Android documentation for gestures on viewgroups but this really doesn't make much sense to me... It mentions overriding onInterceptTouchEvent but I can't figure out where it would make sense to do this or how. If this is indeed the correct approach some direction on where and how to do this would be appreciated.
Another (probably much easier) approach would be to implement an interface such that when a fragment widget detects a horizontal swipe on itself it uses a callback to the parent activity to trigger the action I am expecting, show/hideChatLog(). This seems a pretty dirty way of doing it, however, and would rather avoid if possible.
To show what I mean with layouts;
With this fragment in the content view, swiping right to reveal the chat log works as expected:
<RelativeLayout 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"
android:background="@android:color/holo_blue_bright"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="XXXX.Activities.ExICS_Main$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a fragment" />
</RelativeLayout>
But this one doesn't as the listview is consuming the touch event and it's not being passed to the fragment onTouchEvent GestureListener:
<LinearLayout 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"
tools:context="XXX.Fragments.Room_List_Fragment">
<ListView
android:id="@+id/lvRoomList"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>