package yuku.alkitab.base.widget;
import android.content.Context;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import yuku.alkitab.debug.BuildConfig;
public class TwofingerLinearLayout extends LinearLayout {
public static final String TAG = TwofingerLinearLayout.class.getSimpleName();
State state = State.none;
Mode mode = null;
Listener listener;
// position of one finger (for swiping left and right)
PointF onefingerStart = new PointF();
// distance when twofinger starts
float startDist;
// distance when twofinger enters scale mode
float startScaleDist;
// average position when twofinger starts
PointF startAvg = new PointF();
// minimum distance to be considered swipe
float threshold_swipe;
// minimum distance to be considered drag
float threshold_twofinger_drag;
// minimum distance change to be considered scale
float threshold_twofinger_scale;
// if not enabled, one finger gestures, i.e. swipe, will not be captured.
// by default it's true
boolean onefingerEnabled = true;
// if not enabled, two finger gestures will not be captured.
// by default it's true
boolean twofingerEnabled = true;
public TwofingerLinearLayout(Context context) {
super(context);
init();
}
public TwofingerLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
final float density = getResources().getDisplayMetrics().density;
threshold_swipe = 48.f * density;
threshold_twofinger_drag = 48.f * density;
threshold_twofinger_scale = 72.f * density;
}
public void setOnefingerEnabled(final boolean onefingerEnabled) {
this.onefingerEnabled = onefingerEnabled;
}
public void setTwofingerEnabled(final boolean twofingerEnabled) {
this.twofingerEnabled = twofingerEnabled;
}
public void setListener(final Listener listener) {
this.listener = listener;
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
final int action = event.getActionMasked();
final int pointerCount = event.getPointerCount();
if (BuildConfig.DEBUG) Log.d(TAG, "Touch (((" + actionToString(action) + " pointer_count=" + pointerCount + "))) " + state);
float x1 = event.getX(0);
float y1 = event.getY(0);
float x2 = 0;
float y2 = 0;
if (pointerCount >= 2) {
x2 = event.getX(1);
y2 = event.getY(1);
if (BuildConfig.DEBUG) Log.d(TAG, String.format("--- " + pointerCount + " pointer: (%f,%f) (%f,%f)", x1, y1, x2, y2));
}
if (state == State.onefinger_left) {
listener.onOnefingerLeft();
state = State.none;
mode = null;
return true;
} else if (state == State.onefinger_right) {
listener.onOnefingerRight();
state = State.none;
mode = null;
return true;
} else if (state == State.twofinger_start) {
if (pointerCount >= 2) {
startDist = dist(x1 - x2, y1 - y2);
startAvg.x = 0.5f * (x1 + x2);
startAvg.y = 0.5f * (y1 + y2);
if (BuildConfig.DEBUG) Log.d(TAG, "### Start dist=" + startDist + " avg=" + startAvg);
listener.onTwofingerStart();
state = State.twofinger_performing;
}
return true;
} else if (state == State.twofinger_performing) {
if (pointerCount >= 2) {
float nowDist = dist(x1 - x2, y1 - y2);
float nowAvgX = 0.5f * (x1 + x2);
float nowAvgY = 0.5f * (y1 + y2);
float dx = nowAvgX - startAvg.x;
float dy = nowAvgY - startAvg.y;
if (BuildConfig.DEBUG) Log.d(TAG, ">>>>>> drag=(" + dx + "," + dy + ")");
// start condition
if (mode == null) {
float scale = nowDist / startDist;
float distChange = Math.abs(nowDist - startDist);
if (BuildConfig.DEBUG) Log.d(TAG, ">>>>>> scale=" + scale);
// Scale mode is started when scale differs by 10~15% or more
// and distance between two fingers changes by a certain threshold
if ((scale < 0.9f || scale >= 1.15f) && (distChange > threshold_twofinger_scale)) {
mode = Mode.scale;
startScaleDist = nowDist;
}
}
if (mode == null) {
if (dx > threshold_twofinger_drag || dx < -threshold_twofinger_drag) {
if (Math.abs(dy) < Math.abs(dx) * 0.5f) {
mode = Mode.drag_x; // drag in x
}
}
if (dy > threshold_twofinger_drag || dy < -threshold_twofinger_drag) {
if (Math.abs(dx) < Math.abs(dy) * 0.5f) {
mode = Mode.drag_y; // drag in y
}
}
}
if (mode != null) {
if (BuildConfig.DEBUG) Log.d(TAG, " RESULT: " + mode);
if (mode == Mode.scale) {
listener.onTwofingerScale(nowDist / startScaleDist);
} else if (mode == Mode.drag_x) {
listener.onTwofingerDragX(dx);
} else if (mode == Mode.drag_y) {
listener.onTwofingerDragY(dy);
}
}
return true;
} else {
listener.onTwofingerEnd(mode);
state = State.none;
mode = null;
return true;
}
} else {
listener.onTwofingerEnd(mode);
state = State.none;
mode = null;
return super.onTouchEvent(event);
}
}
float dist(float dx, float dy) {
return (float) Math.sqrt(dx * dx + dy * dy);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
final int pointerCount = event.getPointerCount();
if (BuildConfig.DEBUG) Log.d(TAG, "Intercept (((" + actionToString(action) + " pointer_count=" + pointerCount + ")))" + state);
// one finger for swipe left/right
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
onefingerStart.x = event.getX();
onefingerStart.y = event.getY();
} else if (action == MotionEvent.ACTION_MOVE) {
if (onefingerStart.x == Float.MIN_VALUE) {
// invalidated
} else {
float dx = event.getX() - onefingerStart.x;
float dy = event.getY() - onefingerStart.y;
float ady = Math.abs(dy);
if (onefingerEnabled && dx > threshold_swipe && ady < 0.5f * threshold_swipe) {
// swipe to right
state = State.onefinger_right;
return true;
} else if (onefingerEnabled && dx < -threshold_swipe && ady < 0.5f * threshold_swipe) {
// swipe to left
state = State.onefinger_left;
return true;
} else if (ady > threshold_swipe) {
// invalidate
onefingerStart.x = Float.MIN_VALUE;
}
}
}
} else if (pointerCount == 2 && twofingerEnabled) {
if (action == MotionEvent.ACTION_POINTER_DOWN) {
state = State.twofinger_start;
return true;
}
}
return false;
}
enum State {
none,
onefinger_left,
onefinger_right,
twofinger_start,
twofinger_performing,
}
public enum Mode {
scale,
drag_x,
drag_y,
}
public interface Listener {
void onOnefingerLeft();
void onOnefingerRight();
void onTwofingerStart();
void onTwofingerScale(float scale);
void onTwofingerDragX(float dx);
void onTwofingerDragY(float dy);
void onTwofingerEnd(Mode mode);
}
public abstract static class OnefingerListener implements Listener {
@Override
public void onTwofingerStart() {}
@Override
public void onTwofingerScale(final float scale) {}
@Override
public void onTwofingerDragX(final float dx) {}
@Override
public void onTwofingerDragY(final float dy) {}
@Override
public void onTwofingerEnd(final Mode mode) {}
}
// From API 19
/**
* Returns a string that represents the symbolic name of the specified unmasked action
* such as "ACTION_DOWN", "ACTION_POINTER_DOWN(3)" or an equivalent numeric constant
* such as "35" if unknown.
*
* @param action The unmasked action.
* @return The symbolic name of the specified action.
* @see android.view.MotionEvent#getAction()
*/
public static String actionToString(int action) {
switch (action) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
case MotionEvent.ACTION_OUTSIDE:
return "ACTION_OUTSIDE";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_HOVER_MOVE:
return "ACTION_HOVER_MOVE";
case MotionEvent.ACTION_SCROLL:
return "ACTION_SCROLL";
case MotionEvent.ACTION_HOVER_ENTER:
return "ACTION_HOVER_ENTER";
case MotionEvent.ACTION_HOVER_EXIT:
return "ACTION_HOVER_EXIT";
}
int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
return "ACTION_POINTER_DOWN(" + index + ")";
case MotionEvent.ACTION_POINTER_UP:
return "ACTION_POINTER_UP(" + index + ")";
default:
return Integer.toString(action);
}
}
}