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