286 lines
12 KiB
Java
Executable file
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|