/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.v4.util.Pools; import android.support.v4.util.SparseArrayCompat; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; import android.view.ViewConfiguration; /** * Compound touch delegate that forward touch events to recyclable * inner touch delegates. */ class TouchExpansionDelegate extends TouchDelegate { private static final Rect IGNORED_RECT = new Rect(); private static final Pools.SimplePool<SparseArrayCompat<InnerTouchDelegate>> sInnerTouchDelegateScrapArrayPool = new Pools.SimplePool<>(4); private final SparseArrayCompat<InnerTouchDelegate> mDelegates = new SparseArrayCompat<>(); private SparseArrayCompat<InnerTouchDelegate> mScrapDelegates; TouchExpansionDelegate(ComponentHost host) { super(IGNORED_RECT, host); } /** * Registers an inner touch delegate for the given view with the specified * expansion. It assumes the given view has its final bounds set. * * @param index The drawing order index of the given view. * @param view The view to which touch expansion should be applied. * @param touchExpansion The expansion to be applied to each edge of the given view. */ void registerTouchExpansion(int index, View view, Rect touchExpansion) { mDelegates.put(index, InnerTouchDelegate.acquire(view, touchExpansion)); } /** * Unregisters an inner touch delegate with the given index. * * @param index The drawing order index of the given view. */ void unregisterTouchExpansion(int index) { if (maybeUnregisterFromScrap(index)) { return; } final int valueIndex = mDelegates.indexOfKey(index); final InnerTouchDelegate touchDelegate = mDelegates.valueAt(valueIndex); mDelegates.removeAt(valueIndex); touchDelegate.release(); } private boolean maybeUnregisterFromScrap(int index) { if (mScrapDelegates != null) { final int valueIndex = mScrapDelegates.indexOfKey(index); if (valueIndex >= 0) { final InnerTouchDelegate touchDelegate = mScrapDelegates.valueAt(valueIndex); mScrapDelegates.removeAt(valueIndex); touchDelegate.release(); return true; } } return false; } void draw(Canvas canvas, Paint paint) { for (int i = mDelegates.size() - 1; i >= 0; i--) { canvas.drawRect(mDelegates.valueAt(i).mDelegateBounds, paint); } } @Override public boolean onTouchEvent(MotionEvent event) { for (int i = mDelegates.size() - 1; i >= 0; i--) { final InnerTouchDelegate touchDelegate = mDelegates.valueAt(i); if (touchDelegate.onTouchEvent(event)) { return true; } } return false; } /** * Called when the MountItem this Delegate is referred to is moved to another position to also * update the indexes of the TouchExpansionDelegate. */ void moveTouchExpansionIndexes(int oldIndex, int newIndex) { if (mDelegates.get(newIndex) != null) { ensureScrapDelegates(); ComponentHostUtils.scrapItemAt(newIndex, mDelegates, mScrapDelegates); } ComponentHostUtils.moveItem(oldIndex, newIndex, mDelegates, mScrapDelegates); releaseScrapDelegatesIfNeeded(); } private void ensureScrapDelegates() { if (mScrapDelegates == null) { mScrapDelegates = acquireScrapTouchDelegatesArray(); } } private static SparseArrayCompat<InnerTouchDelegate> acquireScrapTouchDelegatesArray() { SparseArrayCompat<InnerTouchDelegate> sparseArray = sInnerTouchDelegateScrapArrayPool.acquire(); if (sparseArray == null) { sparseArray = new SparseArrayCompat<>(4); } return sparseArray; } private void releaseScrapDelegatesIfNeeded() { if (mScrapDelegates != null && mScrapDelegates.size() == 0) { releaseScrapTouchDelegatesArray(mScrapDelegates); mScrapDelegates = null; } } private static void releaseScrapTouchDelegatesArray( SparseArrayCompat<InnerTouchDelegate> sparseArray) { sInnerTouchDelegateScrapArrayPool.release(sparseArray); } private static class InnerTouchDelegate { private static final Pools.SimplePool<InnerTouchDelegate> sPool = new Pools.SimplePool<>(4); private View mDelegateView; private boolean mIsHandlingTouch; private int mSlop; private final Rect mDelegateBounds = new Rect(); private final Rect mDelegateSlopBounds = new Rect(); void init(View delegateView, Rect delegateBounds) { mDelegateView = delegateView; mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); mDelegateBounds.set(delegateBounds); mDelegateSlopBounds.set(delegateBounds); mDelegateSlopBounds.inset(-mSlop, -mSlop); } boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); boolean shouldDelegateTouchEvent = false; boolean touchWithinViewBounds = true; boolean handled = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (mDelegateBounds.contains(x, y)) { mIsHandlingTouch = true; shouldDelegateTouchEvent = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: shouldDelegateTouchEvent = mIsHandlingTouch; if (mIsHandlingTouch) { if (!mDelegateSlopBounds.contains(x, y)) { touchWithinViewBounds = false; } } break; case MotionEvent.ACTION_CANCEL: shouldDelegateTouchEvent = mIsHandlingTouch; mIsHandlingTouch = false; break; } if (shouldDelegateTouchEvent) { if (touchWithinViewBounds) { // Offset event coordinates to be inside the target view. event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2); } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state). event.setLocation(-(mSlop * 2), -(mSlop * 2)); } handled = mDelegateView.dispatchTouchEvent(event); } return handled; } static InnerTouchDelegate acquire(View delegateView, Rect bounds) { InnerTouchDelegate touchDelegate = sPool.acquire(); if (touchDelegate == null) { touchDelegate = new InnerTouchDelegate(); } touchDelegate.init(delegateView, bounds); return touchDelegate; } void release() { mDelegateView = null; mDelegateBounds.setEmpty(); mDelegateSlopBounds.setEmpty(); mIsHandlingTouch = false; mSlop = 0; sPool.release(this); } } }