/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.acdisplay.ui.view;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import com.achep.base.utils.ViewUtils;
/**
* Abstract class that forwards touch events to a {@link ForwardingLayout}.
*/
public class ForwardingListener implements
View.OnTouchListener, View.OnAttachStateChangeListener {
/**
* Scaled touch slop, used for detecting movement outside bounds.
*/
private final float mScaledTouchSlop;
/**
* Timeout before disallowing intercept on the source's parent.
*/
private final int mTapTimeout;
/**
* Source view from which events are forwarded.
*/
private final View mSrc;
private final ForwardingLayout mDst;
/**
* Runnable used to prevent conflicts with scrolling parents.
*/
private Runnable mDisallowIntercept;
/**
* Whether this listener is currently forwarding touch events.
*/
private boolean mForwarding;
/**
* The id of the first pointer down in the current event stream.
*/
private int mActivePointerId;
private final boolean mImmediately;
public ForwardingListener(View src) {
this(src, false);
}
public ForwardingListener(View src, boolean immediately) {
this(src, immediately, null);
}
public ForwardingListener(View src, boolean immediately, ForwardingLayout dst) {
mSrc = src;
mDst = dst;
mImmediately = immediately;
mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
mTapTimeout = ViewConfiguration.getTapTimeout();
src.addOnAttachStateChangeListener(this);
}
/**
* Returns the layout to which this listener is forwarding events.
* <p>
* Override this to return the correct layout. If the layout is displayed
* asynchronously, you may also need to override
* {@link #onForwardingStopped} to prevent premature cancelation of
* forwarding.
*
* @return the layout to which this listener is forwarding events
*/
public ForwardingLayout getForwardingLayout() {
return null;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
final boolean wasForwarding = mForwarding;
final boolean forwarding;
if (wasForwarding) {
forwarding = onTouchForwarded(event) || !onForwardingStopped();
} else {
forwarding = onTouchObserved(event) && onForwardingStarted();
if (mImmediately && forwarding) {
mForwarding = true;
onTouch(v, event);
}
}
mForwarding = forwarding;
return forwarding || wasForwarding;
}
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
mForwarding = false;
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
if (mDisallowIntercept != null) {
mSrc.removeCallbacks(mDisallowIntercept);
}
}
/**
* Called when forwarding would like to start.
*
* @return true to start forwarding, false otherwise
*/
protected boolean onForwardingStarted() {
return true;
}
/**
* Called when forwarding would like to stop.
*
* @return true to stop forwarding, false otherwise
*/
protected boolean onForwardingStopped() {
return true;
}
/**
* Observes motion events and determines when to start forwarding.
*
* @param srcEvent motion event in source view coordinates
* @return true to start forwarding motion events, false otherwise
*/
private boolean onTouchObserved(MotionEvent srcEvent) {
final View src = mSrc;
if (!src.isEnabled()) {
return false;
}
final int actionMasked = srcEvent.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = srcEvent.getPointerId(0);
if (!mImmediately) {
if (mDisallowIntercept == null) {
mDisallowIntercept = new DisallowIntercept();
}
src.postDelayed(mDisallowIntercept, mTapTimeout);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
final float x = srcEvent.getX(activePointerIndex);
final float y = srcEvent.getY(activePointerIndex);
if (!ViewUtils.pointInView(src, x, y, mScaledTouchSlop) || mImmediately) {
// The pointer has moved outside of the view.
if (mDisallowIntercept != null) {
src.removeCallbacks(mDisallowIntercept);
}
src.getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mDisallowIntercept != null) {
src.removeCallbacks(mDisallowIntercept);
}
break;
}
return false;
}
/**
* Handled forwarded motion events and determines when to stop
* forwarding.
*
* @param srcEvent motion event in source view coordinates
* @return true to continue forwarding motion events, false to cancel
*/
private boolean onTouchForwarded(MotionEvent srcEvent) {
final View src = mSrc;
final ForwardingLayout dst = mDst != null ? mDst : getForwardingLayout();
if (dst == null || !dst.isShown()) {
return false;
}
// Convert event to destination-local coordinates.
final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
assert dstEvent != null;
ViewUtils.toGlobalMotionEvent(src, dstEvent);
ViewUtils.toLocalMotionEvent(dst, dstEvent);
// Forward converted event to destination view, then recycle it.
final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
dstEvent.recycle();
return handled;
}
private class DisallowIntercept implements Runnable {
@Override
public void run() {
final ViewParent parent = mSrc.getParent();
parent.requestDisallowInterceptTouchEvent(true);
}
}
}