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