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