From 2646e1d82ec6d133b35b775f044e156fca6d9d75 Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Tue, 17 Mar 2015 14:33:48 -0700 Subject: [SetupWizardLib] Changed project hierarchy Moved res-ics to ics/res, and res, src, AndroidManifest to main/. This fits more to the "canonical layout" expected by gradle, and works around the issue where blaze is expecting every res directory used by a target to be named the same (including its library dependencies and its transitive dependencies). Change-Id: I658e3c0a67a01f379c43d3fad82cd40f9aa8cd28 --- .../android/setupwizardlib/SetupWizardLayout.java | 282 +++++++++++++++++++++ .../setupwizardlib/SetupWizardListLayout.java | 77 ++++++ .../com/android/setupwizardlib/util/Partner.java | 157 ++++++++++++ .../android/setupwizardlib/util/ResultCodes.java | 28 ++ .../setupwizardlib/util/SystemBarHelper.java | 231 +++++++++++++++++ .../setupwizardlib/util/WizardManagerHelper.java | 69 +++++ .../setupwizardlib/view/BottomScrollView.java | 98 +++++++ .../android/setupwizardlib/view/Illustration.java | 185 ++++++++++++++ .../android/setupwizardlib/view/NavigationBar.java | 130 ++++++++++ .../setupwizardlib/view/StickyHeaderListView.java | 145 +++++++++++ .../view/StickyHeaderScrollView.java | 127 ++++++++++ 11 files changed, 1529 insertions(+) create mode 100644 library/main/src/com/android/setupwizardlib/SetupWizardLayout.java create mode 100644 library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java create mode 100644 library/main/src/com/android/setupwizardlib/util/Partner.java create mode 100644 library/main/src/com/android/setupwizardlib/util/ResultCodes.java create mode 100644 library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java create mode 100644 library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java create mode 100644 library/main/src/com/android/setupwizardlib/view/BottomScrollView.java create mode 100644 library/main/src/com/android/setupwizardlib/view/Illustration.java create mode 100644 library/main/src/com/android/setupwizardlib/view/NavigationBar.java create mode 100644 library/main/src/com/android/setupwizardlib/view/StickyHeaderListView.java create mode 100644 library/main/src/com/android/setupwizardlib/view/StickyHeaderScrollView.java (limited to 'library/main/src/com/android/setupwizardlib') diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java new file mode 100644 index 0000000..2a1000d --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Shader.TileMode; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.setupwizardlib.view.Illustration; + +public class SetupWizardLayout extends FrameLayout { + + private static final String TAG = "SetupWizardLayout"; + + /** + * The container of the actual content. This will be a view in the template, which child views + * will be added to when {@link #addView(android.view.View)} is called. This will be the layout + * in the template that has the ID of {@link #getContainerId()}. For the default implementation + * of SetupWizardLayout, that would be @id/suw_layout_content. + */ + private ViewGroup mContainer; + + public SetupWizardLayout(Context context) { + this(context, 0); + } + + public SetupWizardLayout(Context context, int template) { + this(context, template, null, 0); + } + + public SetupWizardLayout(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.suwLayoutTheme); + } + + public SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, 0, attrs, defStyleAttr); + } + + public SetupWizardLayout(Context context, int template, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwSetupWizardLayout, defStyleAttr, 0); + if (template == 0) { + template = a.getResourceId(R.styleable.SuwSetupWizardLayout_android_layout, 0); + } + inflateTemplate(template); + + // Set the background from XML, either directly or built from a bitmap tile + final Drawable background = + a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackground); + if (background != null) { + setLayoutBackground(background); + } else { + final Drawable backgroundTile = + a.getDrawable(R.styleable.SuwSetupWizardLayout_suwBackgroundTile); + if (backgroundTile != null) { + setBackgroundTile(backgroundTile); + } + } + + // Set the illustration from XML, either directly or built from image + horizontal tile + final Drawable illustration = + a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustration); + if (illustration != null) { + setIllustration(illustration); + } else { + final Drawable illustrationImage = + a.getDrawable(R.styleable.SuwSetupWizardLayout_suwIllustrationImage); + final Drawable horizontalTile = a.getDrawable( + R.styleable.SuwSetupWizardLayout_suwIllustrationHorizontalTile); + if (illustrationImage != null && horizontalTile != null) { + setIllustration(illustrationImage, horizontalTile); + } + } + + // Set the header text + final CharSequence headerText = + a.getText(R.styleable.SuwSetupWizardLayout_suwHeaderText); + if (headerText != null) { + setHeaderText(headerText); + } + + a.recycle(); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + mContainer.addView(child, index, params); + } + + private void addViewInternal(View child) { + super.addView(child, -1, generateDefaultLayoutParams()); + } + + private void inflateTemplate(int templateResource) { + final LayoutInflater inflater = LayoutInflater.from(getContext()); + final View templateRoot = onInflateTemplate(inflater, templateResource); + addViewInternal(templateRoot); + + mContainer = (ViewGroup) findViewById(getContainerId()); + onTemplateInflated(); + } + + /** + * This method inflates the template. Subclasses can override this method to customize the + * template inflation, or change to a different default template. The root of the inflated + * layout should be returned, and not added to the view hierarchy. + * + * @param inflater A LayoutInflater to inflate the template. + * @param template The resource ID of the template to be inflated, or 0 if no template is + * specified. + * @return Root of the inflated layout. + */ + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_template; + } + return inflater.inflate(template, this, false); + } + + /** + * This is called after the template has been inflated and added to the view hierarchy. + * Subclasses can implement this method to modify the template as necessary, such as caching + * views retrieved from findViewById, or other view operations that need to be done in code. + * You can think of this as {@link android.view.View#onFinishInflate()} but for inflation of the + * template instead of for child views. + */ + protected void onTemplateInflated() { + } + + protected int getContainerId() { + return R.id.suw_layout_content; + } + + public void setHeaderText(int title) { + final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + if (titleView != null) { + titleView.setText(title); + } + } + + public void setHeaderText(CharSequence title) { + final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + if (titleView != null) { + titleView.setText(title); + } + } + + /** + * Set the illustration of the layout. The drawable will be applied as is, and the bounds will + * be set as implemented in {@link com.android.setupwizardlib.view.Illustration}. To create + * a suitable drawable from an asset and a horizontal repeating tile, use + * {@link #setIllustration(int, int)} instead. + * + * @param drawable The drawable specifying the illustration. + */ + public void setIllustration(Drawable drawable) { + final View view = findViewById(R.id.suw_layout_decor); + if (view instanceof Illustration) { + final Illustration illustration = (Illustration) view; + illustration.setIllustration(drawable); + } + } + + /** + * Set the illustration of the layout, which will be created asset and the horizontal tile as + * suitable. On phone layouts (not sw600dp), the asset will be scaled, maintaining aspect ratio. + * On tablets (sw600dp), the assets will always have 256dp height and the rest of the + * illustration area that the asset doesn't fill will be covered by the horizontalTile. + * + * @param asset Resource ID of the illustration asset. + * @param horizontalTile Resource ID of the horizontally repeating tile for tablet layout. + */ + public void setIllustration(int asset, int horizontalTile) { + final View view = findViewById(R.id.suw_layout_decor); + if (view instanceof Illustration) { + final Illustration illustration = (Illustration) view; + final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); + illustration.setIllustration(illustrationDrawable); + } + } + + private void setIllustration(Drawable asset, Drawable horizontalTile) { + final View view = findViewById(R.id.suw_layout_decor); + if (view instanceof Illustration) { + final Illustration illustration = (Illustration) view; + final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); + illustration.setIllustration(illustrationDrawable); + } + } + + /** + * Set the background of the layout, which is expected to be able to extend infinitely. If it is + * a bitmap tile and you want it to repeat, use {@link #setBackgroundTile(int)} instead. + */ + public void setLayoutBackground(Drawable background) { + final View view = findViewById(R.id.suw_layout_decor); + if (view != null) { + //noinspection deprecation + view.setBackgroundDrawable(background); + } + } + + /** + * Set the background of the layout to a repeating bitmap tile. To use a different kind of + * drawable, use {@link #setLayoutBackground(android.graphics.drawable.Drawable)} instead. + */ + public void setBackgroundTile(int backgroundTile) { + final Drawable backgroundTileDrawable = + getContext().getResources().getDrawable(backgroundTile); + setBackgroundTile(backgroundTileDrawable); + } + + private void setBackgroundTile(Drawable backgroundTile) { + if (backgroundTile instanceof BitmapDrawable) { + ((BitmapDrawable) backgroundTile).setTileModeXY(TileMode.REPEAT, TileMode.REPEAT); + } + setLayoutBackground(backgroundTile); + } + + private Drawable getIllustration(int asset, int horizontalTile) { + final Context context = getContext(); + final Drawable assetDrawable = context.getResources().getDrawable(asset); + final Drawable tile = context.getResources().getDrawable(horizontalTile); + return getIllustration(assetDrawable, tile); + } + + @SuppressLint("RtlHardcoded") + private Drawable getIllustration(Drawable asset, Drawable horizontalTile) { + final Context context = getContext(); + if (context.getResources().getBoolean(R.bool.suwUseTabletLayout)) { + // If it is a "tablet" (sw600dp), create a LayerDrawable with the horizontal tile. + if (horizontalTile instanceof BitmapDrawable) { + ((BitmapDrawable) horizontalTile).setTileModeX(TileMode.REPEAT); + ((BitmapDrawable) horizontalTile).setGravity(Gravity.TOP); + } + if (asset instanceof BitmapDrawable) { + // Always specify TOP | LEFT, Illustration will flip the entire LayerDrawable. + ((BitmapDrawable) asset).setGravity(Gravity.TOP | Gravity.LEFT); + } + final LayerDrawable layers = + new LayerDrawable(new Drawable[] { horizontalTile, asset }); + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + layers.setAutoMirrored(true); + } + return layers; + } else { + // If it is a "phone" (not sw600dp), simply return the illustration + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + asset.setAutoMirrored(true); + } + return asset; + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java new file mode 100644 index 0000000..228ff12 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ListAdapter; +import android.widget.ListView; + +public class SetupWizardListLayout extends SetupWizardLayout { + + private static final String TAG = "SetupWizardListLayout"; + private ListView mListView; + + public SetupWizardListLayout(Context context) { + this(context, 0); + } + + public SetupWizardListLayout(Context context, int template) { + this(context, template, null, 0); + } + + public SetupWizardListLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SetupWizardListLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, 0, attrs, defStyleAttr); + } + + public SetupWizardListLayout(Context context, int template, AttributeSet attrs, + int defStyleAttr) { + super(context, template, attrs, defStyleAttr); + } + + @Override + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_list_template; + } + return inflater.inflate(template, this, false); + } + + @Override + protected void onTemplateInflated() { + mListView = (ListView) findViewById(android.R.id.list); + } + + @Override + protected int getContainerId() { + return android.R.id.list; + } + + public ListView getListView() { + return mListView; + } + + public void setAdapter(ListAdapter adapter) { + getListView().setAdapter(adapter); + } +} diff --git a/library/main/src/com/android/setupwizardlib/util/Partner.java b/library/main/src/com/android/setupwizardlib/util/Partner.java new file mode 100644 index 0000000..5a81d4c --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/util/Partner.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.util; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.android.annotations.VisibleForTesting; + +/** + * Utilities to discover and interact with partner customizations. There can only be one set of + * customizations on a device, and it must be bundled with the system. + * + * Derived from com.android.launcher3/Partner.java + */ +public class Partner { + private static final String TAG = "(SUW) Partner"; + + /** Marker action used to discover partner */ + private static final String + ACTION_PARTNER_CUSTOMIZATION = "com.android.setupwizard.action.PARTNER_CUSTOMIZATION"; + + private static boolean sSearched = false; + private static Partner sPartner; + + /** + * Convenience to get a drawable from partner overlay, or if not available, the drawable from + * the original context. + * + * @see #getResourceEntry(android.content.Context, int) + */ + public static Drawable getDrawable(Context context, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getDrawable(entry.id); + } + + /** + * Convenience to get a string from partner overlay, or if not available, the string from the + * original context. + * + * @see #getResourceEntry(android.content.Context, int) + */ + public static String getString(Context context, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getString(entry.id); + } + + /** + * Find an entry of resource in the overlay package provided by partners. It will first look for + * the resource in the overlay package, and if not available, will return the one in the + * original context. + * + * @return a ResourceEntry in the partner overlay's resources, if one is defined. Otherwise the + * resources from the original context is returned. Clients can then get the resource by + * {@code entry.resources.getString(entry.id)}, or other methods available in + * {@link android.content.res.Resources}. + */ + public static ResourceEntry getResourceEntry(Context context, int id) { + final Partner partner = Partner.get(context); + if (partner == null) { + return new ResourceEntry(context.getResources(), id); + } else { + final Resources ourResources = context.getResources(); + final String name = ourResources.getResourceEntryName(id); + final String type = ourResources.getResourceTypeName(id); + final int partnerId = partner.getIdentifier(name, type); + return new ResourceEntry(partner.mResources, partnerId); + } + } + + public static class ResourceEntry { + public Resources resources; + public int id; + + ResourceEntry(Resources resources, int id) { + this.resources = resources; + this.id = id; + } + } + + /** + * Find and return partner details, or {@code null} if none exists. A partner package is marked + * by a broadcast receiver declared in the manifest that handles the + * com.android.setupwizard.action.PARTNER_CUSTOMIZATION intent action. The overlay package must + * also be a system package. + */ + public static synchronized Partner get(Context context) { + if (!sSearched) { + PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (info.activityInfo == null) { + continue; + } + final ApplicationInfo appInfo = info.activityInfo.applicationInfo; + if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + try { + final Resources res = pm.getResourcesForApplication(appInfo); + sPartner = new Partner(appInfo.packageName, res); + break; + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + appInfo.packageName); + } + } + } + sSearched = true; + } + return sPartner; + } + + @VisibleForTesting + public static synchronized void resetForTesting() { + sSearched = false; + sPartner = null; + } + + private final String mPackageName; + private final Resources mResources; + + private Partner(String packageName, Resources res) { + mPackageName = packageName; + mResources = res; + } + + public String getPackageName() { + return mPackageName; + } + + public Resources getResources() { + return mResources; + } + + public int getIdentifier(String name, String defType) { + return mResources.getIdentifier(name, defType, mPackageName); + } +} diff --git a/library/main/src/com/android/setupwizardlib/util/ResultCodes.java b/library/main/src/com/android/setupwizardlib/util/ResultCodes.java new file mode 100644 index 0000000..a429e73 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/util/ResultCodes.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.util; + +import static android.app.Activity.RESULT_FIRST_USER; + +public final class ResultCodes { + + public static final int RESULT_SKIP = RESULT_FIRST_USER; + public static final int RESULT_RETRY = RESULT_FIRST_USER + 1; + public static final int RESULT_ACTIVITY_NOT_FOUND = RESULT_FIRST_USER + 2; + + public static final int RESULT_FIRST_SETUP_USER = RESULT_FIRST_USER + 100; +} diff --git a/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java new file mode 100644 index 0000000..a9fc3f5 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import com.android.setupwizardlib.R; + +/** + * A helper class to manage the system navigation bar and status bar. This will add various + * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style. + * + * When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the system + * bars using methods from this class. For Lollipop, {@link #hideSystemBars(android.view.Window)} + * will completely hide the system navigation bar and change the status bar to transparent, and + * layout the screen contents (usually the illustration) behind it. + */ +public class SystemBarHelper { + + @SuppressLint("InlinedApi") + private static final int DEFAULT_IMMERSIVE_FLAGS = + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + + /** + * Needs to be equal to View.STATUS_BAR_DISABLE_BACK + */ + private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; + + /** + * Hide the navigation bar for a dialog. + * + * This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + */ + public static void hideSystemBars(final Dialog dialog) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + final Window window = dialog.getWindow(); + temporarilyDisableDialogFocus(window); + hideSystemBars(window); + } + } + + /** + * Hide the navigation bar, and make the color of the status and navigation bars transparent, + * and specify the LAYOUT_FULLSCREEN flag so that the content is laid-out behind the transparent + * status bar. This is commonly used with Activity.getWindow() to make the navigation and status + * bars follow the Setup Wizard style. + * + * This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + */ + public static void hideSystemBars(final Window window) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + addImmersiveFlagsToWindow(window); + addImmersiveFlagsToDecorView(window, new Handler()); + } + } + + /** + * Convenience method to add a visibility flag in addition to the existing ones. + */ + public static void addVisibilityFlag(final View view, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + final int vis = view.getSystemUiVisibility(); + view.setSystemUiVisibility(vis | flag); + } + } + + /** + * Convenience method to add a visibility flag in addition to the existing ones. + */ + public static void addVisibilityFlag(final Window window, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.systemUiVisibility |= flag; + window.setAttributes(attrs); + } + } + + /** + * Convenience method to remove a visibility flag from the view, leaving other flags that are + * not specified intact. + */ + public static void removeVisibilityFlag(final View view, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + final int vis = view.getSystemUiVisibility(); + view.setSystemUiVisibility(vis & ~flag); + } + } + + /** + * Convenience method to remove a visibility flag from the window, leaving other flags that are + * not specified intact. + */ + public static void removeVisibilityFlag(final Window window, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.systemUiVisibility &= ~flag; + window.setAttributes(attrs); + } + } + + public static void setBackButtonVisible(final Window window, final boolean visible) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + if (visible) { + addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); + } else { + removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); + } + } + } + + /** + * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the + * view to be immediately above the keyboard, and assumes that the view sits immediately above + * the navigation bar. + * + * This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + * + * @param view The view to be resized when the keyboard is shown. + */ + public static void setImeInsetView(final View view) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + view.setOnApplyWindowInsetsListener(new WindowInsetsListener(view.getContext())); + } + } + + /** + * View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN only takes effect when it is added a view instead of + * the window. + */ + @TargetApi(VERSION_CODES.LOLLIPOP) + private static void addImmersiveFlagsToDecorView(final Window window, final Handler handler) { + // Use peekDecorView instead of getDecorView so that clients can still set window features + // after calling this method. + final View decorView = window.peekDecorView(); + if (decorView != null) { + addVisibilityFlag(decorView, DEFAULT_IMMERSIVE_FLAGS); + } else { + // If the decor view is not installed yet, try again in the next loop. + handler.post(new Runnable() { + @Override + public void run() { + addImmersiveFlagsToDecorView(window, handler); + } + }); + } + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + private static void addImmersiveFlagsToWindow(final Window window) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.systemUiVisibility |= DEFAULT_IMMERSIVE_FLAGS; + window.setAttributes(attrs); + + // Also set the navigation bar and status bar to transparent color. Note that this doesn't + // work on some devices. + window.setNavigationBarColor(0); + window.setStatusBarColor(0); + } + + /** + * Apply a hack to temporarily set the window to not focusable, so that the navigation bar + * will not show up during the transition. + */ + private static void temporarilyDisableDialogFocus(final Window window) { + window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + new Handler().post(new Runnable() { + @Override + public void run() { + window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + }); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { + + private int mNavigationBarHeight; + + public WindowInsetsListener(Context context) { + mNavigationBarHeight = + context.getResources().getDimensionPixelSize(R.dimen.suw_navbar_height); + } + + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { + + final int bottomMargin = Math.max( + insets.getSystemWindowInsetBottom() - mNavigationBarHeight, 0); + + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); + view.requestLayout(); + + return insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), + 0 /* bottom */ + ); + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java new file mode 100644 index 0000000..563e97d --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.util; + +import android.content.Intent; + +public class WizardManagerHelper { + + private static final String ACTION_NEXT = "com.android.wizard.NEXT"; + + private static final String EXTRA_SCRIPT_URI = "scriptUri"; + private static final String EXTRA_ACTION_ID = "actionId"; + private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode"; + + public static final String EXTRA_THEME = "theme"; + public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; + + public static final String THEME_MATERIAL = "material"; + public static final String THEME_MATERIAL_LIGHT = "material_light"; + + /** + * Get an intent that will invoke the next step of setup wizard. + * + * @param originalIntent The original intent that was used to start the step, usually via + * Activity.getIntent(). + * @param resultCode The result code of the step. See {@link ResultCodes}. + * @return A new intent that can be used with startActivityForResult() to start the next step of + * the setup flow. + */ + public static Intent getNextIntent(Intent originalIntent, int resultCode) { + return getNextIntent(originalIntent, resultCode, null); + } + + /** + * Get an intent that will invoke the next step of setup wizard. + * + * @param originalIntent The original intent that was used to start the step, usually via + * Activity.getIntent(). + * @param resultCode The result code of the step. See {@link ResultCodes}. + * @param data An intent containing extra result data. + * @return A new intent that can be used with startActivityForResult() to start the next step of + * the setup flow. + */ + public static Intent getNextIntent(Intent originalIntent, int resultCode, Intent data) { + Intent intent = new Intent(ACTION_NEXT); + intent.putExtra(EXTRA_SCRIPT_URI, originalIntent.getStringExtra(EXTRA_SCRIPT_URI)); + intent.putExtra(EXTRA_ACTION_ID, originalIntent.getStringExtra(EXTRA_ACTION_ID)); + intent.putExtra(EXTRA_RESULT_CODE, resultCode); + if (data != null && data.getExtras() != null) { + intent.putExtras(data.getExtras()); + } + intent.putExtra(EXTRA_THEME, originalIntent.getStringExtra(EXTRA_THEME)); + return intent; + } +} diff --git a/library/main/src/com/android/setupwizardlib/view/BottomScrollView.java b/library/main/src/com/android/setupwizardlib/view/BottomScrollView.java new file mode 100644 index 0000000..806ecf4 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/BottomScrollView.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ScrollView; + +/** + * An extension of ScrollView that will invoke a listener callback when the ScrollView needs + * scrolling, and when the ScrollView is being scrolled to the bottom. This is often used in Setup + * Wizard as a way to ensure that users see all the content before proceeding. + */ +public class BottomScrollView extends ScrollView { + + public interface BottomScrollListener { + void onScrolledToBottom(); + void onRequiresScroll(); + } + + private BottomScrollListener mListener; + private int mScrollThreshold; + private boolean mRequiringScroll = false; + + private final Runnable mCheckScrollRunnable = new Runnable() { + @Override + public void run() { + checkScroll(); + } + }; + + public BottomScrollView(Context context) { + super(context); + } + + public BottomScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BottomScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setBottomScrollListener(BottomScrollListener l) { + mListener = l; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + View child = getChildAt(0); + if (child != null) { + mScrollThreshold = Math.max(0, getChildAt(0).getHeight() - b + t + - getPaddingBottom()); + } + if (b - t > 0) { + // Post check scroll in the next run loop, so that the callback methods will be invoked + // after the layout pass. This way a new layout pass will be scheduled if view + // properties are changed in the callbacks. + post(mCheckScrollRunnable); + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + if (oldt != t) { + checkScroll(); + } + } + + private void checkScroll() { + if (mListener != null) { + if (getScrollY() >= mScrollThreshold) { + mListener.onScrolledToBottom(); + } else if (!mRequiringScroll) { + mRequiringScroll = true; + mListener.onRequiresScroll(); + } + } + } + +} diff --git a/library/main/src/com/android/setupwizardlib/view/Illustration.java b/library/main/src/com/android/setupwizardlib/view/Illustration.java new file mode 100644 index 0000000..fd976bc --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/Illustration.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.util.LayoutDirection; +import android.view.Gravity; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; + +import com.android.setupwizardlib.R; + +/** + * Class to draw the illustration of setup wizard. The aspectRatio attribute determines the aspect + * ratio of the top padding, which is leaving space for the illustration. Draws an illustration + * (foreground) to fit the width of the view and fills the rest with the background. + * + * If an aspect ratio is set, then the aspect ratio of the source drawable is maintained. Otherwise + * the the aspect ratio will be ignored, only increasing the width of the illustration. + */ +public class Illustration extends FrameLayout { + + // Size of the baseline grid in pixels + private float mBaselineGridSize; + private Drawable mBackground; + private Drawable mIllustration; + private final Rect mViewBounds = new Rect(); + private final Rect mIllustrationBounds = new Rect(); + private float mScale = 1.0f; + private float mAspectRatio = 0.0f; + + public Illustration(Context context) { + this(context, null); + } + + public Illustration(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Illustration(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwIllustration, defStyleAttr, 0); + mAspectRatio = a.getFloat(R.styleable.SuwIllustration_suwAspectRatio, 0.0f); + a.recycle(); + } + // Number of pixels of the 8dp baseline grid as defined in material design specs + mBaselineGridSize = getResources().getDisplayMetrics().density * 8; + setWillNotDraw(false); + } + + /** + * The background will be drawn to fill up the rest of the view. It will also be scaled by the + * same amount as the foreground so their textures look the same. + */ + // Override the deprecated setBackgroundDrawable method to support API < 16. View.setBackground + // forwards to setBackgroundDrawable in the framework implementation. + @SuppressWarnings("deprecation") + @Override + public void setBackgroundDrawable(Drawable background) { + if (background == mBackground) { + return; + } + mBackground = background; + invalidate(); + requestLayout(); + } + + /** + * Sets the drawable used as the illustration. The drawable is expected to have intrinsic + * width and height defined and will be scaled to fit the width of the view. + */ + public void setIllustration(Drawable illustration) { + if (illustration == mIllustration) { + return; + } + mIllustration = illustration; + invalidate(); + requestLayout(); + } + + @Override + @Deprecated + public void setForeground(Drawable d) { + setIllustration(d); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mAspectRatio != 0.0f) { + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int illustrationHeight = (int) (parentWidth / mAspectRatio); + illustrationHeight -= illustrationHeight % mBaselineGridSize; + setPadding(0, illustrationHeight, 0, 0); + } + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + setOutlineProvider(ViewOutlineProvider.BOUNDS); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int layoutWidth = right - left; + final int layoutHeight = bottom - top; + if (mIllustration != null) { + int intrinsicWidth = mIllustration.getIntrinsicWidth(); + int intrinsicHeight = mIllustration.getIntrinsicHeight(); + + mViewBounds.set(0, 0, layoutWidth, layoutHeight); + if (mAspectRatio != 0f) { + mScale = layoutWidth / (float) intrinsicWidth; + intrinsicWidth = layoutWidth; + intrinsicHeight = (int) (intrinsicHeight * mScale); + } + Gravity.apply(Gravity.FILL_HORIZONTAL | Gravity.TOP, intrinsicWidth, + intrinsicHeight, mViewBounds, mIllustrationBounds); + mIllustration.setBounds(mIllustrationBounds); + } + if (mBackground != null) { + // Scale the background bounds by the same scale to compensate for the scale done to the + // canvas in onDraw. + mBackground.setBounds(0, 0, (int) Math.ceil(layoutWidth / mScale), + (int) Math.ceil((layoutHeight - mIllustrationBounds.height()) / mScale)); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + public void onDraw(Canvas canvas) { + final int layoutDirection = getLayoutDirection(); + if (mBackground != null) { + // Draw the background filling parts not covered by the illustration + canvas.save(); + canvas.translate(0, mIllustrationBounds.height()); + // Scale the background so its size matches the foreground + canvas.scale(mScale, mScale, 0, 0); + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + if (layoutDirection == LayoutDirection.RTL && mBackground.isAutoMirrored()) { + // Flip the illustration for RTL layouts + canvas.scale(-1, 1); + canvas.translate(-mBackground.getBounds().width(), 0); + } + } + mBackground.draw(canvas); + canvas.restore(); + } + if (mIllustration != null) { + canvas.save(); + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + if (layoutDirection == LayoutDirection.RTL && mIllustration.isAutoMirrored()) { + // Flip the illustration for RTL layouts + canvas.scale(-1, 1); + canvas.translate(-mIllustrationBounds.width(), 0); + } + } + // Draw the illustration + mIllustration.draw(canvas); + canvas.restore(); + } + super.onDraw(canvas); + } +} diff --git a/library/main/src/com/android/setupwizardlib/view/NavigationBar.java b/library/main/src/com/android/setupwizardlib/view/NavigationBar.java new file mode 100644 index 0000000..fc99768 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/NavigationBar.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.android.setupwizardlib.R; + +public class NavigationBar extends LinearLayout implements View.OnClickListener { + + public interface NavigationBarListener { + void onNavigateBack(); + void onNavigateNext(); + } + + private static int getNavbarTheme(Context context) { + // Normally we can automatically guess the theme by comparing the foreground color against + // the background color. But we also allow specifying explicitly using suwNavBarTheme. + TypedArray attributes = context.obtainStyledAttributes( + new int[] { + R.attr.suwNavBarTheme, + android.R.attr.colorForeground, + android.R.attr.colorBackground }); + int theme = attributes.getResourceId(0, 0); + if (theme == 0) { + // Compare the value of the foreground against the background color to see if current + // theme is light-on-dark or dark-on-light. + float[] foregroundHsv = new float[3]; + float[] backgroundHsv = new float[3]; + Color.colorToHSV(attributes.getColor(1, 0), foregroundHsv); + Color.colorToHSV(attributes.getColor(2, 0), backgroundHsv); + boolean isDarkBg = foregroundHsv[2] > backgroundHsv[2]; + theme = isDarkBg ? R.style.SuwNavBarThemeDark : R.style.SuwNavBarThemeLight; + } + attributes.recycle(); + return theme; + } + + private static Context getThemedContext(Context context) { + final int theme = getNavbarTheme(context); + return new ContextThemeWrapper(context, theme); + } + + private Button mNextButton; + private Button mBackButton; + private NavigationBarListener mListener; + + public NavigationBar(Context context) { + this(context, null); + } + + public NavigationBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NavigationBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(getThemedContext(context), attrs, defStyleAttr); + View.inflate(getContext(), R.layout.suw_navbar_view, this); + mNextButton = (Button) findViewById(R.id.suw_navbar_next); + mBackButton = (Button) findViewById(R.id.suw_navbar_back); + } + + public Button getBackButton() { + return mBackButton; + } + + public Button getNextButton() { + return mNextButton; + } + + public void setNavigationBarListener(NavigationBarListener listener) { + mListener = listener; + if (mListener != null) { + getBackButton().setOnClickListener(this); + getNextButton().setOnClickListener(this); + } + } + + @Override + public void onClick(View view) { + if (mListener != null) { + if (view == getBackButton()) { + mListener.onNavigateBack(); + } else if (view == getNextButton()) { + mListener.onNavigateNext(); + } + } + } + + public static class NavButton extends Button { + + public NavButton(Context context) { + this(context, null); + } + + public NavButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + // The color of the button is #de000000 / #deffffff when enabled. When disabled, apply + // additional 23% alpha, so the overall opacity is 20%. + setAlpha(enabled ? 1.0f : 0.23f); + } + } + +} diff --git a/library/main/src/com/android/setupwizardlib/view/StickyHeaderListView.java b/library/main/src/com/android/setupwizardlib/view/StickyHeaderListView.java new file mode 100644 index 0000000..5cb8b28 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/StickyHeaderListView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowInsets; +import android.widget.ListView; + +import com.android.setupwizardlib.R; + +/** + * This class provides sticky header functionality in a list view, to use with + * SetupWizardIllustration. To use this, add a header tagged with "sticky", or a header tagged with + * "stickyContainer" and one of its child tagged as "sticky". The sticky container will be drawn + * when the sticky element hits the top of the view. + * + * There are a few things to note: + * 1. The two supported scenarios are StickyHeaderListView -> Header (stickyContainer) -> sticky, + * and StickyHeaderListView -> Header (sticky). The arrow (->) represents parent/child + * relationship and must be immediate child. + * 2. The view does not work well with padding. b/16190933 + * 3. If fitsSystemWindows is true, then this will offset the sticking position by the height of + * the system decorations at the top of the screen. + * + * @see StickyHeaderScrollView + */ +public class StickyHeaderListView extends ListView { + + private View mSticky; + private View mStickyContainer; + private int mStatusBarInset = 0; + private RectF mStickyRect = new RectF(); + + public StickyHeaderListView(Context context) { + this(context, null); + } + + public StickyHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public StickyHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public StickyHeaderListView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwStickyHeaderListView, defStyleAttr, defStyleRes); + int headerResId = a.getResourceId(R.styleable.SuwStickyHeaderListView_suwHeader, 0); + if (headerResId != 0) { + LayoutInflater inflater = LayoutInflater.from(context); + View header = inflater.inflate(headerResId, this, false); + addHeaderView(header); + } + a.recycle(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (mSticky == null) { + updateStickyView(); + } + } + + public void updateStickyView() { + mSticky = findViewWithTag("sticky"); + mStickyContainer = findViewWithTag("stickyContainer"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mStickyRect.contains(ev.getX(), ev.getY())) { + ev.offsetLocation(-mStickyRect.left, -mStickyRect.top); + return mStickyContainer.dispatchTouchEvent(ev); + } else { + return super.dispatchTouchEvent(ev); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mSticky != null) { + final int saveCount = canvas.save(); + // The view to draw when sticking to the top + final View drawTarget = mStickyContainer != null ? mStickyContainer : mSticky; + // The offset to draw the view at when sticky + final int drawOffset = mStickyContainer != null ? mSticky.getTop() : 0; + // Position of the draw target, relative to the outside of the scrollView + final int drawTop = drawTarget.getTop(); + if (drawTop + drawOffset < mStatusBarInset || !drawTarget.isShown()) { + // ListView does not translate the canvas, so we can simply draw at the top + mStickyRect.set(0, -drawOffset + mStatusBarInset, drawTarget.getWidth(), + drawTarget.getHeight() - drawOffset + mStatusBarInset); + canvas.translate(0, mStickyRect.top); + canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight()); + drawTarget.draw(canvas); + } else { + mStickyRect.setEmpty(); + } + canvas.restoreToCount(saveCount); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + if (getFitsSystemWindows()) { + mStatusBarInset = insets.getSystemWindowInsetTop(); + insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + 0, /* top */ + insets.getSystemWindowInsetRight(), + insets.getSystemWindowInsetBottom() + ); + } + return insets; + } +} diff --git a/library/main/src/com/android/setupwizardlib/view/StickyHeaderScrollView.java b/library/main/src/com/android/setupwizardlib/view/StickyHeaderScrollView.java new file mode 100644 index 0000000..a8c942f --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/view/StickyHeaderScrollView.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowInsets; + +/** + * This class provides sticky header functionality in a scroll view, to use with + * SetupWizardIllustration. To use this, add a subview tagged with "sticky", or a subview tagged + * with "stickyContainer" and one of its child tagged as "sticky". The sticky container will be + * drawn when the sticky element hits the top of the view. + * + * There are a few things to note: + * 1. The two supported scenarios are StickyHeaderScrollView -> subview (stickyContainer) -> sticky, + * and StickyHeaderScrollView -> container -> subview (sticky). + * The arrow (->) represents parent/child relationship and must be immediate child. + * 2. The view does not work well with padding. b/16190933 + * 3. If fitsSystemWindows is true, then this will offset the sticking position by the height of + * the system decorations at the top of the screen. + * + * @see StickyHeaderListView + */ +public class StickyHeaderScrollView extends BottomScrollView { + + private View mSticky; + private View mStickyContainer; + private int mStatusBarInset = 0; + private RectF mStickyRect = new RectF(); + + public StickyHeaderScrollView(Context context) { + super(context); + } + + public StickyHeaderScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public StickyHeaderScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (mSticky == null) { + updateStickyView(); + } + } + + public void updateStickyView() { + mSticky = findViewWithTag("sticky"); + mStickyContainer = findViewWithTag("stickyContainer"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mStickyRect.contains(ev.getX(), ev.getY())) { + ev.offsetLocation(-mStickyRect.left, -mStickyRect.top); + return mStickyContainer.dispatchTouchEvent(ev); + } else { + return super.dispatchTouchEvent(ev); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mSticky != null) { + final int saveCount = canvas.save(); + // The view to draw when sticking to the top + final View drawTarget = mStickyContainer != null ? mStickyContainer : mSticky; + // The offset to draw the view at when sticky + final int drawOffset = mStickyContainer != null ? mSticky.getTop() : 0; + // Position of the draw target, relative to the outside of the scrollView + final int drawTop = drawTarget.getTop() - getScrollY(); + if (drawTop + drawOffset < mStatusBarInset || !drawTarget.isShown()) { + // ScrollView translates the whole canvas so we have to compensate for that + mStickyRect.set(0, -drawOffset + mStatusBarInset, drawTarget.getWidth(), + drawTarget.getHeight() - drawOffset + mStatusBarInset); + canvas.translate(0, -drawTop + mStickyRect.top); + canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight()); + drawTarget.draw(canvas); + } else { + mStickyRect.setEmpty(); + } + canvas.restoreToCount(saveCount); + } + onDrawScrollBars(canvas); + } + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + if (getFitsSystemWindows()) { + mStatusBarInset = insets.getSystemWindowInsetTop(); + insets = insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + 0, /* top */ + insets.getSystemWindowInsetRight(), + insets.getSystemWindowInsetBottom() + ); + } + return insets; + } +} -- cgit v1.2.3