1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.setupwizardlib.view; 18 19 import android.content.Context; 20 import android.text.Annotation; 21 import android.text.SpannableString; 22 import android.text.Spanned; 23 import android.text.method.LinkMovementMethod; 24 import android.text.style.ClickableSpan; 25 import android.text.style.TextAppearanceSpan; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.widget.TextView; 29 30 import com.android.setupwizardlib.span.LinkSpan; 31 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener; 32 import com.android.setupwizardlib.span.SpanHelper; 33 34 /** 35 * An extension of TextView that automatically replaces the annotation tags as specified in 36 * {@link SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} 37 * 38 * <p>Note: The accessibility interaction for ClickableSpans (and therefore LinkSpans) are built 39 * into platform in O, although the interaction paradigm is different. (See b/17726921). In this 40 * platform version, the links are exposed in the Local Context Menu of TalkBack instead of 41 * accessible directly through swiping. 42 */ 43 public class RichTextView extends TextView implements OnLinkClickListener { 44 45 /* static section */ 46 47 private static final String TAG = "RichTextView"; 48 49 private static final String ANNOTATION_LINK = "link"; 50 private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance"; 51 52 /** 53 * Replace <annotation> tags in strings to become their respective types. Currently 2 54 * types are supported: 55 * <ol> 56 * <li><annotation link="foobar"> will create a 57 * {@link com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key 58 * "foobar"</li> 59 * <li><annotation textAppearance="TextAppearance.FooBar"> will create a 60 * {@link android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar</li> 61 * </ol> 62 */ 63 public static CharSequence getRichText(Context context, CharSequence text) { 64 if (text instanceof Spanned) { 65 final SpannableString spannable = new SpannableString(text); 66 final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class); 67 for (Annotation span : spans) { 68 final String key = span.getKey(); 69 if (ANNOTATION_TEXT_APPEARANCE.equals(key)) { 70 String textAppearance = span.getValue(); 71 final int style = context.getResources() 72 .getIdentifier(textAppearance, "style", context.getPackageName()); 73 if (style == 0) { 74 Log.w(TAG, "Cannot find resource: " + style); 75 } 76 final TextAppearanceSpan textAppearanceSpan = 77 new TextAppearanceSpan(context, style); 78 SpanHelper.replaceSpan(spannable, span, textAppearanceSpan); 79 } else if (ANNOTATION_LINK.equals(key)) { 80 LinkSpan link = new LinkSpan(span.getValue()); 81 SpanHelper.replaceSpan(spannable, span, link); 82 } 83 } 84 return spannable; 85 } 86 return text; 87 } 88 89 /* non-static section */ 90 91 private OnLinkClickListener mOnLinkClickListener; 92 93 public RichTextView(Context context) { 94 super(context); 95 } 96 97 public RichTextView(Context context, AttributeSet attrs) { 98 super(context, attrs); 99 } 100 101 @Override 102 public void setText(CharSequence text, BufferType type) { 103 text = getRichText(getContext(), text); 104 // Set text first before doing anything else because setMovementMethod internally calls 105 // setText. This in turn ends up calling this method with mText as the first parameter 106 super.setText(text, type); 107 boolean hasLinks = hasLinks(text); 108 109 if (hasLinks) { 110 // When a TextView has a movement method, it will set the view to clickable. This makes 111 // View.onTouchEvent always return true and consumes the touch event, essentially 112 // nullifying any return values of MovementMethod.onTouchEvent. 113 // To still allow propagating touch events to the parent when this view doesn't have 114 // links, we only set the movement method here if the text contains links. 115 setMovementMethod(LinkMovementMethod.getInstance()); 116 } else { 117 setMovementMethod(null); 118 } 119 // ExploreByTouchHelper automatically enables focus for RichTextView 120 // even though it may not have any links. Causes problems during talkback 121 // as individual TextViews consume touch events and thereby reducing the focus window 122 // shown by Talkback. Disable focus if there are no links 123 setFocusable(hasLinks); 124 } 125 126 private boolean hasLinks(CharSequence text) { 127 if (text instanceof Spanned) { 128 final ClickableSpan[] spans = 129 ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); 130 return spans.length > 0; 131 } 132 return false; 133 } 134 135 public void setOnLinkClickListener(OnLinkClickListener listener) { 136 mOnLinkClickListener = listener; 137 } 138 139 public OnLinkClickListener getOnLinkClickListener() { 140 return mOnLinkClickListener; 141 } 142 143 @Override 144 public boolean onLinkClick(LinkSpan span) { 145 if (mOnLinkClickListener != null) { 146 return mOnLinkClickListener.onLinkClick(span); 147 } 148 return false; 149 } 150 } 151