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