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