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