OsmAnd/eclipse-compile/observable/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java
2015-03-23 01:54:51 +01:00

286 lines
12 KiB
Java
Executable file

/*
* Copyright 2014 Soichiro Kashima
*
* 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.github.ksoichiro.android.observablescrollview;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
/**
* A layout that delegates interception of touch motion events.
* This layout is provided to move the container of Scrollable views using scroll position.
* Please note that this class overrides or uses touch events API such as onTouchEvent,
* onInterceptTouchEvent and dispatchTouchEvent,
* so be careful when you handle touches with this layout.
*/
public class TouchInterceptionFrameLayout extends FrameLayout {
/**
* Callbacks for TouchInterceptionFrameLayout.
*/
public interface TouchInterceptionListener {
/**
* Determines whether the layout should intercept this event.
*
* @param ev motion event
* @param moving true if this event is ACTION_MOVE type
* @param diffX difference between previous X and current X, if moving is true
* @param diffY difference between previous Y and current Y, if moving is true
* @return true if the layout should intercept
*/
boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY);
/**
* Called if the down motion event is intercepted by this layout.
*
* @param ev motion event
*/
void onDownMotionEvent(MotionEvent ev);
/**
* Called if the move motion event is intercepted by this layout.
*
* @param ev motion event
* @param diffX difference between previous X and current X
* @param diffY difference between previous Y and current Y
*/
void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY);
/**
* Called if the up (or cancel) motion event is intercepted by this layout.
*
* @param ev motion event
*/
void onUpOrCancelMotionEvent(MotionEvent ev);
}
private boolean mIntercepting;
private boolean mDownMotionEventPended;
private boolean mBeganFromDownMotionEvent;
private boolean mChildrenEventsCanceled;
private PointF mInitialPoint;
private MotionEvent mPendingDownMotionEvent;
private TouchInterceptionListener mTouchInterceptionListener;
public TouchInterceptionFrameLayout(Context context) {
super(context);
}
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setScrollInterceptionListener(TouchInterceptionListener listener) {
mTouchInterceptionListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener == null) {
return false;
}
// In here, we must initialize touch state variables
// and ask if we should intercept this event.
// Whether we should intercept or not is kept for the later event handling.
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialPoint = new PointF(ev.getX(), ev.getY());
mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
mDownMotionEventPended = true;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
mBeganFromDownMotionEvent = mIntercepting;
mChildrenEventsCanceled = false;
return mIntercepting;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
return mIntercepting;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mIntercepting) {
mTouchInterceptionListener.onDownMotionEvent(ev);
duplicateTouchEventForChildren(ev);
return true;
}
break;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
if (mIntercepting) {
// If this layout didn't receive ACTION_DOWN motion event,
// we should generate ACTION_DOWN event with current position.
if (!mBeganFromDownMotionEvent) {
mBeganFromDownMotionEvent = true;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
mTouchInterceptionListener.onDownMotionEvent(event);
mInitialPoint = new PointF(ev.getX(), ev.getY());
diffX = diffY = 0;
}
// Children's touches should be canceled
if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL));
}
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
// If next mIntercepting become false,
// then we should generate fake ACTION_DOWN event.
// Therefore we set pending flag to true as if this is a down motion event.
mDownMotionEventPended = true;
// Whether or not this event is consumed by the listener,
// assume it consumed because we declared to intercept the event.
return true;
} else {
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev, event);
} else {
duplicateTouchEventForChildren(ev);
}
// If next mIntercepting become true,
// then we should generate fake ACTION_DOWN event.
// Therefore we set beganFromDownMotionEvent flag to false
// as if we haven't received a down motion event.
mBeganFromDownMotionEvent = false;
// Reserve children's click cancellation here if they've already canceled
mChildrenEventsCanceled = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mBeganFromDownMotionEvent = false;
if (mIntercepting) {
mTouchInterceptionListener.onUpOrCancelMotionEvent(ev);
}
// Children's touches should be canceled regardless of
// whether or not this layout intercepted the consecutive motion events.
if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev, event);
} else {
duplicateTouchEventForChildren(ev);
}
}
return true;
}
}
return super.onTouchEvent(ev);
}
private MotionEvent obtainMotionEvent(MotionEvent base, int action) {
MotionEvent ev = MotionEvent.obtainNoHistory(base);
ev.setAction(action);
return ev;
}
/**
* Duplicate touch events to child views.
* We want to dispatch a down motion event and the move events to
* child views, but calling dispatchTouchEvent() causes StackOverflowError.
* Therefore we do it manually.
*
* @param ev motion event to be passed to children
* @param pendingEvents pending events like ACTION_DOWN. This will be passed to the children before ev
*/
private void duplicateTouchEventForChildren(MotionEvent ev, MotionEvent... pendingEvents) {
if (ev == null) {
return;
}
for (int i = getChildCount() - 1; 0 <= i; i--) {
View childView = getChildAt(i);
if (childView != null) {
Rect childRect = new Rect();
childView.getHitRect(childRect);
MotionEvent event = MotionEvent.obtainNoHistory(ev);
if (!childRect.contains((int) event.getX(), (int) event.getY())) {
continue;
}
float offsetX = -childView.getLeft();
float offsetY = -childView.getTop();
boolean consumed = false;
if (pendingEvents != null) {
for (MotionEvent pe : pendingEvents) {
if (pe != null) {
MotionEvent peAdjusted = MotionEvent.obtainNoHistory(pe);
peAdjusted.offsetLocation(offsetX, offsetY);
consumed |= childView.dispatchTouchEvent(peAdjusted);
}
}
}
event.offsetLocation(offsetX, offsetY);
consumed |= childView.dispatchTouchEvent(event);
if (consumed) {
break;
}
}
}
}
}