• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 com.google.android.setupdesign.view;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Build;
22 import androidx.recyclerview.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.view.KeyEvent;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.widget.FrameLayout;
30 import com.google.android.setupdesign.DividerItemDecoration;
31 import com.google.android.setupdesign.R;
32 
33 /**
34  * A RecyclerView that can display a header item at the start of the list. The header can be set by
35  * {@code app:sudHeader} in XML. Note that the header will not be inflated until a layout manager is
36  * set.
37  */
38 public class HeaderRecyclerView extends RecyclerView {
39 
40   private static class HeaderViewHolder extends ViewHolder
41       implements DividerItemDecoration.DividedViewHolder {
42 
HeaderViewHolder(View itemView)43     HeaderViewHolder(View itemView) {
44       super(itemView);
45     }
46 
47     @Override
isDividerAllowedAbove()48     public boolean isDividerAllowedAbove() {
49       return false;
50     }
51 
52     @Override
isDividerAllowedBelow()53     public boolean isDividerAllowedBelow() {
54       return false;
55     }
56   }
57 
58   /**
59    * An adapter that can optionally add one header item to the RecyclerView.
60    *
61    * @param <CVH> Type of the content view holder. i.e. view holder type of the wrapped adapter.
62    */
63   public static class HeaderAdapter<CVH extends ViewHolder>
64       extends RecyclerView.Adapter<ViewHolder> {
65 
66     private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
67 
68     private final RecyclerView.Adapter<CVH> adapter;
69     private View header;
70 
71     private final AdapterDataObserver observer =
72         new AdapterDataObserver() {
73 
74           @Override
75           public void onChanged() {
76             notifyDataSetChanged();
77           }
78 
79           @Override
80           public void onItemRangeChanged(int positionStart, int itemCount) {
81             if (header != null) {
82               positionStart++;
83             }
84             notifyItemRangeChanged(positionStart, itemCount);
85           }
86 
87           @Override
88           public void onItemRangeInserted(int positionStart, int itemCount) {
89             if (header != null) {
90               positionStart++;
91             }
92             notifyItemRangeInserted(positionStart, itemCount);
93           }
94 
95           @Override
96           public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
97             if (header != null) {
98               fromPosition++;
99               toPosition++;
100             }
101             // Why is there no notifyItemRangeMoved?
102             for (int i = 0; i < itemCount; i++) {
103               notifyItemMoved(fromPosition + i, toPosition + i);
104             }
105           }
106 
107           @Override
108           public void onItemRangeRemoved(int positionStart, int itemCount) {
109             if (header != null) {
110               positionStart++;
111             }
112             notifyItemRangeRemoved(positionStart, itemCount);
113           }
114         };
115 
HeaderAdapter(RecyclerView.Adapter<CVH> adapter)116     public HeaderAdapter(RecyclerView.Adapter<CVH> adapter) {
117       this.adapter = adapter;
118       this.adapter.registerAdapterDataObserver(observer);
119       setHasStableIds(this.adapter.hasStableIds());
120     }
121 
122     @Override
onCreateViewHolder(ViewGroup parent, int viewType)123     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
124       // Returning the same view (header) results in crash ".. but view is not a real child."
125       // The framework creates more than one instance of header because of "disappear"
126       // animations applied on the header and this necessitates creation of another header
127       // view to use after the animation. We work around this restriction by returning an
128       // empty FrameLayout to which the header is attached using #onBindViewHolder method.
129       if (viewType == HEADER_VIEW_TYPE) {
130         FrameLayout frameLayout = new FrameLayout(parent.getContext());
131         FrameLayout.LayoutParams params =
132             new FrameLayout.LayoutParams(
133                 FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
134         frameLayout.setLayoutParams(params);
135         return new HeaderViewHolder(frameLayout);
136       } else {
137         return adapter.onCreateViewHolder(parent, viewType);
138       }
139     }
140 
141     @Override
142     @SuppressWarnings("unchecked") // Non-header position always return type CVH
onBindViewHolder(ViewHolder holder, int position)143     public void onBindViewHolder(ViewHolder holder, int position) {
144       if (header != null) {
145         position--;
146       }
147 
148       if (holder instanceof HeaderViewHolder) {
149         if (header == null) {
150           throw new IllegalStateException("HeaderViewHolder cannot find mHeader");
151         }
152         if (header.getParent() != null) {
153           ((ViewGroup) header.getParent()).removeView(header);
154         }
155         FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
156         mHeaderParent.addView(header);
157       } else {
158         adapter.onBindViewHolder((CVH) holder, position);
159       }
160     }
161 
162     @Override
getItemViewType(int position)163     public int getItemViewType(int position) {
164       if (header != null) {
165         position--;
166       }
167       if (position < 0) {
168         return HEADER_VIEW_TYPE;
169       }
170       return adapter.getItemViewType(position);
171     }
172 
173     @Override
getItemCount()174     public int getItemCount() {
175       int count = adapter.getItemCount();
176       if (header != null) {
177         count++;
178       }
179       return count;
180     }
181 
182     @Override
getItemId(int position)183     public long getItemId(int position) {
184       if (header != null) {
185         position--;
186       }
187       if (position < 0) {
188         return Long.MAX_VALUE;
189       }
190       return adapter.getItemId(position);
191     }
192 
setHeader(View header)193     public void setHeader(View header) {
194       this.header = header;
195     }
196 
getWrappedAdapter()197     public RecyclerView.Adapter<CVH> getWrappedAdapter() {
198       return adapter;
199     }
200   }
201 
202   private View header;
203   private boolean shouldApplyAdditionalMargin;
204   private int headerRes;
205 
HeaderRecyclerView(Context context)206   public HeaderRecyclerView(Context context) {
207     super(context);
208     init(null, 0);
209   }
210 
HeaderRecyclerView(Context context, AttributeSet attrs)211   public HeaderRecyclerView(Context context, AttributeSet attrs) {
212     super(context, attrs);
213     init(attrs, 0);
214   }
215 
HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)216   public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
217     super(context, attrs, defStyleAttr);
218     init(attrs, defStyleAttr);
219   }
220 
init(AttributeSet attrs, int defStyleAttr)221   private void init(AttributeSet attrs, int defStyleAttr) {
222     if (isInEditMode()) {
223       return;
224     }
225 
226     final TypedArray a =
227         getContext()
228             .obtainStyledAttributes(attrs, R.styleable.SudHeaderRecyclerView, defStyleAttr, 0);
229     headerRes = a.getResourceId(R.styleable.SudHeaderRecyclerView_sudHeader, 0);
230     shouldApplyAdditionalMargin =
231         a.getBoolean(R.styleable.SudHeaderRecyclerView_sudShouldApplyAdditionalMargin, false);
232     a.recycle();
233   }
234 
shouldApplyAdditionalMargin()235   public boolean shouldApplyAdditionalMargin() {
236     return shouldApplyAdditionalMargin;
237   }
238 
239   @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)240   public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
241     super.onInitializeAccessibilityEvent(event);
242 
243     // Decoration-only headers should not count as an item for accessibility, adjust the
244     // accessibility event to account for that.
245     final int numberOfHeaders = header != null ? 1 : 0;
246     event.setItemCount(event.getItemCount() - numberOfHeaders);
247     event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
248     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
249       event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
250     }
251   }
252 
handleDpadDown()253   private boolean handleDpadDown() {
254     View focusedView = findFocus();
255     if (focusedView == null) {
256       return false;
257     }
258 
259     int[] focusdLocationInWindow = new int[2];
260     int[] myLocationInWindow = new int[2];
261 
262     focusedView.getLocationInWindow(focusdLocationInWindow);
263     getLocationInWindow(myLocationInWindow);
264 
265     int offset =
266         (focusdLocationInWindow[1] + focusedView.getMeasuredHeight())
267             - (myLocationInWindow[1] + getMeasuredHeight());
268 
269     /*
270       (focusdLocationInWindow[1] + focusedView.getMeasuredHeight())
271       is the bottom position of focused view
272 
273       (myLocationInWindow[1] + getMeasuredHeight())
274       is the bottom position of recycler view
275 
276       If the bottom of focused view is out of recycler view, means we need to scroll down to show
277       more detail
278 
279       We scroll 70% of recycler view to make sure user can have 30% of previous information, to make
280       sure user can keep reading easily.
281     */
282     if (offset > 0) {
283       // We expect only scroll 70% of recycler view
284       int scrollLength = (int) (getMeasuredHeight() * 0.7f);
285       smoothScrollBy(0, Math.min(scrollLength, offset));
286       return true;
287     }
288 
289     return false;
290   }
291 
handleDpadUp()292   private boolean handleDpadUp() {
293     View focusedView = findFocus();
294     if (focusedView == null) {
295       return false;
296     }
297 
298     int[] focusedLocationInWindow = new int[2];
299     int[] myLocationInWindow = new int[2];
300 
301     focusedView.getLocationInWindow(focusedLocationInWindow);
302     getLocationInWindow(myLocationInWindow);
303 
304     int offset = (focusedLocationInWindow[1] - myLocationInWindow[1]);
305 
306     /*
307       focusedLocationInWindow[1] is top of focused view
308       myLocationInWindow[1] is top of recycler view
309 
310       If top of focused view is higher than recycler view we need scroll up to show more information
311       we try to scroll up 70% of recycler view ot scroll up to the top of focused view
312     */
313     if (offset < 0) {
314       // We expect only scroll 70% of recycler view
315       int scrollLength = (int) (getMeasuredHeight() * -0.7f);
316 
317       smoothScrollBy(0, Math.max(scrollLength, offset));
318       return true;
319     }
320     return false;
321   }
322 
323   private boolean shouldHandleActionUp = false;
324 
handleKeyEvent(KeyEvent keyEvent)325   private boolean handleKeyEvent(KeyEvent keyEvent) {
326     if (shouldHandleActionUp && keyEvent.getAction() == KeyEvent.ACTION_UP) {
327       shouldHandleActionUp = false;
328       return true;
329     } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
330       boolean eventHandled = false;
331       switch (keyEvent.getKeyCode()) {
332         case KeyEvent.KEYCODE_DPAD_DOWN:
333           eventHandled = handleDpadDown();
334           break;
335         case KeyEvent.KEYCODE_DPAD_UP:
336           eventHandled = handleDpadUp();
337           break;
338         default: // fall out
339       }
340       shouldHandleActionUp = eventHandled;
341       return eventHandled;
342     }
343     return false;
344   }
345 
346   @Override
dispatchKeyEvent(KeyEvent event)347   public boolean dispatchKeyEvent(KeyEvent event) {
348     if (handleKeyEvent(event)) {
349       return true;
350     }
351     return super.dispatchKeyEvent(event);
352   }
353 
354   /** Gets the header view of this RecyclerView, or {@code null} if there are no headers. */
getHeader()355   public View getHeader() {
356     return header;
357   }
358 
359   /**
360    * Set the view to use as the header of this recycler view. Note: This must be called before
361    * setAdapter.
362    */
setHeader(View header)363   public void setHeader(View header) {
364     this.header = header;
365   }
366 
367   @Override
setLayoutManager(LayoutManager layout)368   public void setLayoutManager(LayoutManager layout) {
369     super.setLayoutManager(layout);
370     if (layout != null && header == null && headerRes != 0) {
371       // Inflating a child view requires the layout manager to be set. Check here to see if
372       // any header item is specified in XML and inflate them.
373       final LayoutInflater inflater = LayoutInflater.from(getContext());
374       header = inflater.inflate(headerRes, this, false);
375     }
376   }
377 
378   @Override
379   @SuppressWarnings("rawtypes,unchecked") // RecyclerView.setAdapter uses raw type :(
setAdapter(Adapter adapter)380   public void setAdapter(Adapter adapter) {
381     if (header != null && adapter != null) {
382       final HeaderAdapter headerAdapter = new HeaderAdapter(adapter);
383       headerAdapter.setHeader(header);
384       adapter = headerAdapter;
385     }
386     super.setAdapter(adapter);
387   }
388 }
389