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