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 item.onBindView(holder.itemView); 147 holder.setEnabled(item.isEnabled()); 148 holder.setItem(item); 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