diff options
author | Maurice Lam <yukl@google.com> | 2017-03-28 12:48:40 -0700 |
---|---|---|
committer | Maurice Lam <yukl@google.com> | 2017-03-28 14:23:21 -0700 |
commit | 83862bb59558fc044de9aa0d6e9407be53af8b81 (patch) | |
tree | 032855cc188420699c048478a6a24c44731a3151 /library/gingerbread/src | |
parent | 9955331ed7bda114488b1a4701456ec478ff63bf (diff) | |
download | setupwizard-83862bb59558fc044de9aa0d6e9407be53af8b81.tar.gz |
Rename SuwLib directories
Rename eclair-mr1 to gingerbread to reflect the min SDK version
change, and full-support to recyclerview to better reflect what's
inside the directory
Also added comments and applied style fixes to keep checkstyle happy.
Test: Existing tests pass
Change-Id: I20332f718f2aae04092d5e45de944b1efce1a596
Diffstat (limited to 'library/gingerbread/src')
5 files changed, 867 insertions, 0 deletions
diff --git a/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java b/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java new file mode 100644 index 0000000..be9916e --- /dev/null +++ b/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2017 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.items; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.PorterDuff.Mode; +import android.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.TextView; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.view.CheckableLinearLayout; + +/** + * A switch item which is divided into two parts: the start (left for LTR) side shows the title and + * summary, and when that is clicked, will expand to show a longer summary. The end (right for LTR) + * side is a switch which can be toggled by the user. + * + * Note: It is highly recommended to use this item with recycler view rather than list view, because + * list view draws the touch ripple effect on top of the item, rather than letting the item handle + * it. Therefore you might see a double-ripple, one for the expandable area and one for the entire + * list item, when using this in list view. + */ +public class ExpandableSwitchItem extends SwitchItem + implements OnCheckedChangeListener, OnClickListener { + + private CharSequence mCollapsedSummary; + private CharSequence mExpandedSummary; + private boolean mIsExpanded = false; + + public ExpandableSwitchItem() { + super(); + } + + public ExpandableSwitchItem(Context context, AttributeSet attrs) { + super(context, attrs); + final TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.SuwExpandableSwitchItem); + mCollapsedSummary = a.getText(R.styleable.SuwExpandableSwitchItem_suwCollapsedSummary); + mExpandedSummary = a.getText(R.styleable.SuwExpandableSwitchItem_suwExpandedSummary); + a.recycle(); + } + + @Override + protected int getDefaultLayoutResource() { + return R.layout.suw_items_expandable_switch; + } + + @Override + public CharSequence getSummary() { + return mIsExpanded ? getExpandedSummary() : getCollapsedSummary(); + } + + /** + * @return True if the item is currently expanded. + */ + public boolean isExpanded() { + return mIsExpanded; + } + + /** + * Sets whether the item should be expanded. + */ + public void setExpanded(boolean expanded) { + if (mIsExpanded == expanded) { + return; + } + mIsExpanded = expanded; + notifyItemChanged(); + } + + /** + * @return The summary shown when in collapsed state. + */ + public CharSequence getCollapsedSummary() { + return mCollapsedSummary; + } + + /** + * Sets the summary text shown when the item is collapsed. Corresponds to the + * {@code app:suwCollapsedSummary} XML attribute. + */ + public void setCollapsedSummary(CharSequence collapsedSummary) { + mCollapsedSummary = collapsedSummary; + if (!isExpanded()) { + notifyChanged(); + } + } + + /** + * @return The summary shown when in expanded state. + */ + public CharSequence getExpandedSummary() { + return mExpandedSummary; + } + + /** + * Sets the summary text shown when the item is expanded. Corresponds to the + * {@code app:suwExpandedSummary} XML attribute. + */ + public void setExpandedSummary(CharSequence expandedSummary) { + mExpandedSummary = expandedSummary; + if (isExpanded()) { + notifyChanged(); + } + } + + @Override + public void onBindView(View view) { + // TODO: If it is possible to detect, log a warning if this is being used with ListView. + super.onBindView(view); + View content = view.findViewById(R.id.suw_items_expandable_switch_content); + content.setOnClickListener(this); + + if (content instanceof CheckableLinearLayout) { + ((CheckableLinearLayout) content).setChecked(isExpanded()); + } + + tintCompoundDrawables(view); + } + + @Override + public void onClick(View v) { + setExpanded(!isExpanded()); + } + + // Tint the expand arrow with the text color + private void tintCompoundDrawables(View view) { + final TypedArray a = view.getContext() + .obtainStyledAttributes(new int[] {android.R.attr.textColorPrimary}); + final ColorStateList tintColor = a.getColorStateList(0); + a.recycle(); + + if (tintColor != null) { + TextView titleView = (TextView) view.findViewById(R.id.suw_items_title); + for (Drawable drawable : titleView.getCompoundDrawables()) { + if (drawable != null) { + drawable.setColorFilter(tintColor.getDefaultColor(), Mode.SRC_IN); + } + } + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { + for (Drawable drawable : titleView.getCompoundDrawablesRelative()) { + if (drawable != null) { + drawable.setColorFilter(tintColor.getDefaultColor(), Mode.SRC_IN); + } + } + } + + } + } +} diff --git a/library/gingerbread/src/com/android/setupwizardlib/items/SwitchItem.java b/library/gingerbread/src/com/android/setupwizardlib/items/SwitchItem.java new file mode 100644 index 0000000..7459d77 --- /dev/null +++ b/library/gingerbread/src/com/android/setupwizardlib/items/SwitchItem.java @@ -0,0 +1,133 @@ +/* + * 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.items; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v7.widget.SwitchCompat; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CompoundButton; + +import com.android.setupwizardlib.R; + +/** + * An item that is displayed with a switch, with methods to manipulate and listen to the checked + * state of the switch. Note that by default, only click on the switch will change the on-off state. + * To change the switch state when tapping on the text, use the click handlers of list view or + * RecyclerItemAdapter with {@link #toggle(View)}. + */ +public class SwitchItem extends Item implements CompoundButton.OnCheckedChangeListener { + + /** + * Listener for check state changes of this switch item. + */ + public interface OnCheckedChangeListener { + + /** + * Callback when checked state of a {@link SwitchItem} is changed. + * + * @see #setOnCheckedChangeListener(OnCheckedChangeListener) + */ + void onCheckedChange(SwitchItem item, boolean isChecked); + } + + private boolean mChecked = false; + private OnCheckedChangeListener mListener; + + /** + * Creates a default switch item. + */ + public SwitchItem() { + super(); + } + + /** + * Creates a switch item. This constructor is used for inflation from XML. + * + * @param context The context which this item is inflated in. + * @param attrs The XML attributes defined on the item. + */ + public SwitchItem(Context context, AttributeSet attrs) { + super(context, attrs); + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwSwitchItem); + mChecked = a.getBoolean(R.styleable.SuwSwitchItem_android_checked, false); + a.recycle(); + } + + /** + * Sets whether this item should be checked. + */ + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + notifyItemChanged(); + if (mListener != null) { + mListener.onCheckedChange(this, checked); + } + } + } + + /** + * @return True if this switch item is currently checked. + */ + public boolean isChecked() { + return mChecked; + } + + @Override + protected int getDefaultLayoutResource() { + return R.layout.suw_items_switch; + } + + /** + * Toggle the checked state of the switch, without invalidating the entire item. + * + * @param view The root view of this item, typically from the argument of onItemClick. + */ + public void toggle(View view) { + mChecked = !mChecked; + final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch); + switchView.setChecked(mChecked); + } + + @Override + public void onBindView(View view) { + super.onBindView(view); + final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch); + switchView.setOnCheckedChangeListener(null); + switchView.setChecked(mChecked); + switchView.setOnCheckedChangeListener(this); + switchView.setEnabled(isEnabled()); + } + + /** + * Sets a listener to listen for changes in checked state. This listener is invoked in both + * user toggling the switch and calls to {@link #setChecked(boolean)}. + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mListener = listener; + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mChecked = isChecked; + if (mListener != null) { + mListener.onCheckedChange(this, isChecked); + } + } +} diff --git a/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java b/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java new file mode 100644 index 0000000..e6fa497 --- /dev/null +++ b/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2016 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.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import java.util.List; + +/** + * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and + * clicked by accessibility services. + * + * <p />Sample usage: + * <pre> + * LinkAccessibilityHelper mAccessibilityHelper; + * + * private void init() { + * mAccessibilityHelper = new LinkAccessibilityHelper(myTextView); + * ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper); + * } + * + * {@literal @}Override + * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) { + * if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { + * return true; + * } + * return super.dispatchHoverEvent(event); + * } + * </pre> + * + * @see com.android.setupwizardlib.view.RichTextView + * @see android.support.v4.widget.ExploreByTouchHelper + */ +public class LinkAccessibilityHelper extends ExploreByTouchHelper { + + private static final String TAG = "LinkAccessibilityHelper"; + + private final TextView mView; + private final Rect mTempRect = new Rect(); + + public LinkAccessibilityHelper(TextView view) { + super(view); + mView = view; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + final int offset = getOffsetForPosition(mView, x, y); + ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (linkSpans.length == 1) { + ClickableSpan linkSpan = linkSpans[0]; + return spannedText.getSpanStart(linkSpan); + } + } + return INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), + ClickableSpan.class); + for (ClickableSpan span : linkSpans) { + virtualViewIds.add(spannedText.getSpanStart(span)); + } + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + event.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + event.setContentDescription(mView.getText()); + } + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat info) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + info.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + info.setContentDescription(mView.getText()); + } + info.setFocusable(true); + info.setClickable(true); + getBoundsForSpan(span, mTempRect); + if (mTempRect.isEmpty()) { + Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); + mTempRect.set(0, 0, 1, 1); + } + info.setBoundsInParent(mTempRect); + info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + span.onClick(mView); + return true; + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + } + } + return false; + } + + private ClickableSpan getSpanForOffset(int offset) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (spans.length == 1) { + return spans[0]; + } + } + return null; + } + + private CharSequence getTextForSpan(ClickableSpan span) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + return spannedText.subSequence(spannedText.getSpanStart(span), + spannedText.getSpanEnd(span)); + } + return text; + } + + // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the + // section on the first line. + private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { + CharSequence text = mView.getText(); + outRect.setEmpty(); + if (text instanceof Spanned) { + final Layout layout = mView.getLayout(); + if (layout != null) { + Spanned spannedText = (Spanned) text; + final int spanStart = spannedText.getSpanStart(span); + final int spanEnd = spannedText.getSpanEnd(span); + final float xStart = layout.getPrimaryHorizontal(spanStart); + final float xEnd = layout.getPrimaryHorizontal(spanEnd); + final int lineStart = layout.getLineForOffset(spanStart); + final int lineEnd = layout.getLineForOffset(spanEnd); + layout.getLineBounds(lineStart, outRect); + if (lineEnd == lineStart) { + // If the span is on a single line, adjust both the left and right bounds + // so outrect is exactly bounding the span. + outRect.left = (int) Math.min(xStart, xEnd); + outRect.right = (int) Math.max(xStart, xEnd); + } else { + // If the span wraps across multiple lines, only use the first line (as returned + // by layout.getLineBounds above), and adjust the "start" of outrect to where + // the span starts, leaving the "end" of outrect at the end of the line. + // ("start" being left for LTR, and right for RTL) + if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) { + outRect.right = (int) xStart; + } else { + outRect.left = (int) xStart; + } + } + + // Offset for padding + outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); + } + } + return outRect; + } + + // Compat implementation of TextView#getOffsetForPosition(). + + private static int getOffsetForPosition(TextView view, float x, float y) { + if (view.getLayout() == null) return -1; + final int line = getLineAtCoordinate(view, y); + return getOffsetAtCoordinate(view, line, x); + } + + private static float convertToLocalHorizontalCoordinate(TextView view, float x) { + x -= view.getTotalPaddingLeft(); + // Clamp the position to inside of the view. + x = Math.max(0.0f, x); + x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x); + x += view.getScrollX(); + return x; + } + + private static int getLineAtCoordinate(TextView view, float y) { + y -= view.getTotalPaddingTop(); + // Clamp the position to inside of the view. + y = Math.max(0.0f, y); + y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y); + y += view.getScrollY(); + return view.getLayout().getLineForVertical((int) y); + } + + private static int getOffsetAtCoordinate(TextView view, int line, float x) { + x = convertToLocalHorizontalCoordinate(view, x); + return view.getLayout().getOffsetForHorizontal(line, x); + } +} diff --git a/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java b/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java new file mode 100644 index 0000000..5172c47 --- /dev/null +++ b/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java @@ -0,0 +1,159 @@ +/* + * 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.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * Button for navigation bar, which includes tinting of its compound drawables to be used for dark + * and light themes. + */ +public class NavigationBarButton extends Button { + + public NavigationBarButton(Context context) { + super(context); + init(); + } + + public NavigationBarButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + // Unfortunately, drawableStart and drawableEnd set through XML does not call the setter, + // so manually getting it and wrapping it in the compat drawable. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Drawable[] drawables = getCompoundDrawablesRelative(); + for (int i = 0; i < drawables.length; i++) { + if (drawables[i] != null) { + drawables[i] = TintedDrawable.wrap(drawables[i]); + } + } + setCompoundDrawablesRelativeWithIntrinsicBounds(drawables[0], drawables[1], + drawables[2], drawables[3]); + } + } + + @Override + public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { + if (left != null) left = TintedDrawable.wrap(left); + if (top != null) top = TintedDrawable.wrap(top); + if (right != null) right = TintedDrawable.wrap(right); + if (bottom != null) bottom = TintedDrawable.wrap(bottom); + super.setCompoundDrawables(left, top, right, bottom); + tintDrawables(); + } + + @Override + public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, + Drawable bottom) { + if (start != null) start = TintedDrawable.wrap(start); + if (top != null) top = TintedDrawable.wrap(top); + if (end != null) end = TintedDrawable.wrap(end); + if (bottom != null) bottom = TintedDrawable.wrap(bottom); + super.setCompoundDrawablesRelative(start, top, end, bottom); + tintDrawables(); + } + + @Override + public void setTextColor(ColorStateList colors) { + super.setTextColor(colors); + tintDrawables(); + } + + private void tintDrawables() { + final ColorStateList textColors = getTextColors(); + if (textColors != null) { + for (Drawable drawable : getAllCompoundDrawables()) { + if (drawable instanceof TintedDrawable) { + ((TintedDrawable) drawable).setTintListCompat(textColors); + } + } + invalidate(); + } + } + + private Drawable[] getAllCompoundDrawables() { + Drawable[] drawables = new Drawable[6]; + Drawable[] compoundDrawables = getCompoundDrawables(); + drawables[0] = compoundDrawables[0]; // left + drawables[1] = compoundDrawables[1]; // top + drawables[2] = compoundDrawables[2]; // right + drawables[3] = compoundDrawables[3]; // bottom + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Drawable[] compoundDrawablesRelative = getCompoundDrawablesRelative(); + drawables[4] = compoundDrawablesRelative[0]; // start + drawables[5] = compoundDrawablesRelative[2]; // end + } + return drawables; + } + + // TODO: Remove this class and use DrawableCompat.wrap() once we can use support library 22.1.0 + // or above + private static class TintedDrawable extends LayerDrawable { + + public static TintedDrawable wrap(Drawable drawable) { + if (drawable instanceof TintedDrawable) { + return (TintedDrawable) drawable; + } + return new TintedDrawable(drawable.mutate()); + } + + private ColorStateList mTintList = null; + + TintedDrawable(Drawable wrapped) { + super(new Drawable[] { wrapped }); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + public boolean setState(@NonNull int[] stateSet) { + boolean needsInvalidate = super.setState(stateSet); + boolean needsInvalidateForState = updateState(); + return needsInvalidate || needsInvalidateForState; + } + + public void setTintListCompat(ColorStateList colors) { + mTintList = colors; + if (updateState()) { + invalidateSelf(); + } + } + + private boolean updateState() { + if (mTintList != null) { + final int color = mTintList.getColorForState(getState(), 0); + setColorFilter(color, PorterDuff.Mode.SRC_IN); + return true; // Needs invalidate + } + return false; + } + } +} diff --git a/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java b/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java new file mode 100644 index 0000000..6ccedf0 --- /dev/null +++ b/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016 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.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.v4.view.ViewCompat; +import android.text.Annotation; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.TextAppearanceSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.TextView; + +import com.android.setupwizardlib.span.LinkSpan; +import com.android.setupwizardlib.span.SpanHelper; +import com.android.setupwizardlib.util.LinkAccessibilityHelper; + +/** + * An extension of TextView that automatically replaces the annotation tags as specified in + * {@link SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} + */ +public class RichTextView extends TextView { + + /* static section */ + + private static final String TAG = "RichTextView"; + + private static final String ANNOTATION_LINK = "link"; + private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance"; + + /** + * Replace <annotation> tags in strings to become their respective types. Currently 2 + * types are supported: + * <ol> + * <li><annotation link="foobar"> will create a + * {@link com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key + * "foobar"</li> + * <li><annotation textAppearance="TextAppearance.FooBar"> will create a + * {@link android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar</li> + * </ol> + */ + public static CharSequence getRichText(Context context, CharSequence text) { + if (text instanceof Spanned) { + final SpannableString spannable = new SpannableString(text); + final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class); + for (Annotation span : spans) { + final String key = span.getKey(); + if (ANNOTATION_TEXT_APPEARANCE.equals(key)) { + String textAppearance = span.getValue(); + final int style = context.getResources() + .getIdentifier(textAppearance, "style", context.getPackageName()); + if (style == 0) { + Log.w(TAG, "Cannot find resource: " + style); + } + final TextAppearanceSpan textAppearanceSpan = + new TextAppearanceSpan(context, style); + SpanHelper.replaceSpan(spannable, span, textAppearanceSpan); + } else if (ANNOTATION_LINK.equals(key)) { + LinkSpan link = new LinkSpan(span.getValue()); + SpanHelper.replaceSpan(spannable, span, link); + } + } + return spannable; + } + return text; + } + + /* non-static section */ + + private LinkAccessibilityHelper mAccessibilityHelper; + + public RichTextView(Context context) { + super(context); + init(); + } + + public RichTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + mAccessibilityHelper = new LinkAccessibilityHelper(this); + ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); + } + + @Override + public void setText(CharSequence text, BufferType type) { + text = getRichText(getContext(), text); + // Set text first before doing anything else because setMovementMethod internally calls + // setText. This in turn ends up calling this method with mText as the first parameter + super.setText(text, type); + boolean hasLinks = hasLinks(text); + + if (hasLinks) { + // When a TextView has a movement method, it will set the view to clickable. This makes + // View.onTouchEvent always return true and consumes the touch event, essentially + // nullifying any return values of MovementMethod.onTouchEvent. + // To still allow propagating touch events to the parent when this view doesn't have + // links, we only set the movement method here if the text contains links. + setMovementMethod(LinkMovementMethod.getInstance()); + } else { + setMovementMethod(null); + } + // ExploreByTouchHelper automatically enables focus for RichTextView + // even though it may not have any links. Causes problems during talkback + // as individual TextViews consume touch events and thereby reducing the focus window + // shown by Talkback. Disable focus if there are no links + setFocusable(hasLinks); + } + + private boolean hasLinks(CharSequence text) { + if (text instanceof Spanned) { + final ClickableSpan[] spans = + ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); + return spans.length > 0; + } + return false; + } + + @Override + protected boolean dispatchHoverEvent(MotionEvent event) { + if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { + // b/26765507 causes drawableStart and drawableEnd to not get the right state on M. As a + // workaround, set the state on those drawables directly. + final int[] state = getDrawableState(); + for (Drawable drawable : getCompoundDrawablesRelative()) { + if (drawable != null) { + if (drawable.setState(state)) { + invalidateDrawable(drawable); + } + } + } + } + } +} |