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