• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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