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.graphics.drawable.Drawable; 21 import android.os.Build.VERSION; 22 import android.os.Build.VERSION_CODES; 23 import android.support.v4.view.ViewCompat; 24 import android.text.Annotation; 25 import android.text.SpannableString; 26 import android.text.Spanned; 27 import android.text.method.LinkMovementMethod; 28 import android.text.style.ClickableSpan; 29 import android.text.style.TextAppearanceSpan; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.MotionEvent; 33 import android.widget.TextView; 34 35 import com.android.setupwizardlib.span.LinkSpan; 36 import com.android.setupwizardlib.span.SpanHelper; 37 import com.android.setupwizardlib.util.LinkAccessibilityHelper; 38 39 /** 40 * An extension of TextView that automatically replaces the annotation tags as specified in 41 * {@link SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} 42 */ 43 public class RichTextView extends TextView { 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 */ getRichText(Context context, CharSequence text)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 LinkAccessibilityHelper mAccessibilityHelper; 92 RichTextView(Context context)93 public RichTextView(Context context) { 94 super(context); 95 init(); 96 } 97 RichTextView(Context context, AttributeSet attrs)98 public RichTextView(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 init(); 101 } 102 init()103 private void init() { 104 mAccessibilityHelper = new LinkAccessibilityHelper(this); 105 ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); 106 } 107 108 @Override setText(CharSequence text, BufferType type)109 public void setText(CharSequence text, BufferType type) { 110 text = getRichText(getContext(), text); 111 // Set text first before doing anything else because setMovementMethod internally calls 112 // setText. This in turn ends up calling this method with mText as the first parameter 113 super.setText(text, type); 114 boolean hasLinks = hasLinks(text); 115 116 if (hasLinks) { 117 // When a TextView has a movement method, it will set the view to clickable. This makes 118 // View.onTouchEvent always return true and consumes the touch event, essentially 119 // nullifying any return values of MovementMethod.onTouchEvent. 120 // To still allow propagating touch events to the parent when this view doesn't have 121 // links, we only set the movement method here if the text contains links. 122 setMovementMethod(LinkMovementMethod.getInstance()); 123 } else { 124 setMovementMethod(null); 125 } 126 // ExploreByTouchHelper automatically enables focus for RichTextView 127 // even though it may not have any links. Causes problems during talkback 128 // as individual TextViews consume touch events and thereby reducing the focus window 129 // shown by Talkback. Disable focus if there are no links 130 setFocusable(hasLinks); 131 } 132 hasLinks(CharSequence text)133 private boolean hasLinks(CharSequence text) { 134 if (text instanceof Spanned) { 135 final ClickableSpan[] spans = 136 ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); 137 return spans.length > 0; 138 } 139 return false; 140 } 141 142 @Override dispatchHoverEvent(MotionEvent event)143 protected boolean dispatchHoverEvent(MotionEvent event) { 144 if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { 145 return true; 146 } 147 return super.dispatchHoverEvent(event); 148 } 149 150 @Override drawableStateChanged()151 protected void drawableStateChanged() { 152 super.drawableStateChanged(); 153 154 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 155 // b/26765507 causes drawableStart and drawableEnd to not get the right state on M. As a 156 // workaround, set the state on those drawables directly. 157 final int[] state = getDrawableState(); 158 for (Drawable drawable : getCompoundDrawablesRelative()) { 159 if (drawable != null) { 160 if (drawable.setState(state)) { 161 invalidateDrawable(drawable); 162 } 163 } 164 } 165 } 166 } 167 } 168