diff options
Diffstat (limited to 'library/main/src/com/android/setupwizardlib/view')
5 files changed, 685 insertions, 0 deletions
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; + } +} |