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     public interface OnItemSelectedListener {
onItemSelected(IItem item)50         void onItemSelected(IItem item);
51     }
52 
53     private final ItemHierarchy mItemHierarchy;
54     private OnItemSelectedListener mListener;
55 
RecyclerItemAdapter(ItemHierarchy hierarchy)56     public RecyclerItemAdapter(ItemHierarchy hierarchy) {
57         mItemHierarchy = hierarchy;
58         mItemHierarchy.registerObserver(this);
59     }
60 
getItem(int position)61     public IItem getItem(int position) {
62         return mItemHierarchy.getItemAt(position);
63     }
64 
65     @Override
getItemId(int position)66     public long getItemId(int position) {
67         IItem mItem = getItem(position);
68         if (mItem instanceof AbstractItem) {
69             final int id = ((AbstractItem) mItem).getId();
70             return id > 0 ? id : RecyclerView.NO_ID;
71         } else {
72             return RecyclerView.NO_ID;
73         }
74     }
75 
76     @Override
getItemCount()77     public int getItemCount() {
78         return mItemHierarchy.getCount();
79     }
80 
81     @Override
onCreateViewHolder(ViewGroup parent, int viewType)82     public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
83         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
84         final View view = inflater.inflate(viewType, parent, false);
85         final ItemViewHolder viewHolder = new ItemViewHolder(view);
86 
87         final Object viewTag = view.getTag();
88         if (!TAG_NO_BACKGROUND.equals(viewTag)) {
89             final TypedArray typedArray = parent.getContext()
90                     .obtainStyledAttributes(R.styleable.SuwRecyclerItemAdapter);
91             Drawable selectableItemBackground = typedArray.getDrawable(
92                     R.styleable.SuwRecyclerItemAdapter_android_selectableItemBackground);
93             if (selectableItemBackground == null) {
94                 selectableItemBackground = typedArray.getDrawable(
95                         R.styleable.SuwRecyclerItemAdapter_selectableItemBackground);
96             }
97 
98             final Drawable background = typedArray.getDrawable(
99                     R.styleable.SuwRecyclerItemAdapter_android_colorBackground);
100 
101             if (selectableItemBackground == null || background == null) {
102                 Log.e(TAG, "Cannot resolve required attributes."
103                         + " selectableItemBackground=" + selectableItemBackground
104                         + " background=" + background);
105             } else {
106                 final Drawable[] layers = {background, selectableItemBackground};
107                 view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
108             }
109 
110             typedArray.recycle();
111         }
112 
113         view.setOnClickListener(new View.OnClickListener() {
114             @Override
115             public void onClick(View view) {
116                 final IItem item = viewHolder.getItem();
117                 if (mListener != null && item != null && item.isEnabled()) {
118                     mListener.onItemSelected(item);
119                 }
120             }
121         });
122 
123         return viewHolder;
124     }
125 
126     @Override
onBindViewHolder(ItemViewHolder holder, int position)127     public void onBindViewHolder(ItemViewHolder holder, int position) {
128         final IItem item = getItem(position);
129         item.onBindView(holder.itemView);
130         holder.setEnabled(item.isEnabled());
131         holder.setItem(item);
132     }
133 
134     @Override
getItemViewType(int position)135     public int getItemViewType(int position) {
136         // Use layout resource as item view type. RecyclerView item type does not have to be
137         // contiguous.
138         IItem item = getItem(position);
139         return item.getLayoutResource();
140     }
141 
142     @Override
onChanged(ItemHierarchy hierarchy)143     public void onChanged(ItemHierarchy hierarchy) {
144         notifyDataSetChanged();
145     }
146 
147     @Override
onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount)148     public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
149         notifyItemRangeChanged(positionStart, itemCount);
150     }
151 
152     @Override
onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount)153     public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
154         notifyItemRangeInserted(positionStart, itemCount);
155     }
156 
157     @Override
onItemRangeMoved(ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount)158     public void onItemRangeMoved(ItemHierarchy itemHierarchy, int fromPosition, int toPosition,
159             int itemCount) {
160         // There is no notifyItemRangeMoved
161         // https://code.google.com/p/android/issues/detail?id=125984
162         if (itemCount == 1) {
163             notifyItemMoved(fromPosition, toPosition);
164         } else {
165             // If more than one, degenerate into the catch-all data set changed callback, since I'm
166             // not sure how recycler view handles multiple calls to notifyItemMoved (if the result
167             // is committed after every notification then naively calling
168             // notifyItemMoved(from + i, to + i) is wrong).
169             // Logging this in case this is a more common occurrence than expected.
170             Log.i(TAG, "onItemRangeMoved with more than one item");
171             notifyDataSetChanged();
172         }
173     }
174 
175     @Override
onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount)176     public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
177         notifyItemRangeRemoved(positionStart, itemCount);
178     }
179 
findItemById(int id)180     public ItemHierarchy findItemById(int id) {
181         return mItemHierarchy.findItemById(id);
182     }
183 
getRootItemHierarchy()184     public ItemHierarchy getRootItemHierarchy() {
185         return mItemHierarchy;
186     }
187 
setOnItemSelectedListener(OnItemSelectedListener listener)188     public void setOnItemSelectedListener(OnItemSelectedListener listener) {
189         mListener = listener;
190     }
191 
192     /**
193      * Before Lollipop, LayerDrawable always return true in getPadding, even if the children layers
194      * do not have any padding. Patch the implementation so that getPadding returns false if the
195      * padding is empty.
196      *
197      * When getPadding is true, the padding of the view will be replaced by the padding of the
198      * drawable when {@link View#setBackgroundDrawable(Drawable)} is called. This patched class
199      * makes sure layer drawables without padding does not clear out original padding on the view.
200      */
201     @VisibleForTesting
202     static class PatchedLayerDrawable extends LayerDrawable {
203 
204         /**
205          * {@inheritDoc}
206          */
PatchedLayerDrawable(Drawable[] layers)207         PatchedLayerDrawable(Drawable[] layers) {
208             super(layers);
209         }
210 
211         @Override
getPadding(Rect padding)212         public boolean getPadding(Rect padding) {
213             final boolean superHasPadding = super.getPadding(padding);
214             return superHasPadding
215                     && !(padding.left == 0
216                             && padding.top == 0
217                             && padding.right == 0
218                             && padding.bottom == 0);
219         }
220     }
221 }
222