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