1 /* 2 * Copyright (C) 2017 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.template; 18 19 import android.content.Context; 20 import android.content.res.Resources.Theme; 21 import android.content.res.TypedArray; 22 import android.graphics.drawable.Drawable; 23 import android.os.Build; 24 import android.os.Build.VERSION_CODES; 25 import androidx.recyclerview.widget.LinearLayoutManager; 26 import androidx.recyclerview.widget.RecyclerView; 27 import androidx.recyclerview.widget.RecyclerView.Adapter; 28 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 29 import android.util.AttributeSet; 30 import android.util.TypedValue; 31 import android.view.View; 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import com.google.android.setupcompat.internal.TemplateLayout; 35 import com.google.android.setupcompat.partnerconfig.PartnerConfig; 36 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; 37 import com.google.android.setupcompat.template.Mixin; 38 import com.google.android.setupdesign.DividerItemDecoration; 39 import com.google.android.setupdesign.GlifLayout; 40 import com.google.android.setupdesign.R; 41 import com.google.android.setupdesign.items.ItemHierarchy; 42 import com.google.android.setupdesign.items.ItemInflater; 43 import com.google.android.setupdesign.items.RecyclerItemAdapter; 44 import com.google.android.setupdesign.util.DrawableLayoutDirectionHelper; 45 import com.google.android.setupdesign.util.PartnerStyleHelper; 46 import com.google.android.setupdesign.view.HeaderRecyclerView; 47 import com.google.android.setupdesign.view.HeaderRecyclerView.HeaderAdapter; 48 49 /** 50 * A {@link Mixin} for interacting with templates with recycler views. This mixin constructor takes 51 * the instance of the recycler view to allow it to be instantiated dynamically, as in the case for 52 * preference fragments. 53 * 54 * <p>Unlike typical mixins, this mixin is designed to be created in onTemplateInflated, which is 55 * called by the super constructor, and then parse the XML attributes later in the constructor. 56 */ 57 public class RecyclerMixin implements Mixin { 58 59 private final TemplateLayout templateLayout; 60 61 @NonNull private final RecyclerView recyclerView; 62 63 @Nullable private View header; 64 65 @NonNull private DividerItemDecoration dividerDecoration; 66 67 private Drawable defaultDivider; 68 private Drawable divider; 69 70 private int dividerInsetStart; 71 private int dividerInsetEnd; 72 private boolean isDividerDisplay = true; 73 74 /** 75 * Creates the RecyclerMixin. Unlike typical mixins which are created in the constructor, this 76 * mixin should be called in {@link TemplateLayout#onTemplateInflated()}, which is called by the 77 * super constructor, because the recycler view and the header needs to be made available before 78 * other mixins from the super class. 79 * 80 * @param layout The layout this mixin belongs to. 81 */ RecyclerMixin(@onNull TemplateLayout layout, @NonNull RecyclerView recyclerView)82 public RecyclerMixin(@NonNull TemplateLayout layout, @NonNull RecyclerView recyclerView) { 83 templateLayout = layout; 84 85 dividerDecoration = new DividerItemDecoration(templateLayout.getContext()); 86 87 // The recycler view needs to be available 88 this.recyclerView = recyclerView; 89 this.recyclerView.setLayoutManager(new LinearLayoutManager(templateLayout.getContext())); 90 91 if (recyclerView instanceof HeaderRecyclerView) { 92 header = ((HeaderRecyclerView) recyclerView).getHeader(); 93 } 94 95 isDividerDisplay = isShowItemsDivider(layout.getContext()); 96 if (isDividerDisplay) { 97 this.recyclerView.addItemDecoration(dividerDecoration); 98 } 99 } 100 isShowItemsDivider(Context context)101 private boolean isShowItemsDivider(Context context) { 102 // Get the dividershown attribute value from theme 103 TypedValue typedValue = new TypedValue(); 104 Theme theme = context.getTheme(); 105 theme.resolveAttribute(R.attr.sudDividerShown, typedValue, true); 106 boolean isShownDivider = (typedValue.data != 0); 107 108 // Skips to add item decoration if config flag is false. 109 if (PartnerStyleHelper.shouldApplyPartnerResource(templateLayout)) { 110 if (PartnerConfigHelper.get(recyclerView.getContext()) 111 .isPartnerConfigAvailable(PartnerConfig.CONFIG_ITEMS_DIVIDER_SHOWN)) { 112 return PartnerConfigHelper.get(recyclerView.getContext()) 113 .getBoolean( 114 recyclerView.getContext(), 115 PartnerConfig.CONFIG_ITEMS_DIVIDER_SHOWN, 116 isShownDivider); 117 } 118 } 119 return isShownDivider; 120 } 121 122 /** 123 * Parse XML attributes and configures this mixin and the recycler view accordingly. This should 124 * be called from the constructor of the layout. 125 * 126 * @param attrs The {@link AttributeSet} as passed into the constructor. Can be null if the layout 127 * was not created from XML. 128 * @param defStyleAttr The default style attribute as passed into the layout constructor. Can be 0 129 * if it is not needed. 130 */ parseAttributes(@ullable AttributeSet attrs, int defStyleAttr)131 public void parseAttributes(@Nullable AttributeSet attrs, int defStyleAttr) { 132 final Context context = templateLayout.getContext(); 133 final TypedArray a = 134 context.obtainStyledAttributes(attrs, R.styleable.SudRecyclerMixin, defStyleAttr, 0); 135 136 final int entries = a.getResourceId(R.styleable.SudRecyclerMixin_android_entries, 0); 137 if (entries != 0) { 138 final ItemHierarchy inflated = new ItemInflater(context).inflate(entries); 139 140 boolean applyPartnerHeavyThemeResource = false; 141 boolean useFullDynamicColor = false; 142 if (templateLayout instanceof GlifLayout) { 143 applyPartnerHeavyThemeResource = 144 ((GlifLayout) templateLayout).shouldApplyPartnerHeavyThemeResource(); 145 useFullDynamicColor = ((GlifLayout) templateLayout).useFullDynamicColor(); 146 } 147 148 final RecyclerItemAdapter adapter = 149 new RecyclerItemAdapter(inflated, applyPartnerHeavyThemeResource, useFullDynamicColor); 150 adapter.setHasStableIds(a.getBoolean(R.styleable.SudRecyclerMixin_sudHasStableIds, false)); 151 setAdapter(adapter); 152 } 153 154 if (!isDividerDisplay) { 155 a.recycle(); 156 return; 157 } 158 159 int dividerInset = a.getDimensionPixelSize(R.styleable.SudRecyclerMixin_sudDividerInset, -1); 160 if (dividerInset != -1) { 161 setDividerInset(dividerInset); 162 } else { 163 int dividerInsetStart = 164 a.getDimensionPixelSize(R.styleable.SudRecyclerMixin_sudDividerInsetStart, 0); 165 int dividerInsetEnd = 166 a.getDimensionPixelSize(R.styleable.SudRecyclerMixin_sudDividerInsetEnd, 0); 167 168 if (PartnerStyleHelper.shouldApplyPartnerResource(templateLayout)) { 169 if (PartnerConfigHelper.get(context) 170 .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_START)) { 171 dividerInsetStart = 172 (int) 173 PartnerConfigHelper.get(context) 174 .getDimension(context, PartnerConfig.CONFIG_LAYOUT_MARGIN_START); 175 } 176 if (PartnerConfigHelper.get(context) 177 .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_END)) { 178 dividerInsetEnd = 179 (int) 180 PartnerConfigHelper.get(context) 181 .getDimension(context, PartnerConfig.CONFIG_LAYOUT_MARGIN_END); 182 } 183 } 184 setDividerInsets(dividerInsetStart, dividerInsetEnd); 185 } 186 187 a.recycle(); 188 } 189 190 /** 191 * @return The recycler view contained in the layout, as marked by {@code @id/sud_recycler_view}. 192 * This will return {@code null} if the recycler view doesn't exist in the layout. 193 */ 194 @SuppressWarnings("NullableProblems") // If clients guarantee that the template has a recycler 195 // view, and call this after the template is inflated, 196 // this will not return null. getRecyclerView()197 public RecyclerView getRecyclerView() { 198 return recyclerView; 199 } 200 201 /** 202 * Gets the header view of the recycler layout. This is useful for other mixins if they need to 203 * access views within the header, usually via {@link TemplateLayout#findManagedViewById(int)}. 204 */ 205 @SuppressWarnings("NullableProblems") // If clients guarantee that the template has a header, 206 // this call will not return null. getHeader()207 public View getHeader() { 208 return header; 209 } 210 211 /** 212 * Recycler mixin needs to update the dividers if the layout direction has changed. This method 213 * should be called when {@link View#onLayout(boolean, int, int, int, int)} of the template is 214 * called. 215 */ onLayout()216 public void onLayout() { 217 if (divider == null) { 218 // Update divider in case layout direction has just been resolved 219 updateDivider(); 220 } 221 } 222 223 /** 224 * Gets the adapter of the recycler view in this layout. If the adapter includes a header, this 225 * method will unwrap it and return the underlying adapter. 226 * 227 * @return The adapter, or {@code null} if the recycler view has no adapter. 228 */ getAdapter()229 public Adapter<? extends ViewHolder> getAdapter() { 230 // RecyclerView.getAdapter returns raw type :( 231 final RecyclerView.Adapter<? extends ViewHolder> adapter = recyclerView.getAdapter(); 232 if (adapter instanceof HeaderAdapter) { 233 return ((HeaderAdapter<? extends ViewHolder>) adapter).getWrappedAdapter(); 234 } 235 return adapter; 236 } 237 238 /** Sets the adapter on the recycler view in this layout. */ setAdapter(Adapter<? extends ViewHolder> adapter)239 public void setAdapter(Adapter<? extends ViewHolder> adapter) { 240 recyclerView.setAdapter(adapter); 241 } 242 243 /** @deprecated Use {@link #setDividerInsets(int, int)} instead. */ 244 @Deprecated setDividerInset(int inset)245 public void setDividerInset(int inset) { 246 setDividerInsets(inset, 0); 247 } 248 249 /** 250 * Sets the start inset of the divider. This will use the default divider drawable set in the 251 * theme and apply insets to it. 252 * 253 * @param start The number of pixels to inset on the "start" side of the list divider. Typically 254 * this will be either {@code @dimen/sud_items_glif_icon_divider_inset} or 255 * {@code @dimen/sud_items_glif_text_divider_inset}. 256 * @param end The number of pixels to inset on the "end" side of the list divider. 257 */ setDividerInsets(int start, int end)258 public void setDividerInsets(int start, int end) { 259 dividerInsetStart = start; 260 dividerInsetEnd = end; 261 updateDivider(); 262 } 263 264 /** 265 * @return The number of pixels inset on the start side of the divider. 266 * @deprecated This is the same as {@link #getDividerInsetStart()}. Use that instead. 267 */ 268 @Deprecated getDividerInset()269 public int getDividerInset() { 270 return getDividerInsetStart(); 271 } 272 273 /** @return The number of pixels inset on the start side of the divider. */ getDividerInsetStart()274 public int getDividerInsetStart() { 275 return dividerInsetStart; 276 } 277 278 /** @return The number of pixels inset on the end side of the divider. */ getDividerInsetEnd()279 public int getDividerInsetEnd() { 280 return dividerInsetEnd; 281 } 282 283 /** Remove the divider inset from this RecyclerView. */ removeDividerInset()284 public void removeDividerInset() { 285 recyclerView.removeItemDecoration(dividerDecoration); 286 } 287 updateDivider()288 private void updateDivider() { 289 boolean shouldUpdate = true; 290 if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) { 291 shouldUpdate = templateLayout.isLayoutDirectionResolved(); 292 } 293 if (shouldUpdate) { 294 if (defaultDivider == null) { 295 defaultDivider = dividerDecoration.getDivider(); 296 } 297 divider = 298 DrawableLayoutDirectionHelper.createRelativeInsetDrawable( 299 defaultDivider, 300 dividerInsetStart /* start */, 301 0 /* top */, 302 dividerInsetEnd /* end */, 303 0 /* bottom */, 304 templateLayout); 305 dividerDecoration.setDivider(divider); 306 } 307 } 308 309 /** @return The drawable used as the divider. */ getDivider()310 public Drawable getDivider() { 311 return divider; 312 } 313 hasDivider()314 public boolean hasDivider() { 315 return isDividerDisplay; 316 } 317 318 /** 319 * Sets the divider item decoration directly. This is a low level method which should be used only 320 * if custom divider behavior is needed, for example if the divider should be shown / hidden in 321 * some specific cases for view holders that cannot implement {@link 322 * com.google.android.setupdesign.DividerItemDecoration.DividedViewHolder}. 323 */ setDividerItemDecoration(@onNull DividerItemDecoration decoration)324 public void setDividerItemDecoration(@NonNull DividerItemDecoration decoration) { 325 recyclerView.removeItemDecoration(dividerDecoration); 326 dividerDecoration = decoration; 327 recyclerView.addItemDecoration(dividerDecoration); 328 updateDivider(); 329 } 330 } 331