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