Loading packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +1 −44 Original line number Diff line number Diff line Loading @@ -18,12 +18,9 @@ package com.android.systemui.dreams; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; Loading Loading @@ -62,43 +59,6 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates). private final Handler mHandler; // A hook into the internal inset calculation where we declare the overlays as the only // touchable regions. private final ViewTreeObserver.OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = new ViewTreeObserver.OnComputeInternalInsetsListener() { @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets( ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); final Region region = new Region(); final Rect rect = new Rect(); final int childCount = mDreamOverlayContentView.getChildCount(); for (int i = 0; i < childCount; i++) { View child = mDreamOverlayContentView.getChildAt(i); if (mComplicationHostViewController.getView() == child) { region.op(mComplicationHostViewController.getTouchRegions(), Region.Op.UNION); continue; } if (child.getGlobalVisibleRect(rect)) { region.op(rect, Region.Op.UNION); } } // Add the notifications drag area to the tap region (otherwise the // notifications shade can't be dragged down). if (mDreamOverlayContentView.getGlobalVisibleRect(rect)) { rect.bottom = rect.top + mDreamOverlayNotificationsDragAreaHeight; region.op(rect, Region.Op.UNION); } inoutInfo.touchableRegion.set(region); } }; @Inject public DreamOverlayContainerViewController( DreamOverlayContainerView containerView, Loading Loading @@ -136,16 +96,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve @Override protected void onViewAttached() { mView.getViewTreeObserver() .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval); } @Override protected void onViewDetached() { mHandler.removeCallbacks(this::updateBurnInOffsets); mView.getViewTreeObserver() .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); } View getContainerView() { Loading @@ -162,6 +118,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // so no translation occurs when the values don't change. mView.setTranslationX(getBurnInOffset(mMaxBurnInOffset * 2, true) - mMaxBurnInOffset); mView.setTranslationY(getBurnInOffset(mMaxBurnInOffset * 2, false) - mMaxBurnInOffset); Loading packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +7 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.ViewController; import java.lang.annotation.Retention; Loading @@ -48,6 +49,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve private static final int WIFI_STATUS_AVAILABLE = 2; private final ConnectivityManager mConnectivityManager; private final TouchInsetManager.TouchInsetSession mTouchInsetSession; private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() .clearCapabilities() Loading Loading @@ -77,9 +79,11 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve @Inject public DreamOverlayStatusBarViewController( DreamOverlayStatusBarView view, ConnectivityManager connectivityManager) { ConnectivityManager connectivityManager, TouchInsetManager.TouchInsetSession touchInsetSession) { super(view); mConnectivityManager = connectivityManager; mTouchInsetSession = touchInsetSession; } @Override Loading @@ -92,11 +96,13 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve onWifiAvailabilityChanged( capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)); mTouchInsetSession.addViewToTracking(mView); } @Override protected void onViewDetached() { mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); mTouchInsetSession.clear(); } /** Loading packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +19 −5 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.Constraints; import com.android.systemui.R; import com.android.systemui.touch.TouchInsetManager; import java.util.ArrayList; import java.util.Collections; Loading @@ -52,6 +53,7 @@ public class ComplicationLayoutEngine { private static class ViewEntry implements Comparable<ViewEntry> { private final View mView; private final ComplicationLayoutParams mLayoutParams; private final TouchInsetManager.TouchInsetSession mTouchInsetSession; private final Parent mParent; @Complication.Category private final int mCategory; Loading @@ -61,7 +63,8 @@ public class ComplicationLayoutEngine { * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding * view hierarchy to be accessed without traversing the entire view tree. */ ViewEntry(View view, ComplicationLayoutParams layoutParams, int category, Parent parent, ViewEntry(View view, ComplicationLayoutParams layoutParams, TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent, int margin) { mView = view; // Views that are generated programmatically do not have a unique id assigned to them Loading @@ -70,9 +73,12 @@ public class ComplicationLayoutEngine { // {@link Complication.ViewHolder} should not reference the root container by id. mView.setId(View.generateViewId()); mLayoutParams = layoutParams; mTouchInsetSession = touchSession; mCategory = category; mParent = parent; mMargin = margin; touchSession.addViewToTracking(mView); } /** Loading Loading @@ -217,6 +223,7 @@ public class ComplicationLayoutEngine { mParent.removeEntry(this); ((ViewGroup) mView.getParent()).removeView(mView); mTouchInsetSession.removeViewFromTracking(mView); } @Override Loading @@ -242,15 +249,18 @@ public class ComplicationLayoutEngine { */ private static class Builder { private final View mView; private final TouchInsetManager.TouchInsetSession mTouchSession; private final ComplicationLayoutParams mLayoutParams; private final int mCategory; private Parent mParent; private int mMargin; Builder(View view, ComplicationLayoutParams lp, @Complication.Category int category) { Builder(View view, TouchInsetManager.TouchInsetSession touchSession, ComplicationLayoutParams lp, @Complication.Category int category) { mView = view; mLayoutParams = lp; mCategory = category; mTouchSession = touchSession; } /** Loading Loading @@ -291,7 +301,8 @@ public class ComplicationLayoutEngine { * Builds and returns the resulting {@link ViewEntry}. */ ViewEntry build() { return new ViewEntry(mView, mLayoutParams, mCategory, mParent, mMargin); return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent, mMargin); } } Loading Loading @@ -442,13 +453,16 @@ public class ComplicationLayoutEngine { private final int mMargin; private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>(); private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>(); private final TouchInsetManager.TouchInsetSession mSession; /** */ @Inject public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, @Named(COMPLICATION_MARGIN) int margin) { @Named(COMPLICATION_MARGIN) int margin, TouchInsetManager.TouchInsetSession session) { mLayout = layout; mMargin = margin; mSession = session; } /** Loading @@ -468,7 +482,7 @@ public class ComplicationLayoutEngine { removeComplication(id); } final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, lp, category) final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category) .setMargin(mMargin); // Add position group if doesn't already exist Loading packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +18 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,9 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayContainerView; import com.android.systemui.dreams.DreamOverlayStatusBarView; import com.android.systemui.touch.TouchInsetManager; import java.util.concurrent.Executor; import javax.inject.Named; Loading Loading @@ -63,6 +66,21 @@ public abstract class DreamOverlayModule { "R.id.dream_overlay_content must not be null"); } /** */ @Provides public static TouchInsetManager.TouchInsetSession providesTouchInsetSession( TouchInsetManager manager) { return manager.createSession(); } /** */ @Provides @DreamOverlayComponent.DreamOverlayScope public static TouchInsetManager providesTouchInsetManager(@Main Executor executor, DreamOverlayContainerView view) { return new TouchInsetManager(executor, view); } /** */ @Provides @DreamOverlayComponent.DreamOverlayScope Loading packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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.android.systemui.touch; import android.graphics.Rect; import android.graphics.Region; import android.view.View; import android.view.ViewRootImpl; import java.util.HashMap; import java.util.HashSet; import java.util.concurrent.Executor; /** * {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This * is useful for passing through touch events for all but select areas. */ public class TouchInsetManager { /** * {@link TouchInsetSession} provides an individualized session with the * {@link TouchInsetManager}, linking any action to the client. */ public static class TouchInsetSession { private final TouchInsetManager mManager; private final HashSet<View> mTrackedViews; private final Executor mExecutor; private final View.OnLayoutChangeListener mOnLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateTouchRegion(); /** * Default constructor * @param manager The parent {@link TouchInsetManager} which will be affected by actions on * this session. * @param rootView The parent of views that will be tracked. * @param executor An executor for marshalling operations. */ TouchInsetSession(TouchInsetManager manager, Executor executor) { mManager = manager; mTrackedViews = new HashSet<>(); mExecutor = executor; } /** * Adds a descendant of the root view to be tracked. * @param view {@link View} to be tracked. */ public void addViewToTracking(View view) { mExecutor.execute(() -> { mTrackedViews.add(view); view.addOnLayoutChangeListener(mOnLayoutChangeListener); updateTouchRegion(); }); } /** * Removes a view from further tracking * @param view {@link View} to be removed. */ public void removeViewFromTracking(View view) { mExecutor.execute(() -> { mTrackedViews.remove(view); view.removeOnLayoutChangeListener(mOnLayoutChangeListener); updateTouchRegion(); }); } private void updateTouchRegion() { final Region cumulativeRegion = Region.obtain(); mTrackedViews.stream().forEach(view -> { final Rect boundaries = new Rect(); view.getBoundsOnScreen(boundaries); cumulativeRegion.op(boundaries, Region.Op.UNION); }); mManager.setTouchRegion(this, cumulativeRegion); cumulativeRegion.recycle(); } /** * Removes all tracked views and updates insets accordingly. */ public void clear() { mExecutor.execute(() -> { mManager.clearRegion(this); mTrackedViews.clear(); }); } } private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>(); private final Executor mExecutor; private final View mRootView; private final View.OnAttachStateChangeListener mAttachListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { updateTouchInset(); } @Override public void onViewDetachedFromWindow(View v) { } }; /** * Default constructor. * @param executor An {@link Executor} to marshal all operations on. * @param rootView The root {@link View} for all views in sessions. */ public TouchInsetManager(Executor executor, View rootView) { mExecutor = executor; mRootView = rootView; mRootView.addOnAttachStateChangeListener(mAttachListener); } /** * Creates a new associated session. */ public TouchInsetSession createSession() { return new TouchInsetSession(this, mExecutor); } private void updateTouchInset() { final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl(); if (viewRootImpl == null) { return; } final Region aggregateRegion = Region.obtain(); for (Region region : mDefinedRegions.values()) { aggregateRegion.op(region, Region.Op.UNION); } viewRootImpl.setTouchableRegion(aggregateRegion); aggregateRegion.recycle(); } protected void setTouchRegion(TouchInsetSession session, Region region) { final Region introducedRegion = Region.obtain(region); mExecutor.execute(() -> { mDefinedRegions.put(session, introducedRegion); updateTouchInset(); }); } private void clearRegion(TouchInsetSession session) { mExecutor.execute(() -> { final Region storedRegion = mDefinedRegions.remove(session); if (storedRegion != null) { storedRegion.recycle(); } updateTouchInset(); }); } } Loading
packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +1 −44 Original line number Diff line number Diff line Loading @@ -18,12 +18,9 @@ package com.android.systemui.dreams; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; Loading Loading @@ -62,43 +59,6 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates). private final Handler mHandler; // A hook into the internal inset calculation where we declare the overlays as the only // touchable regions. private final ViewTreeObserver.OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = new ViewTreeObserver.OnComputeInternalInsetsListener() { @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets( ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); final Region region = new Region(); final Rect rect = new Rect(); final int childCount = mDreamOverlayContentView.getChildCount(); for (int i = 0; i < childCount; i++) { View child = mDreamOverlayContentView.getChildAt(i); if (mComplicationHostViewController.getView() == child) { region.op(mComplicationHostViewController.getTouchRegions(), Region.Op.UNION); continue; } if (child.getGlobalVisibleRect(rect)) { region.op(rect, Region.Op.UNION); } } // Add the notifications drag area to the tap region (otherwise the // notifications shade can't be dragged down). if (mDreamOverlayContentView.getGlobalVisibleRect(rect)) { rect.bottom = rect.top + mDreamOverlayNotificationsDragAreaHeight; region.op(rect, Region.Op.UNION); } inoutInfo.touchableRegion.set(region); } }; @Inject public DreamOverlayContainerViewController( DreamOverlayContainerView containerView, Loading Loading @@ -136,16 +96,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve @Override protected void onViewAttached() { mView.getViewTreeObserver() .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval); } @Override protected void onViewDetached() { mHandler.removeCallbacks(this::updateBurnInOffsets); mView.getViewTreeObserver() .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); } View getContainerView() { Loading @@ -162,6 +118,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // so no translation occurs when the values don't change. mView.setTranslationX(getBurnInOffset(mMaxBurnInOffset * 2, true) - mMaxBurnInOffset); mView.setTranslationY(getBurnInOffset(mMaxBurnInOffset * 2, false) - mMaxBurnInOffset); Loading
packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +7 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.ViewController; import java.lang.annotation.Retention; Loading @@ -48,6 +49,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve private static final int WIFI_STATUS_AVAILABLE = 2; private final ConnectivityManager mConnectivityManager; private final TouchInsetManager.TouchInsetSession mTouchInsetSession; private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() .clearCapabilities() Loading Loading @@ -77,9 +79,11 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve @Inject public DreamOverlayStatusBarViewController( DreamOverlayStatusBarView view, ConnectivityManager connectivityManager) { ConnectivityManager connectivityManager, TouchInsetManager.TouchInsetSession touchInsetSession) { super(view); mConnectivityManager = connectivityManager; mTouchInsetSession = touchInsetSession; } @Override Loading @@ -92,11 +96,13 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve onWifiAvailabilityChanged( capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)); mTouchInsetSession.addViewToTracking(mView); } @Override protected void onViewDetached() { mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); mTouchInsetSession.clear(); } /** Loading
packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +19 −5 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.Constraints; import com.android.systemui.R; import com.android.systemui.touch.TouchInsetManager; import java.util.ArrayList; import java.util.Collections; Loading @@ -52,6 +53,7 @@ public class ComplicationLayoutEngine { private static class ViewEntry implements Comparable<ViewEntry> { private final View mView; private final ComplicationLayoutParams mLayoutParams; private final TouchInsetManager.TouchInsetSession mTouchInsetSession; private final Parent mParent; @Complication.Category private final int mCategory; Loading @@ -61,7 +63,8 @@ public class ComplicationLayoutEngine { * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding * view hierarchy to be accessed without traversing the entire view tree. */ ViewEntry(View view, ComplicationLayoutParams layoutParams, int category, Parent parent, ViewEntry(View view, ComplicationLayoutParams layoutParams, TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent, int margin) { mView = view; // Views that are generated programmatically do not have a unique id assigned to them Loading @@ -70,9 +73,12 @@ public class ComplicationLayoutEngine { // {@link Complication.ViewHolder} should not reference the root container by id. mView.setId(View.generateViewId()); mLayoutParams = layoutParams; mTouchInsetSession = touchSession; mCategory = category; mParent = parent; mMargin = margin; touchSession.addViewToTracking(mView); } /** Loading Loading @@ -217,6 +223,7 @@ public class ComplicationLayoutEngine { mParent.removeEntry(this); ((ViewGroup) mView.getParent()).removeView(mView); mTouchInsetSession.removeViewFromTracking(mView); } @Override Loading @@ -242,15 +249,18 @@ public class ComplicationLayoutEngine { */ private static class Builder { private final View mView; private final TouchInsetManager.TouchInsetSession mTouchSession; private final ComplicationLayoutParams mLayoutParams; private final int mCategory; private Parent mParent; private int mMargin; Builder(View view, ComplicationLayoutParams lp, @Complication.Category int category) { Builder(View view, TouchInsetManager.TouchInsetSession touchSession, ComplicationLayoutParams lp, @Complication.Category int category) { mView = view; mLayoutParams = lp; mCategory = category; mTouchSession = touchSession; } /** Loading Loading @@ -291,7 +301,8 @@ public class ComplicationLayoutEngine { * Builds and returns the resulting {@link ViewEntry}. */ ViewEntry build() { return new ViewEntry(mView, mLayoutParams, mCategory, mParent, mMargin); return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent, mMargin); } } Loading Loading @@ -442,13 +453,16 @@ public class ComplicationLayoutEngine { private final int mMargin; private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>(); private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>(); private final TouchInsetManager.TouchInsetSession mSession; /** */ @Inject public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, @Named(COMPLICATION_MARGIN) int margin) { @Named(COMPLICATION_MARGIN) int margin, TouchInsetManager.TouchInsetSession session) { mLayout = layout; mMargin = margin; mSession = session; } /** Loading @@ -468,7 +482,7 @@ public class ComplicationLayoutEngine { removeComplication(id); } final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, lp, category) final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category) .setMargin(mMargin); // Add position group if doesn't already exist Loading
packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +18 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,9 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayContainerView; import com.android.systemui.dreams.DreamOverlayStatusBarView; import com.android.systemui.touch.TouchInsetManager; import java.util.concurrent.Executor; import javax.inject.Named; Loading Loading @@ -63,6 +66,21 @@ public abstract class DreamOverlayModule { "R.id.dream_overlay_content must not be null"); } /** */ @Provides public static TouchInsetManager.TouchInsetSession providesTouchInsetSession( TouchInsetManager manager) { return manager.createSession(); } /** */ @Provides @DreamOverlayComponent.DreamOverlayScope public static TouchInsetManager providesTouchInsetManager(@Main Executor executor, DreamOverlayContainerView view) { return new TouchInsetManager(executor, view); } /** */ @Provides @DreamOverlayComponent.DreamOverlayScope Loading
packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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.android.systemui.touch; import android.graphics.Rect; import android.graphics.Region; import android.view.View; import android.view.ViewRootImpl; import java.util.HashMap; import java.util.HashSet; import java.util.concurrent.Executor; /** * {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This * is useful for passing through touch events for all but select areas. */ public class TouchInsetManager { /** * {@link TouchInsetSession} provides an individualized session with the * {@link TouchInsetManager}, linking any action to the client. */ public static class TouchInsetSession { private final TouchInsetManager mManager; private final HashSet<View> mTrackedViews; private final Executor mExecutor; private final View.OnLayoutChangeListener mOnLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateTouchRegion(); /** * Default constructor * @param manager The parent {@link TouchInsetManager} which will be affected by actions on * this session. * @param rootView The parent of views that will be tracked. * @param executor An executor for marshalling operations. */ TouchInsetSession(TouchInsetManager manager, Executor executor) { mManager = manager; mTrackedViews = new HashSet<>(); mExecutor = executor; } /** * Adds a descendant of the root view to be tracked. * @param view {@link View} to be tracked. */ public void addViewToTracking(View view) { mExecutor.execute(() -> { mTrackedViews.add(view); view.addOnLayoutChangeListener(mOnLayoutChangeListener); updateTouchRegion(); }); } /** * Removes a view from further tracking * @param view {@link View} to be removed. */ public void removeViewFromTracking(View view) { mExecutor.execute(() -> { mTrackedViews.remove(view); view.removeOnLayoutChangeListener(mOnLayoutChangeListener); updateTouchRegion(); }); } private void updateTouchRegion() { final Region cumulativeRegion = Region.obtain(); mTrackedViews.stream().forEach(view -> { final Rect boundaries = new Rect(); view.getBoundsOnScreen(boundaries); cumulativeRegion.op(boundaries, Region.Op.UNION); }); mManager.setTouchRegion(this, cumulativeRegion); cumulativeRegion.recycle(); } /** * Removes all tracked views and updates insets accordingly. */ public void clear() { mExecutor.execute(() -> { mManager.clearRegion(this); mTrackedViews.clear(); }); } } private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>(); private final Executor mExecutor; private final View mRootView; private final View.OnAttachStateChangeListener mAttachListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { updateTouchInset(); } @Override public void onViewDetachedFromWindow(View v) { } }; /** * Default constructor. * @param executor An {@link Executor} to marshal all operations on. * @param rootView The root {@link View} for all views in sessions. */ public TouchInsetManager(Executor executor, View rootView) { mExecutor = executor; mRootView = rootView; mRootView.addOnAttachStateChangeListener(mAttachListener); } /** * Creates a new associated session. */ public TouchInsetSession createSession() { return new TouchInsetSession(this, mExecutor); } private void updateTouchInset() { final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl(); if (viewRootImpl == null) { return; } final Region aggregateRegion = Region.obtain(); for (Region region : mDefinedRegions.values()) { aggregateRegion.op(region, Region.Op.UNION); } viewRootImpl.setTouchableRegion(aggregateRegion); aggregateRegion.recycle(); } protected void setTouchRegion(TouchInsetSession session, Region region) { final Region introducedRegion = Region.obtain(region); mExecutor.execute(() -> { mDefinedRegions.put(session, introducedRegion); updateTouchInset(); }); } private void clearRegion(TouchInsetSession session) { mExecutor.execute(() -> { final Region storedRegion = mDefinedRegions.remove(session); if (storedRegion != null) { storedRegion.recycle(); } updateTouchInset(); }); } }