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