• 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.items;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Rect;
22 import android.graphics.drawable.ColorDrawable;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.GradientDrawable;
25 import android.graphics.drawable.LayerDrawable;
26 import android.os.Build;
27 import android.os.Build.VERSION_CODES;
28 import androidx.recyclerview.widget.RecyclerView;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewOutlineProvider;
34 import androidx.annotation.VisibleForTesting;
35 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
36 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
37 import com.google.android.setupdesign.R;
38 import com.google.android.setupdesign.util.ItemStyler;
39 import com.google.android.setupdesign.view.HeaderRecyclerView;
40 
41 /**
42  * An adapter used with RecyclerView to display an {@link ItemHierarchy}. The item hierarchy used to
43  * create this adapter can be inflated by {@link com.google.android.setupdesign.items.ItemInflater}
44  * from XML.
45  */
46 public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder>
47     implements ItemHierarchy.Observer {
48 
49   private static final String TAG = "RecyclerItemAdapter";
50 
51   /**
52    * A view tag set by {@link View#setTag(Object)}. If set on the root view of a layout, it will not
53    * create the default background for the list item. This means the item will not have ripple touch
54    * feedback by default.
55    */
56   public static final String TAG_NO_BACKGROUND = "noBackground";
57 
58   /** Listener for item selection in this adapter. */
59   public interface OnItemSelectedListener {
60 
61     /**
62      * Called when an item in this adapter is clicked.
63      *
64      * @param item The Item corresponding to the position being clicked.
65      */
onItemSelected(IItem item)66     void onItemSelected(IItem item);
67   }
68 
69   private final ItemHierarchy itemHierarchy;
70   @VisibleForTesting public final boolean applyPartnerHeavyThemeResource;
71   @VisibleForTesting public final boolean useFullDynamicColor;
72   private OnItemSelectedListener listener;
73   private RecyclerView recyclerView = null;
74 
RecyclerItemAdapter(ItemHierarchy hierarchy)75   public RecyclerItemAdapter(ItemHierarchy hierarchy) {
76     this(hierarchy, false);
77   }
78 
RecyclerItemAdapter(ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource)79   public RecyclerItemAdapter(ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource) {
80     this(hierarchy, applyPartnerHeavyThemeResource, /* useFullDynamicColor= */ false);
81   }
82 
RecyclerItemAdapter( ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource, boolean useFullDynamicColor)83   public RecyclerItemAdapter(
84       ItemHierarchy hierarchy,
85       boolean applyPartnerHeavyThemeResource,
86       boolean useFullDynamicColor) {
87     this.applyPartnerHeavyThemeResource = applyPartnerHeavyThemeResource;
88     this.useFullDynamicColor = useFullDynamicColor;
89     itemHierarchy = hierarchy;
90     itemHierarchy.registerObserver(this);
91   }
92 
setRecyclerView(RecyclerView recyclerView)93   public void setRecyclerView(RecyclerView recyclerView) {
94     this.recyclerView = recyclerView;
95   }
96 
97   /**
98    * Gets the item at the given position.
99    *
100    * @see ItemHierarchy#getItemAt(int)
101    */
getItem(int position)102   public IItem getItem(int position) {
103     return itemHierarchy.getItemAt(position);
104   }
105 
106   @Override
getItemId(int position)107   public long getItemId(int position) {
108     IItem mItem = getItem(position);
109     if (mItem instanceof AbstractItem) {
110       final int id = ((AbstractItem) mItem).getId();
111       return id > 0 ? id : RecyclerView.NO_ID;
112     } else {
113       return RecyclerView.NO_ID;
114     }
115   }
116 
117   @Override
getItemCount()118   public int getItemCount() {
119     return itemHierarchy.getCount();
120   }
121 
122   @Override
onCreateViewHolder(ViewGroup parent, int viewType)123   public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
124     final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
125     final View view = inflater.inflate(viewType, parent, false);
126     final ItemViewHolder viewHolder = new ItemViewHolder(view);
127     Drawable background = null;
128     final Object viewTag = view.getTag();
129     if (!TAG_NO_BACKGROUND.equals(viewTag)) {
130       final TypedArray typedArray =
131           parent.getContext().obtainStyledAttributes(R.styleable.SudRecyclerItemAdapter);
132       Drawable selectableItemBackground =
133           typedArray.getDrawable(
134               R.styleable.SudRecyclerItemAdapter_android_selectableItemBackground);
135       if (selectableItemBackground == null) {
136         selectableItemBackground =
137             typedArray.getDrawable(R.styleable.SudRecyclerItemAdapter_selectableItemBackground);
138       } else {
139         background = view.getBackground();
140         if (background == null) {
141           // If full dynamic color enabled which means this activity is running outside of setup
142           // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3.
143           if (applyPartnerHeavyThemeResource && !useFullDynamicColor) {
144             int color =
145                 PartnerConfigHelper.get(view.getContext())
146                     .getColor(view.getContext(), PartnerConfig.CONFIG_LAYOUT_BACKGROUND_COLOR);
147             background = new ColorDrawable(color);
148           } else {
149             background =
150                 typedArray.getDrawable(R.styleable.SudRecyclerItemAdapter_android_colorBackground);
151           }
152         }
153       }
154       if (selectableItemBackground == null || background == null) {
155         Log.e(
156             TAG,
157             "Cannot resolve required attributes."
158                 + " selectableItemBackground="
159                 + selectableItemBackground
160                 + " background="
161                 + background);
162       } else {
163         final Drawable[] layers = {background, selectableItemBackground};
164         view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
165       }
166 
167       typedArray.recycle();
168     }
169 
170     view.setOnClickListener(
171         new View.OnClickListener() {
172           @Override
173           public void onClick(View view) {
174             final IItem item = viewHolder.getItem();
175             if (listener != null && item != null && item.isEnabled()) {
176               listener.onItemSelected(item);
177             }
178           }
179         });
180 
181     return viewHolder;
182   }
183 
getFirstBackground(Context context, int position)184   private Drawable getFirstBackground(Context context, int position) {
185     IItem item = getItem(position);
186     TypedArray a = context
187         .getTheme()
188         .obtainStyledAttributes(
189             item.isActionable()
190                 ? new int[] {R.attr.sudItemBackgroundFirst}
191                 : new int[] {R.attr.sudNonActionableItemBackgroundFirst});
192     Drawable firstBackground = a.getDrawable(0);
193     a.recycle();
194     return firstBackground;
195   }
196 
getLastBackground(Context context, int position)197   private Drawable getLastBackground(Context context, int position) {
198     IItem item = getItem(position);
199     TypedArray a = context
200         .getTheme()
201         .obtainStyledAttributes(
202             item.isActionable()
203                 ? new int[] {R.attr.sudItemBackgroundLast}
204                 : new int[] {R.attr.sudNonActionableItemBackgroundLast});
205     Drawable lastBackground = a.getDrawable(0);
206     a.recycle();
207     return lastBackground;
208   }
209 
getMiddleBackground(Context context, int position)210   private Drawable getMiddleBackground(Context context, int position) {
211     IItem item = getItem(position);
212     TypedArray a = context
213         .getTheme()
214         .obtainStyledAttributes(
215             item.isActionable()
216                 ? new int[] {R.attr.sudItemBackground}
217                 : new int[] {R.attr.sudNonActionableItemBackground});
218     Drawable middleBackground = a.getDrawable(0);
219     a.recycle();
220     return middleBackground;
221   }
222 
getSingleBackground(Context context, int position)223   private Drawable getSingleBackground(Context context, int position) {
224     IItem item = getItem(position);
225     TypedArray a = context
226         .getTheme()
227         .obtainStyledAttributes(
228             item.isActionable()
229                 ? new int[] {R.attr.sudItemBackgroundSingle}
230                 : new int[] {R.attr.sudNonActionableItemBackgroundSingle});
231     Drawable singleBackground = a.getDrawable(0);
232     a.recycle();
233     return singleBackground;
234   }
235 
getCornerRadius(Context context)236   private float getCornerRadius(Context context) {
237     TypedArray a =
238         context.getTheme().obtainStyledAttributes(new int[] {R.attr.sudItemCornerRadius});
239     float conerRadius = a.getDimension(0, 0);
240     a.recycle();
241     return conerRadius;
242   }
243 
isFirstItemOfGroup(int position)244   private boolean isFirstItemOfGroup(int position) {
245     return position == 0 || getItem(position - 1).isGroupDivider();
246   }
247 
isLastItemOfGroup(int position)248   private boolean isLastItemOfGroup(int position) {
249     return position == getItemCount() - 1 || getItem(position + 1).isGroupDivider();
250   }
251 
updateBackground(View view, int position)252   public void updateBackground(View view, int position) {
253     if (TAG_NO_BACKGROUND.equals(view.getTag())) {
254       return;
255     }
256     if (getItem(position).isGroupDivider()) {
257       return;
258     }
259     float groupCornerRadius =
260         PartnerConfigHelper.get(view.getContext())
261             .getDimension(view.getContext(), PartnerConfig.CONFIG_ITEMS_GROUP_CORNER_RADIUS);
262     float cornerRadius = getCornerRadius(view.getContext());
263     Drawable drawable = view.getBackground();
264     // TODO add test case for list item group corner partner config
265     if (drawable instanceof LayerDrawable && ((LayerDrawable) drawable).getNumberOfLayers() >= 2) {
266       Drawable clickDrawable = ((LayerDrawable) drawable).getDrawable(1);
267       Drawable backgroundDrawable = null;
268       GradientDrawable background = null;
269 
270       // TODO add test case in updateBackground for list item to get background for Item
271       if (isFirstItemOfGroup(position) && isLastItemOfGroup(position)) {
272         backgroundDrawable = getSingleBackground(view.getContext(), position);
273       } else if (isFirstItemOfGroup(position)) {
274         backgroundDrawable = getFirstBackground(view.getContext(), position);
275       } else if (isLastItemOfGroup(position)) {
276         backgroundDrawable = getLastBackground(view.getContext(), position);
277       } else {
278         backgroundDrawable = getMiddleBackground(view.getContext(), position);
279       }
280 
281       if (backgroundDrawable instanceof GradientDrawable) {
282         float topCornerRadius = cornerRadius;
283         float bottomCornerRadius = cornerRadius;
284         if (isFirstItemOfGroup(position)) {
285           topCornerRadius = groupCornerRadius;
286         }
287         if (isLastItemOfGroup(position)) {
288           bottomCornerRadius = groupCornerRadius;
289         }
290         background = (GradientDrawable) backgroundDrawable;
291         background.setCornerRadii(
292             new float[] {
293               topCornerRadius,
294               topCornerRadius,
295               topCornerRadius,
296               topCornerRadius,
297               bottomCornerRadius,
298               bottomCornerRadius,
299               bottomCornerRadius,
300               bottomCornerRadius
301             });
302         final Drawable[] layers = {background, clickDrawable};
303         view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
304         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
305           view.setClipToOutline(true);
306           view.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
307         }
308       }
309     }
310   }
311 
312   @Override
onBindViewHolder(ItemViewHolder holder, int position)313   public void onBindViewHolder(ItemViewHolder holder, int position) {
314     final IItem item = getItem(position);
315     holder.setEnabled(item.isEnabled());
316     holder.setItem(item);
317     if (holder.isRecyclable() != item.isRecyclable()) {
318       holder.setIsRecyclable(item.isRecyclable());
319     }
320     // TODO  when getContext is not activity context then fallback to out suw behavior
321     if (PartnerConfigHelper.isGlifExpressiveEnabled(holder.itemView.getContext())) {
322       updateBackground(holder.itemView, position);
323       updateMargin(holder.itemView);
324     }
325     item.onBindView(holder.itemView);
326   }
327 
updateMargin(View view)328   private void updateMargin(View view) {
329     // If the item view is inside a recycler layout or list layout with attribute
330     // shouldApplyAdditionalMargin, it needs to adjust the
331     // layout margin start/end here to align other component activity margin. If it is not
332     // inside a recycler layout or list layout with attribute shouldApplyAdditionalMargin, it
333     // will be adjusted by each activity themselves.
334     if (shouldApplyAdditionalMargin()) {
335       ItemStyler.applyPartnerCustomizationLayoutMarginStyle(view);
336     } else {
337       resetMarginStartEnd(view);
338     }
339   }
340 
shouldApplyAdditionalMargin()341   private boolean shouldApplyAdditionalMargin() {
342     if (recyclerView instanceof HeaderRecyclerView headerRecyclerView) {
343       return headerRecyclerView.shouldApplyAdditionalMargin();
344     }
345     return false;
346   }
347 
resetMarginStartEnd(View itemView)348   private void resetMarginStartEnd(View itemView) {
349     ViewGroup.MarginLayoutParams layoutParams =
350         (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
351     layoutParams.setMarginStart(0);
352     layoutParams.setMarginEnd(0);
353     itemView.setLayoutParams(layoutParams);
354   }
355 
356   @Override
getItemViewType(int position)357   public int getItemViewType(int position) {
358     // Use layout resource as item view type. RecyclerView item type does not have to be
359     // contiguous.
360     IItem item = getItem(position);
361     return item.getLayoutResource();
362   }
363 
364   @Override
onChanged(ItemHierarchy hierarchy)365   public void onChanged(ItemHierarchy hierarchy) {
366     notifyDataSetChanged();
367   }
368 
369   @Override
onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount)370   public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
371     notifyItemRangeChanged(positionStart, itemCount);
372   }
373 
374   @Override
onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount)375   public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
376     notifyItemRangeInserted(positionStart, itemCount);
377   }
378 
379   @Override
onItemRangeMoved( ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount)380   public void onItemRangeMoved(
381       ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
382     // There is no notifyItemRangeMoved
383     // https://code.google.com/p/android/issues/detail?id=125984
384     if (itemCount == 1) {
385       notifyItemMoved(fromPosition, toPosition);
386     } else {
387       // If more than one, degenerate into the catch-all data set changed callback, since I'm
388       // not sure how recycler view handles multiple calls to notifyItemMoved (if the result
389       // is committed after every notification then naively calling
390       // notifyItemMoved(from + i, to + i) is wrong).
391       // Logging this in case this is a more common occurrence than expected.
392       Log.i(TAG, "onItemRangeMoved with more than one item");
393       notifyDataSetChanged();
394     }
395   }
396 
397   @Override
onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount)398   public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
399     notifyItemRangeRemoved(positionStart, itemCount);
400   }
401 
402   /**
403    * Find an item hierarchy within the root hierarchy.
404    *
405    * @see ItemHierarchy#findItemById(int)
406    */
findItemById(int id)407   public ItemHierarchy findItemById(int id) {
408     return itemHierarchy.findItemById(id);
409   }
410 
411   /** Gets the root item hierarchy in this adapter. */
getRootItemHierarchy()412   public ItemHierarchy getRootItemHierarchy() {
413     return itemHierarchy;
414   }
415 
416   /**
417    * Sets the listener to listen for when user clicks on a item.
418    *
419    * @see OnItemSelectedListener
420    */
setOnItemSelectedListener(OnItemSelectedListener listener)421   public void setOnItemSelectedListener(OnItemSelectedListener listener) {
422     this.listener = listener;
423   }
424 
425   /**
426    * Before Lollipop, LayerDrawable always return true in getPadding, even if the children layers do
427    * not have any padding. Patch the implementation so that getPadding returns false if the padding
428    * is empty.
429    *
430    * <p>When getPadding is true, the padding of the view will be replaced by the padding of the
431    * drawable when {@link View#setBackgroundDrawable(Drawable)} is called. This patched class makes
432    * sure layer drawables without padding does not clear out original padding on the view.
433    */
434   @VisibleForTesting
435   static class PatchedLayerDrawable extends LayerDrawable {
436 
437     /** {@inheritDoc} */
PatchedLayerDrawable(Drawable[] layers)438     PatchedLayerDrawable(Drawable[] layers) {
439       super(layers);
440     }
441 
442     @Override
getPadding(Rect padding)443     public boolean getPadding(Rect padding) {
444       final boolean superHasPadding = super.getPadding(padding);
445       return superHasPadding
446           && !(padding.left == 0 && padding.top == 0 && padding.right == 0 && padding.bottom == 0);
447     }
448   }
449 }
450