SetupWizardLayout.java revision 3aee7b9de403e669e24ce68da1b390ab74288364
1/*
2 * Copyright (C) 2015 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;
18
19import android.annotation.SuppressLint;
20import android.annotation.TargetApi;
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.content.res.TypedArray;
24import android.graphics.Shader.TileMode;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
27import android.graphics.drawable.LayerDrawable;
28import android.os.Build.VERSION;
29import android.os.Build.VERSION_CODES;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.util.TypedValue;
35import android.view.Gravity;
36import android.view.InflateException;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.ViewStub;
41import android.widget.ProgressBar;
42import android.widget.ScrollView;
43import android.widget.TextView;
44
45import com.android.setupwizardlib.util.RequireScrollHelper;
46import com.android.setupwizardlib.view.BottomScrollView;
47import com.android.setupwizardlib.view.Illustration;
48import com.android.setupwizardlib.view.NavigationBar;
49
50public class SetupWizardLayout extends TemplateLayout {
51
52    private static final String TAG = "SetupWizardLayout";
53
54    private ColorStateList mProgressBarColor;
55
56    public SetupWizardLayout(Context context) {
57        super(context, 0, 0);
58        init(null, R.attr.suwLayoutTheme);
59    }
60
61    public SetupWizardLayout(Context context, int template) {
62        this(context, template, 0);
63    }
64
65    public SetupWizardLayout(Context context, int template, int containerId) {
66        super(context, template, containerId);
67        init(null, R.attr.suwLayoutTheme);
68    }
69
70    public SetupWizardLayout(Context context, AttributeSet attrs) {
71        super(context, attrs);
72        init(attrs, R.attr.suwLayoutTheme);
73    }
74
75    @TargetApi(VERSION_CODES.HONEYCOMB)
76    public SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr) {
77        super(context, attrs, defStyleAttr);
78        init(attrs, defStyleAttr);
79    }
80
81    // All the constructors delegate to this init method. The 3-argument constructor is not
82    // available in LinearLayout before v11, so call super with the exact same arguments.
83    private void init(AttributeSet attrs, int defStyleAttr) {
84        final TypedArray a = getContext().obtainStyledAttributes(attrs,
85                R.styleable.SuwSetupWizardLayout, defStyleAttr, 0);
86
87        // Set the background from XML, either directly or built from a bitmap tile
88        final Drawable background =
89                a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackground);
90        if (background != null) {
91            setLayoutBackground(background);
92        } else {
93            final Drawable backgroundTile =
94                    a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackgroundTile);
95            if (backgroundTile != null) {
96                setBackgroundTile(backgroundTile);
97            }
98        }
99
100        // Set the illustration from XML, either directly or built from image + horizontal tile
101        final Drawable illustration =
102                a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustration);
103        if (illustration != null) {
104            setIllustration(illustration);
105        } else {
106            final Drawable illustrationImage =
107                    a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustrationImage);
108            final Drawable horizontalTile = a.getDrawable(
109                    R.styleable.SuwSetupWizardLayout_suwIllustrationHorizontalTile);
110            if (illustrationImage != null && horizontalTile != null) {
111                setIllustration(illustrationImage, horizontalTile);
112            }
113        }
114
115        // Set the top padding of the illustration
116        int decorPaddingTop = a.getDimensionPixelSize(
117                R.styleable.SuwSetupWizardLayout_suwDecorPaddingTop, -1);
118        if (decorPaddingTop == -1) {
119            decorPaddingTop = getResources().getDimensionPixelSize(R.dimen.suw_decor_padding_top);
120        }
121        setDecorPaddingTop(decorPaddingTop);
122
123
124        // Set the illustration aspect ratio. See Illustration.setAspectRatio(float). This will
125        // override suwIllustrationPaddingTop if its value is not 0.
126        float illustrationAspectRatio = a.getFloat(
127                R.styleable.SuwSetupWizardLayout_suwIllustrationAspectRatio, -1f);
128        if (illustrationAspectRatio == -1f) {
129            final TypedValue out = new TypedValue();
130            getResources().getValue(R.dimen.suw_illustration_aspect_ratio, out, true);
131            illustrationAspectRatio = out.getFloat();
132        }
133        setIllustrationAspectRatio(illustrationAspectRatio);
134
135        // Set the header text
136        final CharSequence headerText =
137                a.getText(R.styleable.SuwSetupWizardLayout_suwHeaderText);
138        if (headerText != null) {
139            setHeaderText(headerText);
140        }
141
142        a.recycle();
143    }
144
145    @Override
146    protected Parcelable onSaveInstanceState() {
147        final Parcelable parcelable = super.onSaveInstanceState();
148        final SavedState ss = new SavedState(parcelable);
149        ss.mIsProgressBarShown = isProgressBarShown();
150        return ss;
151    }
152
153    @Override
154    protected void onRestoreInstanceState(Parcelable state) {
155        final SavedState ss = (SavedState) state;
156        super.onRestoreInstanceState(ss.getSuperState());
157        final boolean isProgressBarShown = ss.mIsProgressBarShown;
158        if (isProgressBarShown) {
159            showProgressBar();
160        } else {
161            hideProgressBar();
162        }
163    }
164
165    @Override
166    protected View onInflateTemplate(LayoutInflater inflater, int template) {
167        if (template == 0) {
168            template = R.layout.suw_template;
169        }
170        try {
171            return super.onInflateTemplate(inflater, template);
172        } catch (RuntimeException e) {
173            // Versions before M throws RuntimeException for unsuccessful attribute resolution
174            // Versions M+ will throw an InflateException (which extends from RuntimeException)
175            throw new InflateException("Unable to inflate layout. Are you using "
176                    + "@style/SuwThemeMaterial (or its descendant) as your theme?", e);
177        }
178    }
179
180    @Override
181    protected ViewGroup findContainer(int containerId) {
182        if (containerId == 0) {
183            containerId = R.id.suw_layout_content;
184        }
185        return super.findContainer(containerId);
186    }
187
188    public NavigationBar getNavigationBar() {
189        final View view = findManagedViewById(R.id.suw_layout_navigation_bar);
190        return view instanceof NavigationBar ? (NavigationBar) view : null;
191    }
192
193    public ScrollView getScrollView() {
194        final View view = findManagedViewById(R.id.suw_bottom_scroll_view);
195        return view instanceof ScrollView ? (ScrollView) view : null;
196    }
197
198    public void requireScrollToBottom() {
199        final NavigationBar navigationBar = getNavigationBar();
200        final ScrollView scrollView = getScrollView();
201        if (navigationBar != null && (scrollView instanceof BottomScrollView)) {
202            RequireScrollHelper.requireScroll(navigationBar, (BottomScrollView) scrollView);
203        } else {
204            Log.e(TAG, "Both suw_layout_navigation_bar and suw_bottom_scroll_view must exist in"
205                    + " the template to require scrolling.");
206        }
207    }
208
209    public void setHeaderText(int title) {
210        final TextView titleView = getHeaderTextView();
211        if (titleView != null) {
212            titleView.setText(title);
213        }
214    }
215
216    public void setHeaderText(CharSequence title) {
217        final TextView titleView = getHeaderTextView();
218        if (titleView != null) {
219            titleView.setText(title);
220        }
221    }
222
223    public CharSequence getHeaderText() {
224        final TextView titleView = getHeaderTextView();
225        return titleView != null ? titleView.getText() : null;
226    }
227
228    public TextView getHeaderTextView() {
229        return (TextView) findManagedViewById(R.id.suw_layout_title);
230    }
231
232    /**
233     * Set the illustration of the layout. The drawable will be applied as is, and the bounds will
234     * be set as implemented in {@link com.android.setupwizardlib.view.Illustration}. To create
235     * a suitable drawable from an asset and a horizontal repeating tile, use
236     * {@link #setIllustration(int, int)} instead.
237     *
238     * @param drawable The drawable specifying the illustration.
239     */
240    public void setIllustration(Drawable drawable) {
241        final View view = findManagedViewById(R.id.suw_layout_decor);
242        if (view instanceof Illustration) {
243            final Illustration illustration = (Illustration) view;
244            illustration.setIllustration(drawable);
245        }
246    }
247
248    /**
249     * Set the illustration of the layout, which will be created asset and the horizontal tile as
250     * suitable. On phone layouts (not sw600dp), the asset will be scaled, maintaining aspect ratio.
251     * On tablets (sw600dp), the assets will always have 256dp height and the rest of the
252     * illustration area that the asset doesn't fill will be covered by the horizontalTile.
253     *
254     * @param asset Resource ID of the illustration asset.
255     * @param horizontalTile Resource ID of the horizontally repeating tile for tablet layout.
256     */
257    public void setIllustration(int asset, int horizontalTile) {
258        final View view = findManagedViewById(R.id.suw_layout_decor);
259        if (view instanceof Illustration) {
260            final Illustration illustration = (Illustration) view;
261            final Drawable illustrationDrawable = getIllustration(asset, horizontalTile);
262            illustration.setIllustration(illustrationDrawable);
263        }
264    }
265
266    private void setIllustration(Drawable asset, Drawable horizontalTile) {
267        final View view = findManagedViewById(R.id.suw_layout_decor);
268        if (view instanceof Illustration) {
269            final Illustration illustration = (Illustration) view;
270            final Drawable illustrationDrawable = getIllustration(asset, horizontalTile);
271            illustration.setIllustration(illustrationDrawable);
272        }
273    }
274
275    /**
276     * Sets the aspect ratio of the illustration. This will be the space (padding top) reserved
277     * above the header text. This will override the padding top of the illustration.
278     *
279     * @param aspectRatio The aspect ratio
280     * @see com.android.setupwizardlib.view.Illustration#setAspectRatio(float)
281     */
282    public void setIllustrationAspectRatio(float aspectRatio) {
283        final View view = findManagedViewById(R.id.suw_layout_decor);
284        if (view instanceof Illustration) {
285            final Illustration illustration = (Illustration) view;
286            illustration.setAspectRatio(aspectRatio);
287        }
288    }
289
290    /**
291     * Set the top padding of the decor view. If the decor is an Illustration and the aspect ratio
292     * is set, this value will be overridden.
293     *
294     * <p>Note: Currently the default top padding for tablet landscape is 128dp, which is the offset
295     * of the card from the top. This is likely to change in future versions so this value aligns
296     * with the height of the illustration instead.
297     *
298     * @param paddingTop The top padding in pixels.
299     */
300    public void setDecorPaddingTop(int paddingTop) {
301        final View view = findManagedViewById(R.id.suw_layout_decor);
302        if (view != null) {
303            view.setPadding(view.getPaddingLeft(), paddingTop, view.getPaddingRight(),
304                    view.getPaddingBottom());
305        }
306    }
307
308    /**
309     * Set the background of the layout, which is expected to be able to extend infinitely. If it is
310     * a bitmap tile and you want it to repeat, use {@link #setBackgroundTile(int)} instead.
311     */
312    public void setLayoutBackground(Drawable background) {
313        final View view = findManagedViewById(R.id.suw_layout_decor);
314        if (view != null) {
315            //noinspection deprecation
316            view.setBackgroundDrawable(background);
317        }
318    }
319
320    /**
321     * Set the background of the layout to a repeating bitmap tile. To use a different kind of
322     * drawable, use {@link #setLayoutBackground(android.graphics.drawable.Drawable)} instead.
323     */
324    public void setBackgroundTile(int backgroundTile) {
325        final Drawable backgroundTileDrawable =
326                getContext().getResources().getDrawable(backgroundTile);
327        setBackgroundTile(backgroundTileDrawable);
328    }
329
330    private void setBackgroundTile(Drawable backgroundTile) {
331        if (backgroundTile instanceof BitmapDrawable) {
332            ((BitmapDrawable) backgroundTile).setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
333        }
334        setLayoutBackground(backgroundTile);
335    }
336
337    private Drawable getIllustration(int asset, int horizontalTile) {
338        final Context context = getContext();
339        final Drawable assetDrawable = context.getResources().getDrawable(asset);
340        final Drawable tile = context.getResources().getDrawable(horizontalTile);
341        return getIllustration(assetDrawable, tile);
342    }
343
344    @SuppressLint("RtlHardcoded")
345    private Drawable getIllustration(Drawable asset, Drawable horizontalTile) {
346        final Context context = getContext();
347        if (context.getResources().getBoolean(R.bool.suwUseTabletLayout)) {
348            // If it is a "tablet" (sw600dp), create a LayerDrawable with the horizontal tile.
349            if (horizontalTile instanceof BitmapDrawable) {
350                ((BitmapDrawable) horizontalTile).setTileModeX(TileMode.REPEAT);
351                ((BitmapDrawable) horizontalTile).setGravity(Gravity.TOP);
352            }
353            if (asset instanceof BitmapDrawable) {
354                // Always specify TOP | LEFT, Illustration will flip the entire LayerDrawable.
355                ((BitmapDrawable) asset).setGravity(Gravity.TOP | Gravity.LEFT);
356            }
357            final LayerDrawable layers =
358                    new LayerDrawable(new Drawable[] { horizontalTile, asset });
359            if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
360                layers.setAutoMirrored(true);
361            }
362            return layers;
363        } else {
364            // If it is a "phone" (not sw600dp), simply return the illustration
365            if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
366                asset.setAutoMirrored(true);
367            }
368            return asset;
369        }
370    }
371
372    /**
373     * Same as {@link android.view.View#findViewById(int)}, but may include views that are managed
374     * by this view but not currently added to the view hierarchy. e.g. recycler view or list view
375     * headers that are not currently shown.
376     */
377    protected View findManagedViewById(int id) {
378        return findViewById(id);
379    }
380
381    public boolean isProgressBarShown() {
382        final View progressBar = findManagedViewById(R.id.suw_layout_progress);
383        return progressBar != null && progressBar.getVisibility() == View.VISIBLE;
384    }
385
386    /**
387     * Sets whether the progress bar below the header text is shown or not. The progress bar is
388     * a lazily inflated ViewStub, which means the progress bar will not actually be part of the
389     * view hierarchy until the first time this is set to {@code true}.
390     */
391    public void setProgressBarShown(boolean shown) {
392        final View progressBar = findManagedViewById(R.id.suw_layout_progress);
393        if (progressBar != null) {
394            progressBar.setVisibility(shown ? View.VISIBLE : View.GONE);
395        } else if (shown) {
396            final ViewStub progressBarStub =
397                    (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub);
398            if (progressBarStub != null) {
399                progressBarStub.inflate();
400            }
401            if (mProgressBarColor != null) {
402                setProgressBarColor(mProgressBarColor);
403            }
404        }
405    }
406
407    /**
408     * @deprecated Use {@link #setProgressBarShown(boolean)}
409     */
410    @Deprecated
411    public void showProgressBar() {
412        setProgressBarShown(true);
413    }
414
415    /**
416     * @deprecated Use {@link #setProgressBarShown(boolean)}
417     */
418    @Deprecated
419    public void hideProgressBar() {
420        setProgressBarShown(false);
421    }
422
423    public void setProgressBarColor(ColorStateList color) {
424        mProgressBarColor = color;
425        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
426            // Suppress lint error caused by
427            // https://code.google.com/p/android/issues/detail?id=183136
428            // noinspection AndroidLintWrongViewCast
429            final ProgressBar bar = (ProgressBar) findViewById(R.id.suw_layout_progress);
430            if (bar != null) {
431                bar.setIndeterminateTintList(color);
432            }
433        }
434    }
435
436    public ColorStateList getProgressBarColor() {
437        return mProgressBarColor;
438    }
439
440    /* Misc */
441
442    protected static class SavedState extends BaseSavedState {
443
444        boolean mIsProgressBarShown = false;
445
446        public SavedState(Parcelable parcelable) {
447            super(parcelable);
448        }
449
450        public SavedState(Parcel source) {
451            super(source);
452            mIsProgressBarShown = source.readInt() != 0;
453        }
454
455        @Override
456        public void writeToParcel(Parcel dest, int flags) {
457            super.writeToParcel(dest, flags);
458            dest.writeInt(mIsProgressBarShown ? 1 : 0);
459        }
460
461        public static final Parcelable.Creator<SavedState> CREATOR =
462                new Parcelable.Creator<SavedState>() {
463
464                    @Override
465                    public SavedState createFromParcel(Parcel parcel) {
466                        return new SavedState(parcel);
467                    }
468
469                    @Override
470                    public SavedState[] newArray(int size) {
471                        return new SavedState[size];
472                    }
473                };
474    }
475}
476