1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.core.view; 18 19 import android.os.Build; 20 import android.view.View; 21 import android.view.ViewGroup; 22 import android.view.WindowInsets; 23 import android.view.accessibility.AccessibilityEvent; 24 25 import androidx.annotation.RequiresApi; 26 import androidx.core.R; 27 import androidx.core.view.ViewCompat.ScrollAxis; 28 29 import org.jspecify.annotations.NonNull; 30 31 /** 32 * Helper for accessing features in {@link ViewGroup}. 33 */ 34 public final class ViewGroupCompat { 35 36 /** 37 * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. 38 * Clip bounds are the raw values of {@link View#getLeft() left}, 39 * {@link View#getTop() top}, 40 * {@link View#getRight() right} and {@link View#getBottom() bottom}. 41 */ 42 public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; 43 44 /** 45 * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. 46 * Optical bounds describe where a widget appears to be. They sit inside the clip 47 * bounds which need to cover a larger area to allow other effects, 48 * such as shadows and glows, to be drawn. 49 */ 50 public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1; 51 52 private static final WindowInsets CONSUMED = WindowInsetsCompat.CONSUMED.toWindowInsets(); 53 54 static boolean sCompatInsetsDispatchInstalled = false; 55 56 /* 57 * Hide the constructor. 58 */ ViewGroupCompat()59 private ViewGroupCompat() {} 60 61 /** 62 * Called when a child has requested sending an {@link AccessibilityEvent} and 63 * gives an opportunity to its parent to augment the event. 64 * <p> 65 * If an {@link AccessibilityDelegateCompat} has been specified via calling 66 * {@link ViewCompat#setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its 67 * {@link AccessibilityDelegateCompat#onRequestSendAccessibilityEvent(ViewGroup, View, 68 * AccessibilityEvent)} is responsible for handling this call. 69 * </p> 70 * 71 * @param group The group whose method to invoke. 72 * @param child The child which requests sending the event. 73 * @param event The event to be sent. 74 * @return True if the event should be sent. 75 * 76 * @deprecated Use {@link ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)} 77 * directly. 78 */ 79 @androidx.annotation.ReplaceWith(expression = "group.onRequestSendAccessibilityEvent(child, event)") 80 @Deprecated onRequestSendAccessibilityEvent(ViewGroup group, View child, AccessibilityEvent event)81 public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, 82 AccessibilityEvent event) { 83 return group.onRequestSendAccessibilityEvent(child, event); 84 } 85 86 /** 87 * Enable or disable the splitting of MotionEvents to multiple children during touch event 88 * dispatch. This behavior is enabled by default for applications that target an 89 * SDK version of 11 (Honeycomb) or newer. On earlier platform versions this feature 90 * was not supported and this method is a no-op. 91 * 92 * <p>When this option is enabled MotionEvents may be split and dispatched to different child 93 * views depending on where each pointer initially went down. This allows for user interactions 94 * such as scrolling two panes of content independently, chording of buttons, and performing 95 * independent gestures on different pieces of content. 96 * 97 * @param group ViewGroup to modify 98 * @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple 99 * child views. <code>false</code> to only allow one child view to be the target of 100 * any MotionEvent received by this ViewGroup. 101 * 102 * @deprecated Use {@link ViewGroup#setMotionEventSplittingEnabled(boolean)} directly. 103 */ 104 @androidx.annotation.ReplaceWith(expression = "group.setMotionEventSplittingEnabled(split)") 105 @Deprecated setMotionEventSplittingEnabled(ViewGroup group, boolean split)106 public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { 107 group.setMotionEventSplittingEnabled(split); 108 } 109 110 /** 111 * Returns the basis of alignment during layout operations on this ViewGroup: 112 * either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. 113 * <p> 114 * If no layoutMode was explicitly set, either programmatically or in an XML resource, 115 * the method returns the layoutMode of the view's parent ViewGroup if such a parent exists, 116 * otherwise the method returns a default value of {@link #LAYOUT_MODE_CLIP_BOUNDS}. 117 * 118 * @return the layout mode to use during layout operations 119 * 120 * @see #setLayoutMode(ViewGroup, int) 121 * @deprecated Call {@link ViewGroup#getLayoutMode()} directly. 122 */ 123 @Deprecated 124 @androidx.annotation.ReplaceWith(expression = "group.getLayoutMode()") getLayoutMode(@onNull ViewGroup group)125 public static int getLayoutMode(@NonNull ViewGroup group) { 126 return group.getLayoutMode(); 127 } 128 129 /** 130 * Sets the basis of alignment during the layout of this ViewGroup. 131 * Valid values are either {@link #LAYOUT_MODE_CLIP_BOUNDS} or 132 * {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. 133 * 134 * @param group ViewGroup for which to set the mode. 135 * @param mode the layout mode to use during layout operations 136 * 137 * @see #getLayoutMode(ViewGroup) 138 * @deprecated Call {@link ViewGroup#setLayoutMode()} directly. 139 */ 140 @Deprecated 141 @androidx.annotation.ReplaceWith(expression = "group.setLayoutMode(mode)") setLayoutMode(@onNull ViewGroup group, int mode)142 public static void setLayoutMode(@NonNull ViewGroup group, int mode) { 143 group.setLayoutMode(mode); 144 } 145 146 /** 147 * Changes whether or not this ViewGroup should be treated as a single entity during 148 * Activity Transitions. 149 * @param group ViewGroup for which to set the mode. 150 * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit 151 * in Activity transitions. If false, the ViewGroup won't transition, 152 * only its children. If true, the entire ViewGroup will transition 153 * together. 154 */ setTransitionGroup(@onNull ViewGroup group, boolean isTransitionGroup)155 public static void setTransitionGroup(@NonNull ViewGroup group, boolean isTransitionGroup) { 156 if (Build.VERSION.SDK_INT >= 21) { 157 Api21Impl.setTransitionGroup(group, isTransitionGroup); 158 } else { 159 group.setTag(R.id.tag_transition_group, isTransitionGroup); 160 } 161 } 162 163 /** 164 * Returns true if this ViewGroup should be considered as a single entity for removal 165 * when executing an Activity transition. If this is false, child elements will move 166 * individually during the transition. 167 */ isTransitionGroup(@onNull ViewGroup group)168 public static boolean isTransitionGroup(@NonNull ViewGroup group) { 169 if (Build.VERSION.SDK_INT >= 21) { 170 return Api21Impl.isTransitionGroup(group); 171 } 172 Boolean explicit = (Boolean) group.getTag(R.id.tag_transition_group); 173 return (explicit != null && explicit) 174 || group.getBackground() != null 175 || ViewCompat.getTransitionName(group) != null; 176 } 177 178 /** 179 * Return the current axes of nested scrolling for this ViewGroup. 180 * 181 * <p>A ViewGroup returning something other than {@link ViewCompat#SCROLL_AXIS_NONE} is 182 * currently acting as a nested scrolling parent for one or more descendant views in 183 * the hierarchy.</p> 184 * 185 * @return Flags indicating the current axes of nested scrolling 186 * @see ViewCompat#SCROLL_AXIS_HORIZONTAL 187 * @see ViewCompat#SCROLL_AXIS_VERTICAL 188 * @see ViewCompat#SCROLL_AXIS_NONE 189 */ 190 @ScrollAxis 191 @SuppressWarnings("RedundantCast") // Intentionally invoking interface method. getNestedScrollAxes(@onNull ViewGroup group)192 public static int getNestedScrollAxes(@NonNull ViewGroup group) { 193 if (Build.VERSION.SDK_INT >= 21) { 194 return Api21Impl.getNestedScrollAxes(group); 195 } 196 if (group instanceof NestedScrollingParent) { 197 return ((NestedScrollingParent) group).getNestedScrollAxes(); 198 } 199 return ViewCompat.SCROLL_AXIS_NONE; 200 } 201 202 /** 203 * Installs a custom {@link View.OnApplyWindowInsetsListener} which dispatches WindowInsets to 204 * the given root and its descendants in a way compatible with Android R+ that consuming or 205 * modifying insets will only affect the descendants. 206 * <P> 207 * Note: When using this method, ensure that ViewCompat.setOnApplyWindowInsetsListener() is used 208 * instead of the platform call. 209 * 210 * @param root The root view that the custom listener is installed on. Note: the listener will 211 * consume the insets, so make sure the given root is the root view of the window, 212 * or its siblings might not be able to get insets dispatched. 213 */ installCompatInsetsDispatch(@onNull View root)214 public static void installCompatInsetsDispatch(@NonNull View root) { 215 if (Build.VERSION.SDK_INT >= 30) { 216 return; 217 } 218 final View.OnApplyWindowInsetsListener listener = (view, windowInsets) -> { 219 dispatchApplyWindowInsets(view, windowInsets); 220 221 // The insets have been dispatched to descendants of the given view. Here returns the 222 // consumed insets to prevent redundant dispatching by the framework. 223 return CONSUMED; 224 }; 225 root.setTag(R.id.tag_compat_insets_dispatch, listener); 226 root.setOnApplyWindowInsetsListener(listener); 227 sCompatInsetsDispatchInstalled = true; 228 } 229 230 @NonNull dispatchApplyWindowInsets(View view, WindowInsets windowInsets)231 static WindowInsets dispatchApplyWindowInsets(View view, WindowInsets windowInsets) { 232 final Object wrappedUserListener = view.getTag(R.id.tag_on_apply_window_listener); 233 final Object animCallback = view.getTag(R.id.tag_window_insets_animation_callback); 234 final View.OnApplyWindowInsetsListener listener = 235 (wrappedUserListener instanceof View.OnApplyWindowInsetsListener) 236 ? (View.OnApplyWindowInsetsListener) wrappedUserListener 237 : (animCallback instanceof View.OnApplyWindowInsetsListener) 238 ? (View.OnApplyWindowInsetsListener) animCallback 239 : null; 240 241 // Don't call View#onApplyWindowInsets directly, but via View#dispatchApplyWindowInsets. 242 // Otherwise, the view won't get PFLAG3_APPLYING_INSETS and it will dispatch insets on its 243 // own. 244 final WindowInsets[] outInsets = {CONSUMED}; 245 view.setOnApplyWindowInsetsListener((v, w) -> { 246 outInsets[0] = listener != null 247 ? listener.onApplyWindowInsets(v, w) 248 : v.onApplyWindowInsets(w); 249 250 // Only apply window insets to this view. 251 return CONSUMED; 252 }); 253 // If our OnApplyWindowInsetsListener doesn't get called, it means the view has its own 254 // dispatching logic, the outInsets will remain CONSUMED, and we don't have to dispatch 255 // insets to its child views. 256 view.dispatchApplyWindowInsets(windowInsets); 257 258 // Restore the listener. 259 final Object compatInsetsDispatch = view.getTag(R.id.tag_compat_insets_dispatch); 260 view.setOnApplyWindowInsetsListener( 261 compatInsetsDispatch instanceof View.OnApplyWindowInsetsListener 262 ? (View.OnApplyWindowInsetsListener) compatInsetsDispatch 263 : listener); 264 265 if (outInsets[0] != null && !outInsets[0].isConsumed() && view instanceof ViewGroup) { 266 final ViewGroup parent = (ViewGroup) view; 267 final int count = parent.getChildCount(); 268 for (int i = 0; i < count; i++) { 269 dispatchApplyWindowInsets(parent.getChildAt(i), outInsets[0]); 270 } 271 } 272 return outInsets[0] != null ? outInsets[0] : CONSUMED; 273 } 274 275 @RequiresApi(21) 276 static class Api21Impl { Api21Impl()277 private Api21Impl() { 278 // This class is not instantiable. 279 } 280 setTransitionGroup(ViewGroup viewGroup, boolean isTransitionGroup)281 static void setTransitionGroup(ViewGroup viewGroup, boolean isTransitionGroup) { 282 viewGroup.setTransitionGroup(isTransitionGroup); 283 } 284 isTransitionGroup(ViewGroup viewGroup)285 static boolean isTransitionGroup(ViewGroup viewGroup) { 286 return viewGroup.isTransitionGroup(); 287 } 288 getNestedScrollAxes(ViewGroup viewGroup)289 static int getNestedScrollAxes(ViewGroup viewGroup) { 290 return viewGroup.getNestedScrollAxes(); 291 } 292 } 293 } 294