/* * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.remixer.ui.gesture; import android.annotation.SuppressLint; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; import com.google.android.libraries.remixer.ui.view.RemixerFragment; /** * A Gesture Listener that expects multi-finger swipes in a direction to trigger the display of a * RemixerFragment. * * <p>It is configurable, allowing you to set the number of fingers (two or more), and the direction * of the swipe. It can be set up by calling {@link #attach(FragmentActivity, Direction, int, * RemixerFragment)} or {@link RemixerFragment#attachToGesture(FragmentActivity, Direction, int)}. */ public class GestureListener implements View.OnTouchListener { /** * Minimum movement in pixels to consider this a successful swipe */ private static final int THRESHOLD = 100; /** * The FragmentManager for the activity, it is used to show the RemixerFragment. */ private final FragmentManager fragmentManager; /** * Minimum number of fingers to swipe to trigger the callback. */ private final int numberOfFingers; /** * Direction to swipe. */ private final Direction direction; /** * The remixer fragment to show once the swipe succeeds. */ private final RemixerFragment remixerFragment; /** * Map of all the fingers that are currently touching the screen. */ private SparseArray<FingerPosition> fingerPositions = new SparseArray<>(); /** * Attaches a GestureListener to {@code activity} that watches for swipes in the direction {@code * direction} with at least {@code numberOfFingers} and shows {@code remixerFragment} when found. */ public static void attach( FragmentActivity activity, Direction direction, int numberOfFingers, RemixerFragment remixerFragment) { activity.findViewById(android.R.id.content).setOnTouchListener( new GestureListener( direction, numberOfFingers, activity.getSupportFragmentManager(), remixerFragment)); } private GestureListener( Direction direction, int numberOfFingers, FragmentManager fragmentManager, RemixerFragment remixerFragment) { if (numberOfFingers < 2) { throw new IllegalArgumentException("GestureListener requires at least 2 fingers"); } this.direction = direction; this.numberOfFingers = numberOfFingers; this.fragmentManager = fragmentManager; this.remixerFragment = remixerFragment; } @Override @SuppressLint("ClickableViewAccessibility") public boolean onTouch(View view, MotionEvent motionEvent) { int pointerIndex = motionEvent.getActionIndex(); int pointerId = motionEvent.getPointerId(pointerIndex); FingerPosition position; switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Paranoia mode, if it's the first finger down make sure that we're in a clean slate fingerPositions.clear(); // fall-through case MotionEvent.ACTION_POINTER_DOWN: position = new FingerPosition(motionEvent.getX(pointerIndex), motionEvent.getY(pointerIndex)); fingerPositions.put(pointerId, position); break; case MotionEvent.ACTION_MOVE: // When the action is MotionEvent.ACTION_MOVE, the action index is meaningless, it is // always the original pointer from ACTION_DOWN, you have to iterate through all of them // because they all could contain updates. for (int i = 0; i < motionEvent.getPointerCount(); i++) { int movedPointerId = motionEvent.getPointerId(i); position = fingerPositions.get(movedPointerId); position.setCurrentPosition(motionEvent.getX(i), motionEvent.getY(i)); } break; case MotionEvent.ACTION_UP: boolean correctlySwiped = fingerPositions.size() == numberOfFingers; for (int i = 0; i < fingerPositions.size() && correctlySwiped; i++) { if (!fingerPositions.valueAt(i).movedInDirectionPastThreshold(direction)) { correctlySwiped = false; } } if (correctlySwiped) { remixerFragment.showRemixer(fragmentManager, RemixerFragment.REMIXER_TAG); } // fall-through case MotionEvent.ACTION_CANCEL: fingerPositions.clear(); break; case MotionEvent.ACTION_POINTER_UP: break; } return true; } /** * Simple struct that represents the finger position, including a value for where it used to be * when it first touched the screen. */ private static class FingerPosition { private final float initialX; private final float initialY; private float currentX; private float currentY; private FingerPosition(float initialX, float initialY) { this.initialX = initialX; this.initialY = initialY; currentX = initialX; currentY = initialY; } public void setCurrentPosition(float currentX, float currentY) { this.currentX = currentX; this.currentY = currentY; } /** * Checks whether this finger has moved at least {@link #THRESHOLD} in the correct direction. */ public boolean movedInDirectionPastThreshold(Direction direction) { return direction.checkMovement(currentX - initialX, currentY - initialY, THRESHOLD); } } }