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