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.support.v4.view.ViewCompat; 21 import android.text.Annotation; 22 import android.text.SpannableString; 23 import android.text.Spanned; 24 import android.text.method.LinkMovementMethod; 25 import android.text.style.ClickableSpan; 26 import android.text.style.TextAppearanceSpan; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 import android.widget.TextView; 31 32 import com.android.setupwizardlib.span.LinkSpan; 33 import com.android.setupwizardlib.span.SpanHelper; 34 import com.android.setupwizardlib.util.LinkAccessibilityHelper; 35 36 /** 37 * An extension of TextView that automatically replaces the annotation tags as specified in 38 * {@link SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} 39 */ 40 public class RichTextView extends TextView { 41 42 /* static section */ 43 44 private static final String TAG = "RichTextView"; 45 46 private static final String ANNOTATION_LINK = "link"; 47 private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance"; 48 49 /** 50 * Replace <annotation> tags in strings to become their respective types. Currently 2 51 * types are supported: 52 * <ol> 53 * <li><annotation link="foobar"> will create a 54 * {@link com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key 55 * "foobar"</li> 56 * <li><annotation textAppearance="TextAppearance.FooBar"> will create a 57 * {@link android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar</li> 58 * </ol> 59 */ getRichText(Context context, CharSequence text)60 public static CharSequence getRichText(Context context, CharSequence text) { 61 if (text instanceof Spanned) { 62 final SpannableString spannable = new SpannableString(text); 63 final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class); 64 for (Annotation span : spans) { 65 final String key = span.getKey(); 66 if (ANNOTATION_TEXT_APPEARANCE.equals(key)) { 67 String textAppearance = span.getValue(); 68 final int style = context.getResources() 69 .getIdentifier(textAppearance, "style", context.getPackageName()); 70 if (style == 0) { 71 Log.w(TAG, "Cannot find resource: " + style); 72 } 73 final TextAppearanceSpan textAppearanceSpan = 74 new TextAppearanceSpan(context, style); 75 SpanHelper.replaceSpan(spannable, span, textAppearanceSpan); 76 } else if (ANNOTATION_LINK.equals(key)) { 77 LinkSpan link = new LinkSpan(span.getValue()); 78 SpanHelper.replaceSpan(spannable, span, link); 79 } 80 } 81 return spannable; 82 } 83 return text; 84 } 85 86 /* non-static section */ 87 88 private LinkAccessibilityHelper mAccessibilityHelper; 89 RichTextView(Context context)90 public RichTextView(Context context) { 91 super(context); 92 init(); 93 } 94 RichTextView(Context context, AttributeSet attrs)95 public RichTextView(Context context, AttributeSet attrs) { 96 super(context, attrs); 97 init(); 98 } 99 init()100 private void init() { 101 mAccessibilityHelper = new LinkAccessibilityHelper(this); 102 ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); 103 } 104 105 @Override setText(CharSequence text, BufferType type)106 public void setText(CharSequence text, BufferType type) { 107 text = getRichText(getContext(), text); 108 // Set text first before doing anything else because setMovementMethod internally calls 109 // setText. This in turn ends up calling this method with mText as the first parameter 110 super.setText(text, type); 111 boolean hasLinks = hasLinks(text); 112 113 if (hasLinks) { 114 // When a TextView has a movement method, it will set the view to clickable. This makes 115 // View.onTouchEvent always return true and consumes the touch event, essentially 116 // nullifying any return values of MovementMethod.onTouchEvent. 117 // To still allow propagating touch events to the parent when this view doesn't have 118 // links, we only set the movement method here if the text contains links. 119 setMovementMethod(LinkMovementMethod.getInstance()); 120 } else { 121 setMovementMethod(null); 122 } 123 // ExploreByTouchHelper automatically enables focus for RichTextView 124 // even though it may not have any links. Causes problems during talkback 125 // as individual TextViews consume touch events and thereby reducing the focus window 126 // shown by Talkback. Disable focus if there are no links 127 setFocusable(hasLinks); 128 } 129 hasLinks(CharSequence text)130 private boolean hasLinks(CharSequence text) { 131 if (text instanceof Spanned) { 132 final ClickableSpan[] spans = 133 ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); 134 return spans.length > 0; 135 } 136 return false; 137 } 138 139 @Override dispatchHoverEvent(MotionEvent event)140 protected boolean dispatchHoverEvent(MotionEvent event) { 141 if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { 142 return true; 143 } 144 return super.dispatchHoverEvent(event); 145 } 146 } 147