• 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.res.TypedArray;
20 import android.graphics.Rect;
21 import android.graphics.drawable.ColorDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.graphics.drawable.LayerDrawable;
24 import androidx.recyclerview.widget.RecyclerView;
25 import android.util.Log;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import androidx.annotation.VisibleForTesting;
30 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
31 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
32 import com.google.android.setupdesign.R;
33 
34 /**
35  * An adapter used with RecyclerView to display an {@link ItemHierarchy}. The item hierarchy used to
36  * create this adapter can be inflated by {@link com.google.android.setupdesign.items.ItemInflater}
37  * from XML.
38  */
39 public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder>
40     implements ItemHierarchy.Observer {
41 
42   private static final String TAG = "RecyclerItemAdapter";
43 
44   /**
45    * A view tag set by {@link View#setTag(Object)}. If set on the root view of a layout, it will not
46    * create the default background for the list item. This means the item will not have ripple touch
47    * feedback by default.
48    */
49   public static final String TAG_NO_BACKGROUND = "noBackground";
50 
51   /** Listener for item selection in this adapter. */
52   public interface OnItemSelectedListener {
53 
54     /**
55      * Called when an item in this adapter is clicked.
56      *
57      * @param item The Item corresponding to the position being clicked.
58      */
onItemSelected(IItem item)59     void onItemSelected(IItem item);
60   }
61 
62   private final ItemHierarchy itemHierarchy;
63   @VisibleForTesting public final boolean applyPartnerHeavyThemeResource;
64   @VisibleForTesting public final boolean useFullDynamicColor;
65   private OnItemSelectedListener listener;
66 
RecyclerItemAdapter(ItemHierarchy hierarchy)67   public RecyclerItemAdapter(ItemHierarchy hierarchy) {
68     this(hierarchy, false);
69   }
70 
RecyclerItemAdapter(ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource)71   public RecyclerItemAdapter(ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource) {
72     this(hierarchy, applyPartnerHeavyThemeResource, /* useFullDynamicColor= */ false);
73   }
74 
RecyclerItemAdapter( ItemHierarchy hierarchy, boolean applyPartnerHeavyThemeResource, boolean useFullDynamicColor)75   public RecyclerItemAdapter(
76       ItemHierarchy hierarchy,
77       boolean applyPartnerHeavyThemeResource,
78       boolean useFullDynamicColor) {
79     this.applyPartnerHeavyThemeResource = applyPartnerHeavyThemeResource;
80     this.useFullDynamicColor = useFullDynamicColor;
81     itemHierarchy = hierarchy;
82     itemHierarchy.registerObserver(this);
83   }
84 
85   /**
86    * Gets the item at the given position.
87    *
88    * @see ItemHierarchy#getItemAt(int)
89    */
getItem(int position)90   public IItem getItem(int position) {
91     return itemHierarchy.getItemAt(position);
92   }
93 
94   @Override
getItemId(int position)95   public long getItemId(int position) {
96     IItem mItem = getItem(position);
97     if (mItem instanceof AbstractItem) {
98       final int id = ((AbstractItem) mItem).getId();
99       return id > 0 ? id : RecyclerView.NO_ID;
100     } else {
101       return RecyclerView.NO_ID;
102     }
103   }
104 
105   @Override
getItemCount()106   public int getItemCount() {
107     return itemHierarchy.getCount();
108   }
109 
110   @Override
onCreateViewHolder(ViewGroup parent, int viewType)111   public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
112     final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
113     final View view = inflater.inflate(viewType, parent, false);
114     final ItemViewHolder viewHolder = new ItemViewHolder(view);
115     Drawable background = null;
116 
117     final Object viewTag = view.getTag();
118     if (!TAG_NO_BACKGROUND.equals(viewTag)) {
119       final TypedArray typedArray =
120           parent.getContext().obtainStyledAttributes(R.styleable.SudRecyclerItemAdapter);
121       Drawable selectableItemBackground =
122           typedArray.getDrawable(
123               R.styleable.SudRecyclerItemAdapter_android_selectableItemBackground);
124       if (selectableItemBackground == null) {
125         selectableItemBackground =
126             typedArray.getDrawable(R.styleable.SudRecyclerItemAdapter_selectableItemBackground);
127       } else {
128         background = view.getBackground();
129         if (background == null) {
130           // If full dynamic color enabled which means this activity is running outside of setup
131           // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3.
132           if (applyPartnerHeavyThemeResource && !useFullDynamicColor) {
133             int color =
134                 PartnerConfigHelper.get(view.getContext())
135                     .getColor(view.getContext(), PartnerConfig.CONFIG_LAYOUT_BACKGROUND_COLOR);
136             background = new ColorDrawable(color);
137           } else {
138             background =
139                 typedArray.getDrawable(R.styleable.SudRecyclerItemAdapter_android_colorBackground);
140           }
141         }
142       }
143 
144       if (selectableItemBackground == null || background == null) {
145         Log.e(
146             TAG,
147             "Cannot resolve required attributes."
148                 + " selectableItemBackground="
149                 + selectableItemBackground
150                 + " background="
151                 + background);
152       } else {
153         final Drawable[] layers = {background, selectableItemBackground};
154         view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
155       }
156 
157       typedArray.recycle();
158     }
159 
160     view.setOnClickListener(
161         new View.OnClickListener() {
162           @Override
163           public void onClick(View view) {
164             final IItem item = viewHolder.getItem();
165             if (listener != null && item != null && item.isEnabled()) {
166               listener.onItemSelected(item);
167             }
168           }
169         });
170 
171     return viewHolder;
172   }
173 
174   @Override
onBindViewHolder(ItemViewHolder holder, int position)175   public void onBindViewHolder(ItemViewHolder holder, int position) {
176     final IItem item = getItem(position);
177     holder.setEnabled(item.isEnabled());
178     holder.setItem(item);
179     item.onBindView(holder.itemView);
180   }
181 
182   @Override
getItemViewType(int position)183   public int getItemViewType(int position) {
184     // Use layout resource as item view type. RecyclerView item type does not have to be
185     // contiguous.
186     IItem item = getItem(position);
187     return item.getLayoutResource();
188   }
189 
190   @Override
onChanged(ItemHierarchy hierarchy)191   public void onChanged(ItemHierarchy hierarchy) {
192     notifyDataSetChanged();
193   }
194 
195   @Override
onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount)196   public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
197     notifyItemRangeChanged(positionStart, itemCount);
198   }
199 
200   @Override
onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount)201   public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
202     notifyItemRangeInserted(positionStart, itemCount);
203   }
204 
205   @Override
onItemRangeMoved( ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount)206   public void onItemRangeMoved(
207       ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
208     // There is no notifyItemRangeMoved
209     // https://code.google.com/p/android/issues/detail?id=125984
210     if (itemCount == 1) {
211       notifyItemMoved(fromPosition, toPosition);
212     } else {
213       // If more than one, degenerate into the catch-all data set changed callback, since I'm
214       // not sure how recycler view handles multiple calls to notifyItemMoved (if the result
215       // is committed after every notification then naively calling
216       // notifyItemMoved(from + i, to + i) is wrong).
217       // Logging this in case this is a more common occurrence than expected.
218       Log.i(TAG, "onItemRangeMoved with more than one item");
219       notifyDataSetChanged();
220     }
221   }
222 
223   @Override
onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount)224   public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
225     notifyItemRangeRemoved(positionStart, itemCount);
226   }
227 
228   /**
229    * Find an item hierarchy within the root hierarchy.
230    *
231    * @see ItemHierarchy#findItemById(int)
232    */
findItemById(int id)233   public ItemHierarchy findItemById(int id) {
234     return itemHierarchy.findItemById(id);
235   }
236 
237   /** Gets the root item hierarchy in this adapter. */
getRootItemHierarchy()238   public ItemHierarchy getRootItemHierarchy() {
239     return itemHierarchy;
240   }
241 
242   /**
243    * Sets the listener to listen for when user clicks on a item.
244    *
245    * @see OnItemSelectedListener
246    */
setOnItemSelectedListener(OnItemSelectedListener listener)247   public void setOnItemSelectedListener(OnItemSelectedListener listener) {
248     this.listener = listener;
249   }
250 
251   /**
252    * Before Lollipop, LayerDrawable always return true in getPadding, even if the children layers do
253    * not have any padding. Patch the implementation so that getPadding returns false if the padding
254    * is empty.
255    *
256    * <p>When getPadding is true, the padding of the view will be replaced by the padding of the
257    * drawable when {@link View#setBackgroundDrawable(Drawable)} is called. This patched class makes
258    * sure layer drawables without padding does not clear out original padding on the view.
259    */
260   @VisibleForTesting
261   static class PatchedLayerDrawable extends LayerDrawable {
262 
263     /** {@inheritDoc} */
PatchedLayerDrawable(Drawable[] layers)264     PatchedLayerDrawable(Drawable[] layers) {
265       super(layers);
266     }
267 
268     @Override
getPadding(Rect padding)269     public boolean getPadding(Rect padding) {
270       final boolean superHasPadding = super.getPadding(padding);
271       return superHasPadding
272           && !(padding.left == 0 && padding.top == 0 && padding.right == 0 && padding.bottom == 0);
273     }
274   }
275 }
276