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.recyclerview.widget; 18 19 import android.annotation.SuppressLint; 20 import android.os.Bundle; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.view.accessibility.AccessibilityEvent; 24 25 import androidx.core.view.AccessibilityDelegateCompat; 26 import androidx.core.view.ViewCompat; 27 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 28 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat; 29 30 import org.jspecify.annotations.NonNull; 31 import org.jspecify.annotations.Nullable; 32 33 import java.util.Map; 34 import java.util.WeakHashMap; 35 36 /** 37 * The AccessibilityDelegate used by RecyclerView. 38 * <p> 39 * This class handles basic accessibility actions and delegates them to LayoutManager. 40 */ 41 public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat { 42 final RecyclerView mRecyclerView; 43 private final ItemDelegate mItemDelegate; 44 RecyclerViewAccessibilityDelegate(@onNull RecyclerView recyclerView)45 public RecyclerViewAccessibilityDelegate(@NonNull RecyclerView recyclerView) { 46 mRecyclerView = recyclerView; 47 AccessibilityDelegateCompat itemDelegate = getItemDelegate(); 48 if (itemDelegate != null && itemDelegate instanceof ItemDelegate) { 49 mItemDelegate = (ItemDelegate) itemDelegate; 50 } else { 51 mItemDelegate = new ItemDelegate(this); 52 } 53 } 54 shouldIgnore()55 boolean shouldIgnore() { 56 return mRecyclerView.hasPendingAdapterUpdates(); 57 } 58 59 @Override performAccessibilityAction( @uppressLint"InvalidNullabilityOverride") @onNull View host, int action, @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args )60 public boolean performAccessibilityAction( 61 @SuppressLint("InvalidNullabilityOverride") @NonNull View host, 62 int action, 63 @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args 64 ) { 65 if (super.performAccessibilityAction(host, action, args)) { 66 return true; 67 } 68 if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { 69 return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args); 70 } 71 72 return false; 73 } 74 75 @Override onInitializeAccessibilityNodeInfo( @uppressLint"InvalidNullabilityOverride") @onNull View host, @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityNodeInfoCompat info )76 public void onInitializeAccessibilityNodeInfo( 77 @SuppressLint("InvalidNullabilityOverride") @NonNull View host, 78 @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityNodeInfoCompat info 79 ) { 80 super.onInitializeAccessibilityNodeInfo(host, info); 81 if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { 82 mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info); 83 } 84 } 85 86 @Override onInitializeAccessibilityEvent( @uppressLint"InvalidNullabilityOverride") @onNull View host, @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityEvent event )87 public void onInitializeAccessibilityEvent( 88 @SuppressLint("InvalidNullabilityOverride") @NonNull View host, 89 @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityEvent event 90 ) { 91 super.onInitializeAccessibilityEvent(host, event); 92 if (host instanceof RecyclerView && !shouldIgnore()) { 93 RecyclerView rv = (RecyclerView) host; 94 if (rv.getLayoutManager() != null) { 95 rv.getLayoutManager().onInitializeAccessibilityEvent(event); 96 } 97 } 98 } 99 100 /** 101 * Gets the AccessibilityDelegate for an individual item in the RecyclerView. 102 * A basic item delegate is provided by default, but you can override this 103 * method to provide a custom per-item delegate. 104 * For now, returning an {@code AccessibilityDelegateCompat} as opposed to an 105 * {@code ItemDelegate} will prevent use of the {@code ViewCompat} accessibility API on 106 * item views. 107 */ getItemDelegate()108 public @NonNull AccessibilityDelegateCompat getItemDelegate() { 109 return mItemDelegate; 110 } 111 112 /** 113 * The default implementation of accessibility delegate for the individual items of the 114 * RecyclerView. 115 * <p> 116 * If you are overriding {@code RecyclerViewAccessibilityDelegate#getItemDelegate()} but still 117 * want to keep some default behavior, you can create an instance of this class and delegate to 118 * the parent as necessary. 119 */ 120 public static class ItemDelegate extends AccessibilityDelegateCompat { 121 final RecyclerViewAccessibilityDelegate mRecyclerViewDelegate; 122 private Map<View, AccessibilityDelegateCompat> mOriginalItemDelegates = new WeakHashMap<>(); 123 124 /** 125 * Creates an item delegate for the given {@code RecyclerViewAccessibilityDelegate}. 126 * 127 * @param recyclerViewDelegate The parent RecyclerView's accessibility delegate. 128 */ ItemDelegate(@onNull RecyclerViewAccessibilityDelegate recyclerViewDelegate)129 public ItemDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate) { 130 mRecyclerViewDelegate = recyclerViewDelegate; 131 } 132 133 /** 134 * Saves a reference to the original delegate of the itemView so that it's behavior can be 135 * combined with the ItemDelegate's behavior. 136 */ saveOriginalDelegate(View itemView)137 void saveOriginalDelegate(View itemView) { 138 AccessibilityDelegateCompat delegate = ViewCompat.getAccessibilityDelegate(itemView); 139 if (delegate != null && delegate != this) { 140 mOriginalItemDelegates.put(itemView, delegate); 141 } 142 } 143 144 /** 145 * @return The delegate associated with itemView before the view was bound. 146 */ getAndRemoveOriginalDelegateForItem(View itemView)147 AccessibilityDelegateCompat getAndRemoveOriginalDelegateForItem(View itemView) { 148 return mOriginalItemDelegates.remove(itemView); 149 } 150 151 @Override onInitializeAccessibilityNodeInfo( @uppressLint"InvalidNullabilityOverride") @onNull View host, @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityNodeInfoCompat info )152 public void onInitializeAccessibilityNodeInfo( 153 @SuppressLint("InvalidNullabilityOverride") @NonNull View host, 154 @SuppressLint("InvalidNullabilityOverride") 155 @NonNull AccessibilityNodeInfoCompat info 156 ) { 157 if (!mRecyclerViewDelegate.shouldIgnore() 158 && mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) { 159 mRecyclerViewDelegate.mRecyclerView.getLayoutManager() 160 .onInitializeAccessibilityNodeInfoForItem(host, info); 161 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 162 if (originalDelegate != null) { 163 originalDelegate.onInitializeAccessibilityNodeInfo(host, info); 164 } else { 165 super.onInitializeAccessibilityNodeInfo(host, info); 166 } 167 } else { 168 super.onInitializeAccessibilityNodeInfo(host, info); 169 } 170 } 171 172 @Override performAccessibilityAction( @uppressLint"InvalidNullabilityOverride") @onNull View host, int action, @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args )173 public boolean performAccessibilityAction( 174 @SuppressLint("InvalidNullabilityOverride") @NonNull View host, 175 int action, 176 @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args 177 ) { 178 if (!mRecyclerViewDelegate.shouldIgnore() 179 && mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) { 180 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 181 if (originalDelegate != null) { 182 if (originalDelegate.performAccessibilityAction(host, action, args)) { 183 return true; 184 } 185 } else if (super.performAccessibilityAction(host, action, args)) { 186 return true; 187 } 188 return mRecyclerViewDelegate.mRecyclerView.getLayoutManager() 189 .performAccessibilityActionForItem(host, action, args); 190 } else { 191 return super.performAccessibilityAction(host, action, args); 192 } 193 } 194 195 @Override sendAccessibilityEvent(@onNull View host, int eventType)196 public void sendAccessibilityEvent(@NonNull View host, int eventType) { 197 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 198 if (originalDelegate != null) { 199 originalDelegate.sendAccessibilityEvent(host, eventType); 200 } else { 201 super.sendAccessibilityEvent(host, eventType); 202 } 203 } 204 205 @Override sendAccessibilityEventUnchecked(@onNull View host, @NonNull AccessibilityEvent event)206 public void sendAccessibilityEventUnchecked(@NonNull View host, 207 @NonNull AccessibilityEvent event) { 208 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 209 if (originalDelegate != null) { 210 originalDelegate.sendAccessibilityEventUnchecked(host, event); 211 } else { 212 super.sendAccessibilityEventUnchecked(host, event); 213 } 214 } 215 216 @Override dispatchPopulateAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)217 public boolean dispatchPopulateAccessibilityEvent(@NonNull View host, 218 @NonNull AccessibilityEvent event) { 219 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 220 if (originalDelegate != null) { 221 return originalDelegate.dispatchPopulateAccessibilityEvent(host, event); 222 } else { 223 return super.dispatchPopulateAccessibilityEvent(host, event); 224 } 225 } 226 227 @Override onPopulateAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)228 public void onPopulateAccessibilityEvent(@NonNull View host, 229 @NonNull AccessibilityEvent event) { 230 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 231 if (originalDelegate != null) { 232 originalDelegate.onPopulateAccessibilityEvent(host, event); 233 } else { 234 super.onPopulateAccessibilityEvent(host, event); 235 } 236 } 237 238 @Override onInitializeAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)239 public void onInitializeAccessibilityEvent(@NonNull View host, 240 @NonNull AccessibilityEvent event) { 241 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 242 if (originalDelegate != null) { 243 originalDelegate.onInitializeAccessibilityEvent(host, event); 244 } else { 245 super.onInitializeAccessibilityEvent(host, event); 246 } 247 } 248 249 @Override onRequestSendAccessibilityEvent(@onNull ViewGroup host, @NonNull View child, @NonNull AccessibilityEvent event)250 public boolean onRequestSendAccessibilityEvent(@NonNull ViewGroup host, 251 @NonNull View child, @NonNull AccessibilityEvent event) { 252 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 253 if (originalDelegate != null) { 254 return originalDelegate.onRequestSendAccessibilityEvent(host, child, event); 255 } else { 256 return super.onRequestSendAccessibilityEvent(host, child, event); 257 } 258 } 259 260 @Override getAccessibilityNodeProvider( @onNull View host)261 public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider( 262 @NonNull View host) { 263 AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); 264 if (originalDelegate != null) { 265 return originalDelegate.getAccessibilityNodeProvider(host); 266 } else { 267 return super.getAccessibilityNodeProvider(host); 268 } 269 } 270 } 271 } 272 273