• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.setupwizardlib;
18 
19 import android.annotation.SuppressLint;
20 import android.annotation.TargetApi;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Shader.TileMode;
24 import android.graphics.drawable.BitmapDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.LayerDrawable;
27 import android.os.Build.VERSION;
28 import android.os.Build.VERSION_CODES;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.util.TypedValue;
34 import android.view.Gravity;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.ViewStub;
39 import android.view.ViewTreeObserver;
40 import android.widget.FrameLayout;
41 import android.widget.TextView;
42 
43 import com.android.setupwizardlib.annotations.Keep;
44 import com.android.setupwizardlib.util.RequireScrollHelper;
45 import com.android.setupwizardlib.view.BottomScrollView;
46 import com.android.setupwizardlib.view.Illustration;
47 import com.android.setupwizardlib.view.NavigationBar;
48 
49 public class SetupWizardLayout extends FrameLayout {
50 
51     private static final String TAG = "SetupWizardLayout";
52 
53     /**
54      * The container of the actual content. This will be a view in the template, which child views
55      * will be added to when {@link #addView(android.view.View)} is called. This will be the layout
56      * in the template that has the ID of {@link #getContainerId()}. For the default implementation
57      * of SetupWizardLayout, that would be @id/suw_layout_content.
58      */
59     private ViewGroup mContainer;
60 
SetupWizardLayout(Context context)61     public SetupWizardLayout(Context context) {
62         super(context);
63         init(0, null, R.attr.suwLayoutTheme);
64     }
65 
SetupWizardLayout(Context context, int template)66     public SetupWizardLayout(Context context, int template) {
67         super(context);
68         init(template, null, R.attr.suwLayoutTheme);
69     }
70 
SetupWizardLayout(Context context, AttributeSet attrs)71     public SetupWizardLayout(Context context, AttributeSet attrs) {
72         super(context, attrs);
73         init(0, attrs, R.attr.suwLayoutTheme);
74     }
75 
76     @TargetApi(VERSION_CODES.HONEYCOMB)
SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr)77     public SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr) {
78         super(context, attrs, defStyleAttr);
79         init(0, attrs, defStyleAttr);
80     }
81 
82     @TargetApi(VERSION_CODES.HONEYCOMB)
SetupWizardLayout(Context context, int template, AttributeSet attrs, int defStyleAttr)83     public SetupWizardLayout(Context context, int template, AttributeSet attrs, int defStyleAttr) {
84         super(context, attrs, defStyleAttr);
85         init(template, attrs, defStyleAttr);
86     }
87 
88     // All the constructors delegate to this init method. The 3-argument constructor is not
89     // available in LinearLayout before v11, so call super with the exact same arguments.
init(int template, AttributeSet attrs, int defStyleAttr)90     private void init(int template, AttributeSet attrs, int defStyleAttr) {
91         final TypedArray a = getContext().obtainStyledAttributes(attrs,
92                 R.styleable.SuwSetupWizardLayout, defStyleAttr, 0);
93         if (template == 0) {
94             template = a.getResourceId(R.styleable.SuwSetupWizardLayout_android_layout, 0);
95         }
96         inflateTemplate(template);
97 
98         // Set the background from XML, either directly or built from a bitmap tile
99         final Drawable background =
100                 a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackground);
101         if (background != null) {
102             setLayoutBackground(background);
103         } else {
104             final Drawable backgroundTile =
105                     a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackgroundTile);
106             if (backgroundTile != null) {
107                 setBackgroundTile(backgroundTile);
108             }
109         }
110 
111         // Set the illustration from XML, either directly or built from image + horizontal tile
112         final Drawable illustration =
113                 a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustration);
114         if (illustration != null) {
115             setIllustration(illustration);
116         } else {
117             final Drawable illustrationImage =
118                     a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustrationImage);
119             final Drawable horizontalTile = a.getDrawable(
120                     R.styleable.SuwSetupWizardLayout_suwIllustrationHorizontalTile);
121             if (illustrationImage != null && horizontalTile != null) {
122                 setIllustration(illustrationImage, horizontalTile);
123             }
124         }
125 
126         // Set the top padding of the illustration
127         int decorPaddingTop = a.getDimensionPixelSize(
128                 R.styleable.SuwSetupWizardLayout_suwDecorPaddingTop, -1);
129         if (decorPaddingTop == -1) {
130             decorPaddingTop = getResources().getDimensionPixelSize(R.dimen.suw_decor_padding_top);
131         }
132         setDecorPaddingTop(decorPaddingTop);
133 
134 
135         // Set the illustration aspect ratio. See Illustration.setAspectRatio(float). This will
136         // override suwIllustrationPaddingTop if its value is not 0.
137         float illustrationAspectRatio = a.getFloat(
138                 R.styleable.SuwSetupWizardLayout_suwIllustrationAspectRatio, -1f);
139         if (illustrationAspectRatio == -1f) {
140             final TypedValue out = new TypedValue();
141             getResources().getValue(R.dimen.suw_illustration_aspect_ratio, out, true);
142             illustrationAspectRatio = out.getFloat();
143         }
144         setIllustrationAspectRatio(illustrationAspectRatio);
145 
146         // Set the header text
147         final CharSequence headerText =
148                 a.getText(R.styleable.SuwSetupWizardLayout_suwHeaderText);
149         if (headerText != null) {
150             setHeaderText(headerText);
151         }
152 
153         a.recycle();
154     }
155 
156     @Override
onSaveInstanceState()157     protected Parcelable onSaveInstanceState() {
158         final Parcelable parcelable = super.onSaveInstanceState();
159         final SavedState ss = new SavedState(parcelable);
160         ss.isProgressBarShown = isProgressBarShown();
161         return ss;
162     }
163 
164     @Override
onRestoreInstanceState(Parcelable state)165     protected void onRestoreInstanceState(Parcelable state) {
166         final SavedState ss = (SavedState) state;
167         super.onRestoreInstanceState(ss.getSuperState());
168         final boolean isProgressBarShown = ss.isProgressBarShown;
169         if (isProgressBarShown) {
170             showProgressBar();
171         } else {
172             hideProgressBar();
173         }
174     }
175 
176     @Override
addView(View child, int index, ViewGroup.LayoutParams params)177     public void addView(View child, int index, ViewGroup.LayoutParams params) {
178         mContainer.addView(child, index, params);
179     }
180 
addViewInternal(View child)181     private void addViewInternal(View child) {
182         super.addView(child, -1, generateDefaultLayoutParams());
183     }
184 
inflateTemplate(int templateResource)185     private void inflateTemplate(int templateResource) {
186         final LayoutInflater inflater = LayoutInflater.from(getContext());
187         final View templateRoot = onInflateTemplate(inflater, templateResource);
188         addViewInternal(templateRoot);
189 
190         mContainer = (ViewGroup) findViewById(getContainerId());
191         onTemplateInflated();
192     }
193 
194     /**
195      * This method inflates the template. Subclasses can override this method to customize the
196      * template inflation, or change to a different default template. The root of the inflated
197      * layout should be returned, and not added to the view hierarchy.
198      *
199      * @param inflater A LayoutInflater to inflate the template.
200      * @param template The resource ID of the template to be inflated, or 0 if no template is
201      *                 specified.
202      * @return Root of the inflated layout.
203      */
onInflateTemplate(LayoutInflater inflater, int template)204     protected View onInflateTemplate(LayoutInflater inflater, int template) {
205         if (template == 0) {
206             template = R.layout.suw_template;
207         }
208         return inflater.inflate(template, this, false);
209     }
210 
211     /**
212      * This is called after the template has been inflated and added to the view hierarchy.
213      * Subclasses can implement this method to modify the template as necessary, such as caching
214      * views retrieved from findViewById, or other view operations that need to be done in code.
215      * You can think of this as {@link android.view.View#onFinishInflate()} but for inflation of the
216      * template instead of for child views.
217      */
onTemplateInflated()218     protected void onTemplateInflated() {
219     }
220 
getContainerId()221     protected int getContainerId() {
222         return R.id.suw_layout_content;
223     }
224 
getNavigationBar()225     public NavigationBar getNavigationBar() {
226         final View view = findViewById(R.id.suw_layout_navigation_bar);
227         return view instanceof NavigationBar ? (NavigationBar) view : null;
228     }
229 
getScrollView()230     private BottomScrollView getScrollView() {
231         final View view = findViewById(R.id.suw_bottom_scroll_view);
232         return view instanceof BottomScrollView ? (BottomScrollView) view : null;
233     }
234 
requireScrollToBottom()235     public void requireScrollToBottom() {
236         final NavigationBar navigationBar = getNavigationBar();
237         final BottomScrollView scrollView = getScrollView();
238         if (navigationBar != null && scrollView != null) {
239             RequireScrollHelper.requireScroll(navigationBar, scrollView);
240         } else {
241             Log.e(TAG, "Both suw_layout_navigation_bar and suw_bottom_scroll_view must exist in"
242                     + " the template to require scrolling.");
243         }
244     }
245 
setHeaderText(int title)246     public void setHeaderText(int title) {
247         final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
248         if (titleView != null) {
249             titleView.setText(title);
250         }
251     }
252 
setHeaderText(CharSequence title)253     public void setHeaderText(CharSequence title) {
254         final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
255         if (titleView != null) {
256             titleView.setText(title);
257         }
258     }
259 
getHeaderText()260     public CharSequence getHeaderText() {
261         final TextView titleView = (TextView) findViewById(R.id.suw_layout_title);
262         return titleView != null ? titleView.getText() : null;
263     }
264 
265     /**
266      * Set the illustration of the layout. The drawable will be applied as is, and the bounds will
267      * be set as implemented in {@link com.android.setupwizardlib.view.Illustration}. To create
268      * a suitable drawable from an asset and a horizontal repeating tile, use
269      * {@link #setIllustration(int, int)} instead.
270      *
271      * @param drawable The drawable specifying the illustration.
272      */
setIllustration(Drawable drawable)273     public void setIllustration(Drawable drawable) {
274         final View view = findViewById(R.id.suw_layout_decor);
275         if (view instanceof Illustration) {
276             final Illustration illustration = (Illustration) view;
277             illustration.setIllustration(drawable);
278         }
279     }
280 
281     /**
282      * Set the illustration of the layout, which will be created asset and the horizontal tile as
283      * suitable. On phone layouts (not sw600dp), the asset will be scaled, maintaining aspect ratio.
284      * On tablets (sw600dp), the assets will always have 256dp height and the rest of the
285      * illustration area that the asset doesn't fill will be covered by the horizontalTile.
286      *
287      * @param asset Resource ID of the illustration asset.
288      * @param horizontalTile Resource ID of the horizontally repeating tile for tablet layout.
289      */
setIllustration(int asset, int horizontalTile)290     public void setIllustration(int asset, int horizontalTile) {
291         final View view = findViewById(R.id.suw_layout_decor);
292         if (view instanceof Illustration) {
293             final Illustration illustration = (Illustration) view;
294             final Drawable illustrationDrawable = getIllustration(asset, horizontalTile);
295             illustration.setIllustration(illustrationDrawable);
296         }
297     }
298 
setIllustration(Drawable asset, Drawable horizontalTile)299     private void setIllustration(Drawable asset, Drawable horizontalTile) {
300         final View view = findViewById(R.id.suw_layout_decor);
301         if (view instanceof Illustration) {
302             final Illustration illustration = (Illustration) view;
303             final Drawable illustrationDrawable = getIllustration(asset, horizontalTile);
304             illustration.setIllustration(illustrationDrawable);
305         }
306     }
307 
308     /**
309      * Sets the aspect ratio of the illustration. This will be the space (padding top) reserved
310      * above the header text. This will override the padding top of the illustration.
311      *
312      * @param aspectRatio The aspect ratio
313      * @see com.android.setupwizardlib.view.Illustration#setAspectRatio(float)
314      */
setIllustrationAspectRatio(float aspectRatio)315     public void setIllustrationAspectRatio(float aspectRatio) {
316         final View view = findViewById(R.id.suw_layout_decor);
317         if (view instanceof Illustration) {
318             final Illustration illustration = (Illustration) view;
319             illustration.setAspectRatio(aspectRatio);
320         }
321     }
322 
323     /**
324      * Set the top padding of the decor view. If the decor is an Illustration and the aspect ratio
325      * is set, this value will be overridden.
326      *
327      * Note: Currently the default top padding for tablet landscape is 128dp, which is the offset
328      * of the card from the top. This is likely to change in future versions so this value aligns
329      * with the height of the illustration instead.
330      *
331      * @param paddingTop The top padding in pixels.
332      */
setDecorPaddingTop(int paddingTop)333     public void setDecorPaddingTop(int paddingTop) {
334         final View view = findViewById(R.id.suw_layout_decor);
335         if (view != null) {
336             view.setPadding(view.getPaddingLeft(), paddingTop, view.getPaddingRight(),
337                     view.getPaddingBottom());
338         }
339     }
340 
341     /**
342      * Set the background of the layout, which is expected to be able to extend infinitely. If it is
343      * a bitmap tile and you want it to repeat, use {@link #setBackgroundTile(int)} instead.
344      */
setLayoutBackground(Drawable background)345     public void setLayoutBackground(Drawable background) {
346         final View view = findViewById(R.id.suw_layout_decor);
347         if (view != null) {
348             //noinspection deprecation
349             view.setBackgroundDrawable(background);
350         }
351     }
352 
353     /**
354      * Set the background of the layout to a repeating bitmap tile. To use a different kind of
355      * drawable, use {@link #setLayoutBackground(android.graphics.drawable.Drawable)} instead.
356      */
setBackgroundTile(int backgroundTile)357     public void setBackgroundTile(int backgroundTile) {
358         final Drawable backgroundTileDrawable =
359                 getContext().getResources().getDrawable(backgroundTile);
360         setBackgroundTile(backgroundTileDrawable);
361     }
362 
setBackgroundTile(Drawable backgroundTile)363     private void setBackgroundTile(Drawable backgroundTile) {
364         if (backgroundTile instanceof BitmapDrawable) {
365             ((BitmapDrawable) backgroundTile).setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
366         }
367         setLayoutBackground(backgroundTile);
368     }
369 
getIllustration(int asset, int horizontalTile)370     private Drawable getIllustration(int asset, int horizontalTile) {
371         final Context context = getContext();
372         final Drawable assetDrawable = context.getResources().getDrawable(asset);
373         final Drawable tile = context.getResources().getDrawable(horizontalTile);
374         return getIllustration(assetDrawable, tile);
375     }
376 
377     @SuppressLint("RtlHardcoded")
getIllustration(Drawable asset, Drawable horizontalTile)378     private Drawable getIllustration(Drawable asset, Drawable horizontalTile) {
379         final Context context = getContext();
380         if (context.getResources().getBoolean(R.bool.suwUseTabletLayout)) {
381             // If it is a "tablet" (sw600dp), create a LayerDrawable with the horizontal tile.
382             if (horizontalTile instanceof BitmapDrawable) {
383                 ((BitmapDrawable) horizontalTile).setTileModeX(TileMode.REPEAT);
384                 ((BitmapDrawable) horizontalTile).setGravity(Gravity.TOP);
385             }
386             if (asset instanceof BitmapDrawable) {
387                 // Always specify TOP | LEFT, Illustration will flip the entire LayerDrawable.
388                 ((BitmapDrawable) asset).setGravity(Gravity.TOP | Gravity.LEFT);
389             }
390             final LayerDrawable layers =
391                     new LayerDrawable(new Drawable[] { horizontalTile, asset });
392             if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
393                 layers.setAutoMirrored(true);
394             }
395             return layers;
396         } else {
397             // If it is a "phone" (not sw600dp), simply return the illustration
398             if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
399                 asset.setAutoMirrored(true);
400             }
401             return asset;
402         }
403     }
404 
isProgressBarShown()405     public boolean isProgressBarShown() {
406         final View progressBar = findViewById(R.id.suw_layout_progress);
407         return progressBar != null && progressBar.getVisibility() == View.VISIBLE;
408     }
409 
showProgressBar()410     public void showProgressBar() {
411         final View progressBar = findViewById(R.id.suw_layout_progress);
412         if (progressBar != null) {
413             progressBar.setVisibility(View.VISIBLE);
414         } else {
415             final ViewStub progressBarStub = (ViewStub) findViewById(R.id.suw_layout_progress_stub);
416             if (progressBarStub != null) {
417                 progressBarStub.inflate();
418             }
419         }
420     }
421 
hideProgressBar()422     public void hideProgressBar() {
423         final View progressBar = findViewById(R.id.suw_layout_progress);
424         if (progressBar != null) {
425             progressBar.setVisibility(View.GONE);
426         }
427     }
428 
429     /* Animator support */
430 
431     private float mXFraction;
432     private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
433 
434     /**
435      * Set the X translation as a fraction of the width of this view. Make sure this method is not
436      * stripped out by proguard when using ObjectAnimator. You may need to add
437      *     -keep @com.android.setupwizardlib.annotations.Keep class *
438      * to your proguard configuration if you are seeing mysterious MethodNotFoundExceptions at
439      * runtime.
440      */
441     @Keep
setXFraction(float fraction)442     public void setXFraction(float fraction) {
443         mXFraction = fraction;
444         final int width = getWidth();
445         if (width != 0) {
446             setTranslationX(width * fraction);
447         } else {
448             // If we haven't done a layout pass yet, wait for one and then set the fraction before
449             // the draw occurs using an OnPreDrawListener. Don't call translationX until we know
450             // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on
451             // screen.
452             if (mPreDrawListener == null) {
453                 mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
454                     @Override
455                     public boolean onPreDraw() {
456                         getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
457                         setXFraction(mXFraction);
458                         return true;
459                     }
460                 };
461                 getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
462             }
463         }
464     }
465 
466     /**
467      * Return the X translation as a fraction of the width, as previously set in setXFraction.
468      *
469      * @see #setXFraction(float)
470      */
471     @Keep
getXFraction()472     public float getXFraction() {
473         return mXFraction;
474     }
475 
476     /* Misc */
477 
478     protected static class SavedState extends BaseSavedState {
479 
480         boolean isProgressBarShown = false;
481 
SavedState(Parcelable parcelable)482         public SavedState(Parcelable parcelable) {
483             super(parcelable);
484         }
485 
SavedState(Parcel source)486         public SavedState(Parcel source) {
487             super(source);
488             isProgressBarShown = source.readInt() != 0;
489         }
490 
491         @Override
writeToParcel(Parcel dest, int flags)492         public void writeToParcel(Parcel dest, int flags) {
493             super.writeToParcel(dest, flags);
494             dest.writeInt(isProgressBarShown ? 1 : 0);
495         }
496 
497         public static final Parcelable.Creator<SavedState> CREATOR =
498                 new Parcelable.Creator<SavedState>() {
499 
500                     @Override
501                     public SavedState createFromParcel(Parcel parcel) {
502                         return new SavedState(parcel);
503                     }
504 
505                     @Override
506                     public SavedState[] newArray(int size) {
507                         return new SavedState[size];
508                     }
509                 };
510     }
511 }
512